Add ERC7739 and ERC7739Utils (#5664)

This commit is contained in:
Ernesto García
2025-05-06 12:47:36 -06:00
committed by GitHub
parent 08566bfe0d
commit a3a9e8cc3b
11 changed files with 821 additions and 1 deletions

View File

@ -0,0 +1,203 @@
const { expect } = require('chai');
const { ethers } = require('hardhat');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { Permit } = require('../../helpers/eip712');
const { ERC4337Utils, PersonalSign } = require('../../helpers/erc7739');
const details = ERC4337Utils.getContentsDetail({ Permit });
const fixture = async () => {
const mock = await ethers.deployContract('$ERC7739Utils');
const domain = {
name: 'SomeDomain',
version: '1',
chainId: await ethers.provider.getNetwork().then(({ chainId }) => chainId),
verifyingContract: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512',
};
const otherDomain = {
name: 'SomeOtherDomain',
version: '2',
chainId: await ethers.provider.getNetwork().then(({ chainId }) => chainId),
verifyingContract: '0x92C32cadBc39A15212505B5530aA765c441F306f',
};
const permit = {
owner: '0x1ab5E417d9AF00f1ca9d159007e12c401337a4bb',
spender: '0xD68E96620804446c4B1faB3103A08C98d4A8F55f',
value: 1_000_000n,
nonce: 0n,
deadline: ethers.MaxUint256,
};
return { mock, domain, otherDomain, permit };
};
describe('ERC7739Utils', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('encodeTypedDataSig', function () {
it('wraps a typed data signature', async function () {
const signature = ethers.randomBytes(65);
const appSeparator = ethers.id('SomeApp');
const contentsHash = ethers.id('SomeData');
const contentsDescr = 'SomeType()';
const encoded = ethers.concat([
signature,
appSeparator,
contentsHash,
ethers.toUtf8Bytes(contentsDescr),
ethers.toBeHex(contentsDescr.length, 2),
]);
await expect(
this.mock.$encodeTypedDataSig(signature, appSeparator, contentsHash, contentsDescr),
).to.eventually.equal(encoded);
});
});
describe('decodeTypedDataSig', function () {
it('unwraps a typed data signature', async function () {
const signature = ethers.randomBytes(65);
const appSeparator = ethers.id('SomeApp');
const contentsHash = ethers.id('SomeData');
const contentsDescr = 'SomeType()';
const encoded = ethers.concat([
signature,
appSeparator,
contentsHash,
ethers.toUtf8Bytes(contentsDescr),
ethers.toBeHex(contentsDescr.length, 2),
]);
await expect(this.mock.$decodeTypedDataSig(encoded)).to.eventually.deep.equal([
ethers.hexlify(signature),
appSeparator,
contentsHash,
contentsDescr,
]);
});
it('returns default empty values if the signature is too short', async function () {
const encoded = ethers.randomBytes(65); // DOMAIN_SEPARATOR (32 bytes) + CONTENTS (32 bytes) + CONTENTS_TYPE_LENGTH (2 bytes) - 1
await expect(this.mock.$decodeTypedDataSig(encoded)).to.eventually.deep.equal([
'0x',
ethers.ZeroHash,
ethers.ZeroHash,
'',
]);
});
it('returns default empty values if the length is invalid', async function () {
const encoded = ethers.concat([ethers.randomBytes(64), '0x3f']); // Can't be less than 64 bytes
await expect(this.mock.$decodeTypedDataSig(encoded)).to.eventually.deep.equal([
'0x',
ethers.ZeroHash,
ethers.ZeroHash,
'',
]);
});
});
describe('personalSignStructhash', function () {
it('should produce a personal signature EIP-712 nested type', async function () {
const text = 'Hello, world!';
await expect(this.mock.$personalSignStructHash(ethers.hashMessage(text))).to.eventually.equal(
ethers.TypedDataEncoder.hashStruct('PersonalSign', { PersonalSign }, ERC4337Utils.preparePersonalSign(text)),
);
});
});
describe('typedDataSignStructHash', function () {
it('should match the typed data nested struct hash', async function () {
const message = ERC4337Utils.prepareSignTypedData(this.permit, this.domain);
const contentsHash = ethers.TypedDataEncoder.hashStruct('Permit', { Permit }, this.permit);
const hash = ethers.TypedDataEncoder.hashStruct('TypedDataSign', details.allTypes, message);
const domainBytes = ethers.AbiCoder.defaultAbiCoder().encode(
['bytes32', 'bytes32', 'uint256', 'address', 'bytes32'],
[
ethers.id(this.domain.name),
ethers.id(this.domain.version),
this.domain.chainId,
this.domain.verifyingContract,
ethers.ZeroHash,
],
);
await expect(
this.mock.$typedDataSignStructHash(
details.contentsTypeName,
ethers.Typed.string(details.contentsDescr),
contentsHash,
domainBytes,
),
).to.eventually.equal(hash);
await expect(
this.mock.$typedDataSignStructHash(details.contentsDescr, contentsHash, domainBytes),
).to.eventually.equal(hash);
});
});
describe('typedDataSignTypehash', function () {
it('should match', async function () {
const typedDataSignType = ethers.TypedDataEncoder.from(details.allTypes).encodeType('TypedDataSign');
await expect(
this.mock.$typedDataSignTypehash(
details.contentsTypeName,
typedDataSignType.slice(typedDataSignType.indexOf(')') + 1),
),
).to.eventually.equal(ethers.keccak256(ethers.toUtf8Bytes(typedDataSignType)));
});
});
describe('decodeContentsDescr', function () {
const forbiddenChars = ', )\x00';
for (const { descr, contentsDescr, contentTypeName, contentType } of [].concat(
{
descr: 'should parse a valid descriptor (implicit)',
contentsDescr: 'SomeType(address foo,uint256 bar)',
contentTypeName: 'SomeType',
},
{
descr: 'should parse a valid descriptor (explicit)',
contentsDescr: 'A(C c)B(A a)C(uint256 v)B',
contentTypeName: 'B',
contentType: 'A(C c)B(A a)C(uint256 v)',
},
{ descr: 'should return nothing for an empty descriptor', contentsDescr: '', contentTypeName: null },
{ descr: 'should return nothing if no [(] is present', contentsDescr: 'SomeType', contentTypeName: null },
{
descr: 'should return nothing if starts with [(] (implicit)',
contentsDescr: '(SomeType(address foo,uint256 bar)',
contentTypeName: null,
},
{
descr: 'should return nothing if starts with [(] (explicit)',
contentsDescr: '(SomeType(address foo,uint256 bar)(SomeType',
contentTypeName: null,
},
forbiddenChars.split('').map(char => ({
descr: `should return nothing if contains [${char}] (implicit)`,
contentsDescr: `SomeType${char}(address foo,uint256 bar)`,
contentTypeName: null,
})),
forbiddenChars.split('').map(char => ({
descr: `should return nothing if contains [${char}] (explicit)`,
contentsDescr: `SomeType${char}(address foo,uint256 bar)SomeType${char}`,
contentTypeName: null,
})),
)) {
it(descr, async function () {
await expect(this.mock.$decodeContentsDescr(contentsDescr)).to.eventually.deep.equal([
contentTypeName ?? '',
contentTypeName ? (contentType ?? contentsDescr) : '',
]);
});
}
});
});