Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 148 additions & 7 deletions src/sign-eip7702-authorization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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);

Expand Down Expand Up @@ -165,7 +306,7 @@ describe('signAuthorization', () => {
});
});

describe('recoverAuthorization()', () => {
describe('recoverEIP7702Authorization()', () => {
it('should recover the address from a signature', () => {
const recoveredAddress = recoverEIP7702Authorization({
authorization: TEST_AUTHORIZATION,
Expand Down
32 changes: 31 additions & 1 deletion src/sign-eip7702-authorization.ts
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down Expand Up @@ -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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record: This function does verify the address length too.

See: https://github.com/MetaMask/utils/blob/v11.3.0/src/hex.ts#L15-L18 ({40} in hex-format, each bytes is 2-digits/chars long, which makes it a length of 20 bytes).

throw new Error(
`Invalid contractAddress: must be a ${ADDRESS_BYTE_LENGTH} byte hex string`,
);
}
}
Loading