Skip to content

Commit

Permalink
feat: add isValidSignature
Browse files Browse the repository at this point in the history
  • Loading branch information
Hugoo committed Mar 11, 2022
1 parent e225f9f commit 6490751
Show file tree
Hide file tree
Showing 8 changed files with 245 additions and 22 deletions.
50 changes: 50 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,56 @@ describe('Running @erc725/erc725.js tests...', () => {
}
});

describe('isValidSignature', () => {
it('should return true if the signature is valid [mock HttpProvider]', async () => {
const provider = new HttpProvider({ returnData: [] }, [], true); // we mock a valid return response (magic number)
const erc725 = new ERC725(
[],
'0xD295E4748c1DFDFE028D7Dd2FEC3e52de2b1EB42', // result is mocked so we can use any address
provider,
);

const res = await erc725.isValidSignature(
'hello',
'0x6c54ad4814ed6de85b9786e79de48ad0d597a243158194fa6b3604254ff58f9c2e4ffcc080e18a68c8e813f720b893c8d47d6f757b9e288a5814263642811c1b1c',
);

assert.deepStrictEqual(res, true);
});

it('should return true if the signature is valid [mock EthereumProvider]', async () => {
const provider = new EthereumProvider({ returnData: [] }, [], true); // we mock a valid return response (magic number)
const erc725 = new ERC725(
[],
'0xD295E4748c1DFDFE028D7Dd2FEC3e52de2b1EB42', // result is mocked so we can use any address
provider,
);

const res = await erc725.isValidSignature(
'hello',
'0x6c54ad4814ed6de85b9786e79de48ad0d597a243158194fa6b3604254ff58f9c2e4ffcc080e18a68c8e813f720b893c8d47d6f757b9e288a5814263642811c1b1c',
);

assert.deepStrictEqual(res, true);
});

it('should return false if the signature is valid [mock EthereumProvider]', async () => {
const provider = new EthereumProvider({ returnData: [] }, [], false); // we mock a valid return response
const erc725 = new ERC725(
[],
'0xD295E4748c1DFDFE028D7Dd2FEC3e52de2b1EB42', // result is mocked so we can use any address
provider,
);

const res = await erc725.isValidSignature(
'hello',
'0xcafecafecafecafecafe6ce85b786ef79de48a43158194fa6b3604254ff58f9c2e4ffcc080e18a68c8e813f720b893c8d47d6f757b9e288a5814263642811c1b1c',
);

assert.deepStrictEqual(res, false);
});
});

