diff --git a/src/sign-eip7702-authorization.test.ts b/src/sign-eip7702-authorization.test.ts index 3c062197..f44fe5c9 100644 --- a/src/sign-eip7702-authorization.test.ts +++ b/src/sign-eip7702-authorization.test.ts @@ -29,7 +29,7 @@ const EXPECTED_SIGNATURE = '0xebea1ac12f17a56a514dfecbcbc8bbee7b089fa3fcee31680d1e2c1588f623df7973cab74e12536678995377da38c96c65c52897750b73462c6760ef2737dba41b'; describe('signAuthorization', () => { - describe('signAuthorization()', () => { + describe('signEIP7702Authorization()', () => { it('should produce the correct signature', () => { const signature = signEIP7702Authorization({ privateKey: TEST_PRIVATE_KEY, @@ -88,17 +88,45 @@ describe('signAuthorization', () => { ).toThrow('Missing chainId parameter'); }); - it('should throw if contractAddress is null', () => { + it('should throw if chainId is not a number', () => { expect(() => signEIP7702Authorization({ privateKey: TEST_PRIVATE_KEY, authorization: [ - TEST_AUTHORIZATION[0], - null as unknown as string, + '123' as any as number, + TEST_AUTHORIZATION[1], TEST_AUTHORIZATION[2], ], }), - ).toThrow('Missing contractAddress parameter'); + ).toThrow( + 'Invalid chainId: must be a non-negative number less than 2^256', + ); + }); + + it('should throw if chainId is too large', () => { + expect(() => + signEIP7702Authorization({ + privateKey: TEST_PRIVATE_KEY, + authorization: [ + 2 ** 256, + TEST_AUTHORIZATION[1], + TEST_AUTHORIZATION[2], + ], + }), + ).toThrow( + 'Invalid chainId: must be a non-negative number less than 2^256', + ); + }); + + it('should throw if chainId is negative', () => { + expect(() => + signEIP7702Authorization({ + privateKey: TEST_PRIVATE_KEY, + authorization: [-1, TEST_AUTHORIZATION[1], TEST_AUTHORIZATION[2]], + }), + ).toThrow( + 'Invalid chainId: must be a non-negative number less than 2^256', + ); }); it('should throw if nonce is null', () => { @@ -113,9 +141,122 @@ describe('signAuthorization', () => { }), ).toThrow('Missing nonce parameter'); }); + + it('should throw if nonce is not a number', () => { + expect(() => + signEIP7702Authorization({ + privateKey: TEST_PRIVATE_KEY, + authorization: [ + TEST_AUTHORIZATION[0], + TEST_AUTHORIZATION[1], + '123' as any as number, + ], + }), + ).toThrow('Invalid nonce: must be a non-negative number less than 2^64'); + }); + + it('should throw if nonce is negative', () => { + expect(() => + signEIP7702Authorization({ + privateKey: TEST_PRIVATE_KEY, + authorization: [TEST_AUTHORIZATION[0], TEST_AUTHORIZATION[1], -123], + }), + ).toThrow('Invalid nonce: must be a non-negative number less than 2^64'); + }); + + it('should throw if nonce is too large', () => { + expect(() => + signEIP7702Authorization({ + privateKey: TEST_PRIVATE_KEY, + authorization: [ + TEST_AUTHORIZATION[0], + TEST_AUTHORIZATION[1], + 2 ** 64, + ], + }), + ).toThrow('Invalid nonce: must be a non-negative number less than 2^64'); + }); + + it('should throw if contractAddress is null', () => { + expect(() => + signEIP7702Authorization({ + privateKey: TEST_PRIVATE_KEY, + authorization: [ + TEST_AUTHORIZATION[0], + null as unknown as string, + TEST_AUTHORIZATION[2], + ], + }), + ).toThrow('Missing contractAddress parameter'); + }); + + it('should throw if contractAddress is not a string', () => { + expect(() => + signEIP7702Authorization({ + privateKey: TEST_PRIVATE_KEY, + authorization: [ + TEST_AUTHORIZATION[0], + 123 as any as string, + TEST_AUTHORIZATION[2], + ], + }), + ).toThrow('Invalid contractAddress: must be a 20 byte hex string'); + }); + + it('should throw if contractAddress is too short', () => { + expect(() => + signEIP7702Authorization({ + privateKey: TEST_PRIVATE_KEY, + authorization: [ + TEST_AUTHORIZATION[0], + TEST_AUTHORIZATION[1].slice(10), + TEST_AUTHORIZATION[2], + ], + }), + ).toThrow('Invalid contractAddress: must be a 20 byte hex string'); + }); + + it('should throw if contractAddress is too long', () => { + expect(() => + signEIP7702Authorization({ + privateKey: TEST_PRIVATE_KEY, + authorization: [ + TEST_AUTHORIZATION[0], + `${TEST_AUTHORIZATION[1]}00`, + TEST_AUTHORIZATION[2], + ], + }), + ).toThrow('Invalid contractAddress: must be a 20 byte hex string'); + }); + + it('should throw if contractAddress is not valid hex', () => { + expect(() => + signEIP7702Authorization({ + privateKey: TEST_PRIVATE_KEY, + authorization: [ + TEST_AUTHORIZATION[0], + '0xghijklmnopqrstuvwxyghijklmnopqrstuvwxyghij', + TEST_AUTHORIZATION[2], + ], + }), + ).toThrow('Invalid contractAddress: must be a 20 byte hex string'); + }); + + it('should throw if contractAddress is missing the 0x prefix', () => { + expect(() => + signEIP7702Authorization({ + privateKey: TEST_PRIVATE_KEY, + authorization: [ + TEST_AUTHORIZATION[0], + TEST_AUTHORIZATION[1].slice(2), + TEST_AUTHORIZATION[2], + ], + }), + ).toThrow('Invalid contractAddress: must be a 20 byte hex string'); + }); }); - describe('hashAuthorization()', () => { + describe('hashEIP7702Authorization()', () => { it('should produce the correct hash', () => { const hash = hashEIP7702Authorization(TEST_AUTHORIZATION); @@ -165,7 +306,7 @@ describe('signAuthorization', () => { }); }); - describe('recoverAuthorization()', () => { + describe('recoverEIP7702Authorization()', () => { it('should recover the address from a signature', () => { const recoveredAddress = recoverEIP7702Authorization({ authorization: TEST_AUTHORIZATION, diff --git a/src/sign-eip7702-authorization.ts b/src/sign-eip7702-authorization.ts index b73f3b3d..52ce75ed 100644 --- a/src/sign-eip7702-authorization.ts +++ b/src/sign-eip7702-authorization.ts @@ -1,10 +1,14 @@ import { encode } from '@ethereumjs/rlp'; import { ecsign, publicToAddress, toBuffer } from '@ethereumjs/util'; -import { bytesToHex } from '@metamask/utils'; +import { bytesToHex, Hex, isValidHexAddress } from '@metamask/utils'; import { keccak256 } from 'ethereum-cryptography/keccak'; import { concatSig, isNullish, recoverPublicKey } from './utils'; +const CHAIN_ID_MAX_BITLENGTH = 256; +const NONCE_MAX_BITLENGTH = 64; +const ADDRESS_BYTE_LENGTH = 20; + /** * The authorization struct as defined in EIP-7702. * @@ -126,4 +130,30 @@ function validateEIP7702Authorization(authorization: EIP7702Authorization) { if (isNullish(nonce)) { throw new Error('Missing nonce parameter'); } + + if ( + typeof chainId !== 'number' || + chainId >= 2 ** CHAIN_ID_MAX_BITLENGTH || + chainId < 0 + ) { + throw new Error( + `Invalid chainId: must be a non-negative number less than 2^${CHAIN_ID_MAX_BITLENGTH}`, + ); + } + + if ( + typeof nonce !== 'number' || + nonce >= 2 ** NONCE_MAX_BITLENGTH || + nonce < 0 + ) { + throw new Error( + `Invalid nonce: must be a non-negative number less than 2^${NONCE_MAX_BITLENGTH}`, + ); + } + + if (!isValidHexAddress(contractAddress as Hex)) { + throw new Error( + `Invalid contractAddress: must be a ${ADDRESS_BYTE_LENGTH} byte hex string`, + ); + } }