ERC7739Utils.test.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. const { expect } = require('chai');
  2. const { ethers } = require('hardhat');
  3. const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
  4. const { Permit } = require('../../helpers/eip712');
  5. const { ERC4337Utils, PersonalSign } = require('../../helpers/erc7739');
  6. const details = ERC4337Utils.getContentsDetail({ Permit });
  7. const fixture = async () => {
  8. const mock = await ethers.deployContract('$ERC7739Utils');
  9. const domain = {
  10. name: 'SomeDomain',
  11. version: '1',
  12. chainId: await ethers.provider.getNetwork().then(({ chainId }) => chainId),
  13. verifyingContract: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512',
  14. };
  15. const otherDomain = {
  16. name: 'SomeOtherDomain',
  17. version: '2',
  18. chainId: await ethers.provider.getNetwork().then(({ chainId }) => chainId),
  19. verifyingContract: '0x92C32cadBc39A15212505B5530aA765c441F306f',
  20. };
  21. const permit = {
  22. owner: '0x1ab5E417d9AF00f1ca9d159007e12c401337a4bb',
  23. spender: '0xD68E96620804446c4B1faB3103A08C98d4A8F55f',
  24. value: 1_000_000n,
  25. nonce: 0n,
  26. deadline: ethers.MaxUint256,
  27. };
  28. return { mock, domain, otherDomain, permit };
  29. };
  30. describe('ERC7739Utils', function () {
  31. beforeEach(async function () {
  32. Object.assign(this, await loadFixture(fixture));
  33. });
  34. describe('encodeTypedDataSig', function () {
  35. it('wraps a typed data signature', async function () {
  36. const signature = ethers.randomBytes(65);
  37. const appSeparator = ethers.id('SomeApp');
  38. const contentsHash = ethers.id('SomeData');
  39. const contentsDescr = 'SomeType()';
  40. const encoded = ethers.concat([
  41. signature,
  42. appSeparator,
  43. contentsHash,
  44. ethers.toUtf8Bytes(contentsDescr),
  45. ethers.toBeHex(contentsDescr.length, 2),
  46. ]);
  47. await expect(
  48. this.mock.$encodeTypedDataSig(signature, appSeparator, contentsHash, contentsDescr),
  49. ).to.eventually.equal(encoded);
  50. });
  51. });
  52. describe('decodeTypedDataSig', function () {
  53. it('unwraps a typed data signature', async function () {
  54. const signature = ethers.randomBytes(65);
  55. const appSeparator = ethers.id('SomeApp');
  56. const contentsHash = ethers.id('SomeData');
  57. const contentsDescr = 'SomeType()';
  58. const encoded = ethers.concat([
  59. signature,
  60. appSeparator,
  61. contentsHash,
  62. ethers.toUtf8Bytes(contentsDescr),
  63. ethers.toBeHex(contentsDescr.length, 2),
  64. ]);
  65. await expect(this.mock.$decodeTypedDataSig(encoded)).to.eventually.deep.equal([
  66. ethers.hexlify(signature),
  67. appSeparator,
  68. contentsHash,
  69. contentsDescr,
  70. ]);
  71. });
  72. it('returns default empty values if the signature is too short', async function () {
  73. const encoded = ethers.randomBytes(65); // DOMAIN_SEPARATOR (32 bytes) + CONTENTS (32 bytes) + CONTENTS_TYPE_LENGTH (2 bytes) - 1
  74. await expect(this.mock.$decodeTypedDataSig(encoded)).to.eventually.deep.equal([
  75. '0x',
  76. ethers.ZeroHash,
  77. ethers.ZeroHash,
  78. '',
  79. ]);
  80. });
  81. it('returns default empty values if the length is invalid', async function () {
  82. const encoded = ethers.concat([ethers.randomBytes(64), '0x3f']); // Can't be less than 64 bytes
  83. await expect(this.mock.$decodeTypedDataSig(encoded)).to.eventually.deep.equal([
  84. '0x',
  85. ethers.ZeroHash,
  86. ethers.ZeroHash,
  87. '',
  88. ]);
  89. });
  90. });
  91. describe('personalSignStructhash', function () {
  92. it('should produce a personal signature EIP-712 nested type', async function () {
  93. const text = 'Hello, world!';
  94. await expect(this.mock.$personalSignStructHash(ethers.hashMessage(text))).to.eventually.equal(
  95. ethers.TypedDataEncoder.hashStruct('PersonalSign', { PersonalSign }, ERC4337Utils.preparePersonalSign(text)),
  96. );
  97. });
  98. });
  99. describe('typedDataSignStructHash', function () {
  100. it('should match the typed data nested struct hash', async function () {
  101. const message = ERC4337Utils.prepareSignTypedData(this.permit, this.domain);
  102. const contentsHash = ethers.TypedDataEncoder.hashStruct('Permit', { Permit }, this.permit);
  103. const hash = ethers.TypedDataEncoder.hashStruct('TypedDataSign', details.allTypes, message);
  104. const domainBytes = ethers.AbiCoder.defaultAbiCoder().encode(
  105. ['bytes32', 'bytes32', 'uint256', 'address', 'bytes32'],
  106. [
  107. ethers.id(this.domain.name),
  108. ethers.id(this.domain.version),
  109. this.domain.chainId,
  110. this.domain.verifyingContract,
  111. ethers.ZeroHash,
  112. ],
  113. );
  114. await expect(
  115. this.mock.$typedDataSignStructHash(
  116. details.contentsTypeName,
  117. ethers.Typed.string(details.contentsDescr),
  118. contentsHash,
  119. domainBytes,
  120. ),
  121. ).to.eventually.equal(hash);
  122. await expect(
  123. this.mock.$typedDataSignStructHash(details.contentsDescr, contentsHash, domainBytes),
  124. ).to.eventually.equal(hash);
  125. });
  126. });
  127. describe('typedDataSignTypehash', function () {
  128. it('should match', async function () {
  129. const typedDataSignType = ethers.TypedDataEncoder.from(details.allTypes).encodeType('TypedDataSign');
  130. await expect(
  131. this.mock.$typedDataSignTypehash(
  132. details.contentsTypeName,
  133. typedDataSignType.slice(typedDataSignType.indexOf(')') + 1),
  134. ),
  135. ).to.eventually.equal(ethers.keccak256(ethers.toUtf8Bytes(typedDataSignType)));
  136. });
  137. });
  138. describe('decodeContentsDescr', function () {
  139. const forbiddenChars = ', )\x00';
  140. for (const { descr, contentsDescr, contentTypeName, contentType } of [].concat(
  141. {
  142. descr: 'should parse a valid descriptor (implicit)',
  143. contentsDescr: 'SomeType(address foo,uint256 bar)',
  144. contentTypeName: 'SomeType',
  145. },
  146. {
  147. descr: 'should parse a valid descriptor (explicit)',
  148. contentsDescr: 'A(C c)B(A a)C(uint256 v)B',
  149. contentTypeName: 'B',
  150. contentType: 'A(C c)B(A a)C(uint256 v)',
  151. },
  152. { descr: 'should return nothing for an empty descriptor', contentsDescr: '', contentTypeName: null },
  153. { descr: 'should return nothing if no [(] is present', contentsDescr: 'SomeType', contentTypeName: null },
  154. {
  155. descr: 'should return nothing if starts with [(] (implicit)',
  156. contentsDescr: '(SomeType(address foo,uint256 bar)',
  157. contentTypeName: null,
  158. },
  159. {
  160. descr: 'should return nothing if starts with [(] (explicit)',
  161. contentsDescr: '(SomeType(address foo,uint256 bar)(SomeType',
  162. contentTypeName: null,
  163. },
  164. forbiddenChars.split('').map(char => ({
  165. descr: `should return nothing if contains [${char}] (implicit)`,
  166. contentsDescr: `SomeType${char}(address foo,uint256 bar)`,
  167. contentTypeName: null,
  168. })),
  169. forbiddenChars.split('').map(char => ({
  170. descr: `should return nothing if contains [${char}] (explicit)`,
  171. contentsDescr: `SomeType${char}(address foo,uint256 bar)SomeType${char}`,
  172. contentTypeName: null,
  173. })),
  174. )) {
  175. it(descr, async function () {
  176. await expect(this.mock.$decodeContentsDescr(contentsDescr)).to.eventually.deep.equal([
  177. contentTypeName ?? '',
  178. contentTypeName ? (contentType ?? contentsDescr) : '',
  179. ]);
  180. });
  181. }
  182. });
  183. });