describe('Getting all data in schema by provider [e2e]', () => {
const web3 = new Web3('https://rpc.l14.lukso.network');

Expand Down
31 changes: 29 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
} from './lib/utils';

import { getSchema } from './lib/schemaParser';
import { isValidSignature } from './lib/isValidSignature';

import {
ERC725JSONSchema,
Expand Down Expand Up @@ -316,6 +317,32 @@ export class ERC725<Schema extends GenericSchema> {
return provider.getOwner(_address || address);
}

/**
* A helper function which checks if a signature is valid according to the EIP-1271 standard.
*
* @param messageOrHash if it is a 66 chars string with 0x prefix, it will be considered as a hash (keccak256). If not, the message will be wrapped as follows: "\x19Ethereum Signed Message:\n" + message.length + message and hashed.
* @param signature
* @returns true if isValidSignature call on the contract returns the magic value. false otherwise
*/
async isValidSignature(
messageOrHash: string,
signature: string,
): Promise<boolean> {
if (!this.options.address || !isAddress(this.options.address)) {
throw new Error('Missing ERC725 contract address.');
}
if (!this.options.provider) {
throw new Error('Missing provider.');
}

return isValidSignature(
messageOrHash,
signature,
this.options.address,
this.options.provider,
);
}

/**
* @internal
* @param schema associated with the schema with keyType = 'Array'
Expand Down Expand Up @@ -469,15 +496,15 @@ export class ERC725<Schema extends GenericSchema> {
}

private getAddressAndProvider() {
if (!isAddress(this.options.address as string)) {
if (!this.options.address || !isAddress(this.options.address)) {
throw new Error('Missing ERC725 contract address.');
}
if (!this.options.provider) {
throw new Error('Missing provider.');
}

return {
address: this.options.address as string,
address: this.options.address,
provider: this.options.provider,
};
}
Expand Down
25 changes: 11 additions & 14 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,21 @@ export enum ERC725_VERSION {

export const METHODS: Record<Method, MethodData> = {
[Method.GET_DATA_LEGACY]: {
// Legacy version of ERC725Y - before v0.3.0
sig: '0x54f6127f',
gas: numberToHex(2000000),
gasPrice: numberToHex(100000000),
value: numberToHex(0),
returnEncoding: Encoding.BYTES,
},
[Method.GET_DATA]: {
// https://github.com/ERC725Alliance/erc725/blob/main/docs/ERC-725.md#erc725y
sig: '0x4e3e6e9c',
gas: numberToHex(2000000),
gasPrice: numberToHex(100000000),
value: numberToHex(0),
returnEncoding: Encoding.BYTES_ARRAY,
},
[Method.DATA_COUNT]: {
sig: '0x5da40c47',
gas: numberToHex(2000000),
gasPrice: numberToHex(100000000),
value: numberToHex(0),
returnEncoding: Encoding.UINT256,
},
[Method.ALL_DATA]: {
sig: '0xc559acef',
gas: numberToHex(2000000),
gasPrice: numberToHex(100000000),
value: numberToHex(0),
returnEncoding: Encoding.BYTES32_ARRAY,
},
[Method.OWNER]: {
sig: '0x8da5cb5b',
gas: numberToHex(2000000),
Expand All @@ -52,12 +40,21 @@ export const METHODS: Record<Method, MethodData> = {
returnEncoding: Encoding.ADDRESS,
},
[Method.SUPPORTS_INTERFACE]: {
// https://eips.ethereum.org/EIPS/eip-165
sig: '0x01ffc9a7',
gas: numberToHex(2000000),
gasPrice: numberToHex(100000000),
value: numberToHex(0),
returnEncoding: Encoding.BOOL,
},
[Method.IS_VALID_SIGNATURE]: {
// https://eips.ethereum.org/EIPS/eip-1271
sig: '0x1626ba7e',
gas: numberToHex(2000000),
gasPrice: numberToHex(100000000),
value: numberToHex(0),
returnEncoding: Encoding.BYTES4,
},
};

export enum SUPPORTED_HASH_FUNCTION_STRINGS {
Expand Down
69 changes: 69 additions & 0 deletions src/lib/isValidSignature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
This file is part of @erc725/erc725.js.
@erc725/erc725.js is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
@erc725/erc725.js is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with @erc725/erc725.js. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* @file lib/isValidSignature.ts
* @author Hugo Masclet <@Hugoo>
* @date 2022
*/

import { keccak256 } from 'web3-utils';
import { EthereumProviderWrapper } from '../providers/ethereumProviderWrapper';
import { Web3ProviderWrapper } from '../providers/web3ProviderWrapper';

const MAGIC_VALUE = '0x1626ba7e';

// https://ethereum.stackexchange.com/a/72625
function validateHash(hash) {
return /^0x([A-Fa-f0-9]{64})$/.test(hash);
}

/**
*
* https://eips.ethereum.org/EIPS/eip-1271
*
* @param {string} messageOrHash
* @param {string} signature
* @param {string} address the contract address
* @returns {Promise<boolean>} Return true if the signature is valid (if the contract returns the magic value), false if not.
*/
export const isValidSignature = async (
messageOrHash: string,
signature: string,
address: string,
wrappedProvider: EthereumProviderWrapper | Web3ProviderWrapper,
): Promise<boolean> => {
const hash = validateHash(messageOrHash)
? messageOrHash
: keccak256(
`\x19Ethereum Signed Message:\n${messageOrHash.length}${messageOrHash}`,
);

if (signature.length !== 132) {
throw new Error('Signature length should be 132 (65bytes)');
}

try {
const value = await wrappedProvider.isValidSignature(
address,
hash,
signature,
);

return value === MAGIC_VALUE;
} catch (err: any) {
throw new Error(
`Error when checking signature. Is ${address} a valid contract address which supports EIP-1271 standard?`,
);
}
};
24 changes: 24 additions & 0 deletions src/providers/ethereumProviderWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,30 @@ export class EthereumProviderWrapper {
);
}

/**
* https://eips.ethereum.org/EIPS/eip-1271
*
* @param {string} address the contract address
* @param {string} hash
* @param {string} signature
*/
async isValidSignature(address: string, hash: string, signature: string) {
const encodedParams = abiCoder.encodeParameters(
['bytes32', 'bytes'],
[hash, signature],
);

const result = await this.callContract([
this.constructJSONRPC(address, Method.IS_VALID_SIGNATURE, encodedParams),
]);

if (result.error) {
throw result.error;
}

return this.decodeResult(Method.IS_VALID_SIGNATURE, result);
}

async getData(address: string, keyHash: string) {
const result = this.getAllData(address, [keyHash]);

Expand Down
24 changes: 24 additions & 0 deletions src/providers/web3ProviderWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,29 @@ export class Web3ProviderWrapper {
);
}

/**
* https://eips.ethereum.org/EIPS/eip-1271
*
* @param address the contract address
* @param hash
* @param signature
*/
async isValidSignature(address: string, hash: string, signature: string) {
const encodedParams = abiCoder.encodeParameters(
['bytes32', 'bytes'],
[hash, signature],
);

const results = await this.callContract([
constructJSONRPC(address, Method.IS_VALID_SIGNATURE, encodedParams),
]);
if (results.error) {
throw results.error;
}

return decodeResult(Method.IS_VALID_SIGNATURE, results[0]);
}

async getData(address: string, keyHash: string) {
const result = await this.getAllData(address, [keyHash]);

Expand Down Expand Up @@ -180,6 +203,7 @@ export class Web3ProviderWrapper {
private async callContract(payload: JsonRpc[] | JsonRpc): Promise<any> {
return new Promise((resolve, reject) => {
// Send old web3 method with callback to resolve promise
// This is deprecated: https://docs.metamask.io/guide/ethereum-provider.html#ethereum-send-deprecated
this.provider.send(payload, (e, r) => {
if (e) {
reject(e);
Expand Down
6 changes: 3 additions & 3 deletions src/types/Method.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
export enum Method {
GET_DATA_LEGACY = 'getDataLegacy', // For legacy ERC725 with interface id: 0x2bd57b73 NOTE: I had to add Legacy at the end so the map keys stays unique
GET_DATA = 'getData', // For latest ERC725 with interface id: 0x5a988c0f
DATA_COUNT = 'dataCount',
ALL_DATA = 'allData',
OWNER = 'owner',
SUPPORTS_INTERFACE = 'supportsInterface',
SUPPORTS_INTERFACE = 'supportsInterface', // https://eips.ethereum.org/EIPS/eip-165
IS_VALID_SIGNATURE = 'isValidSignature', // https://eips.ethereum.org/EIPS/eip-1271
}

export enum Encoding {
BYTES = 'bytes',
BYTES4 = 'bytes4',
BOOL = 'bool',
UINT256 = 'uint256',
BYTES32_ARRAY = 'bytes32[]',
Expand Down
Loading

0 comments on commit 6490751

Please sign in to comment.