AccountMultiSigner.test.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. const { ethers, predeploy } = require('hardhat');
  2. const { expect } = require('chai');
  3. const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
  4. const { getDomain } = require('../helpers/eip712');
  5. const { ERC4337Helper } = require('../helpers/erc4337');
  6. const { NonNativeSigner, P256SigningKey, RSASHA256SigningKey, MultiERC7913SigningKey } = require('../helpers/signers');
  7. const { MAX_UINT64 } = require('../helpers/constants');
  8. const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
  9. const { shouldBehaveLikeERC1271 } = require('../utils/cryptography/ERC1271.behavior');
  10. const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
  11. const { PackedUserOperation } = require('../helpers/eip712-types');
  12. // Prepare signers in advance (RSA are long to initialize)
  13. const signerECDSA1 = ethers.Wallet.createRandom();
  14. const signerECDSA2 = ethers.Wallet.createRandom();
  15. const signerECDSA3 = ethers.Wallet.createRandom();
  16. const signerECDSA4 = ethers.Wallet.createRandom(); // Unauthorized signer
  17. const signerP256 = new NonNativeSigner(P256SigningKey.random());
  18. const signerRSA = new NonNativeSigner(RSASHA256SigningKey.random());
  19. // Minimal fixture common to the different signer verifiers
  20. async function fixture() {
  21. // EOAs and environment
  22. const [beneficiary, other] = await ethers.getSigners();
  23. const target = await ethers.deployContract('CallReceiverMock');
  24. // ERC-7913 verifiers
  25. const verifierP256 = await ethers.deployContract('ERC7913P256Verifier');
  26. const verifierRSA = await ethers.deployContract('ERC7913RSAVerifier');
  27. // ERC-4337 env
  28. const helper = new ERC4337Helper();
  29. await helper.wait();
  30. const entrypointDomain = await getDomain(predeploy.entrypoint.v08);
  31. const domain = { name: 'AccountMultiSigner', version: '1', chainId: entrypointDomain.chainId }; // Missing verifyingContract
  32. const makeMock = (signers, threshold) =>
  33. helper.newAccount('$AccountMultiSignerMock', [signers, threshold, 'AccountMultiSigner', '1']).then(mock => {
  34. domain.verifyingContract = mock.address;
  35. return mock;
  36. });
  37. // Sign user operations using MultiERC7913SigningKey
  38. const signUserOp = function (userOp) {
  39. return this.signer
  40. .signTypedData(entrypointDomain, { PackedUserOperation }, userOp.packed)
  41. .then(signature => Object.assign(userOp, { signature }));
  42. };
  43. const invalidSig = function () {
  44. return this.signer.signMessage('invalid');
  45. };
  46. return {
  47. helper,
  48. verifierP256,
  49. verifierRSA,
  50. domain,
  51. target,
  52. beneficiary,
  53. other,
  54. makeMock,
  55. signUserOp,
  56. invalidSig,
  57. };
  58. }
  59. describe('AccountMultiSigner', function () {
  60. beforeEach(async function () {
  61. Object.assign(this, await loadFixture(fixture));
  62. });
  63. describe('Multi ECDSA signers with threshold=1', function () {
  64. beforeEach(async function () {
  65. this.signer = new NonNativeSigner(new MultiERC7913SigningKey([signerECDSA1]));
  66. this.mock = await this.makeMock([signerECDSA1.address], 1);
  67. });
  68. shouldBehaveLikeAccountCore();
  69. shouldBehaveLikeAccountHolder();
  70. shouldBehaveLikeERC1271({ erc7739: true });
  71. shouldBehaveLikeERC7821();
  72. });
  73. describe('Multi ECDSA signers with threshold=2', function () {
  74. beforeEach(async function () {
  75. this.signer = new NonNativeSigner(new MultiERC7913SigningKey([signerECDSA1, signerECDSA2]));
  76. this.mock = await this.makeMock([signerECDSA1.address, signerECDSA2.address], 2);
  77. });
  78. shouldBehaveLikeAccountCore();
  79. shouldBehaveLikeAccountHolder();
  80. shouldBehaveLikeERC1271({ erc7739: true });
  81. shouldBehaveLikeERC7821();
  82. });
  83. describe('Mixed signers with threshold=2', function () {
  84. beforeEach(async function () {
  85. // Create signers array with all three types
  86. signerP256.bytes = ethers.concat([
  87. this.verifierP256.target,
  88. signerP256.signingKey.publicKey.qx,
  89. signerP256.signingKey.publicKey.qy,
  90. ]);
  91. signerRSA.bytes = ethers.concat([
  92. this.verifierRSA.target,
  93. ethers.AbiCoder.defaultAbiCoder().encode(
  94. ['bytes', 'bytes'],
  95. [signerRSA.signingKey.publicKey.e, signerRSA.signingKey.publicKey.n],
  96. ),
  97. ]);
  98. this.signer = new NonNativeSigner(new MultiERC7913SigningKey([signerECDSA1, signerP256, signerRSA]));
  99. this.mock = await this.makeMock([signerECDSA1.address, signerP256.bytes, signerRSA.bytes], 2);
  100. });
  101. shouldBehaveLikeAccountCore();
  102. shouldBehaveLikeAccountHolder();
  103. shouldBehaveLikeERC1271({ erc7739: true });
  104. shouldBehaveLikeERC7821();
  105. });
  106. describe('Signer management', function () {
  107. beforeEach(async function () {
  108. this.signer = new NonNativeSigner(new MultiERC7913SigningKey([signerECDSA1, signerECDSA2]));
  109. this.mock = await this.makeMock([signerECDSA1.address, signerECDSA2.address], 1);
  110. await this.mock.deploy();
  111. });
  112. it('can add signers', async function () {
  113. const signers = [signerECDSA3.address];
  114. // Successfully adds a signer
  115. const signersArrayBefore = await this.mock.getSigners(0, MAX_UINT64).then(s => s.map(ethers.getAddress));
  116. await expect(this.mock.$_addSigners(signers))
  117. .to.emit(this.mock, 'ERC7913SignerAdded')
  118. .withArgs(signerECDSA3.address);
  119. const signersArrayAfter = await this.mock.getSigners(0, MAX_UINT64).then(s => s.map(ethers.getAddress));
  120. expect(signersArrayAfter.length).to.equal(signersArrayBefore.length + 1);
  121. expect(signersArrayAfter).to.include(ethers.getAddress(signerECDSA3.address));
  122. // Reverts if the signer was already added
  123. await expect(this.mock.$_addSigners(signers))
  124. .to.be.revertedWithCustomError(this.mock, 'MultiSignerERC7913AlreadyExists')
  125. .withArgs(...signers.map(s => s.toLowerCase()));
  126. });
  127. it('can remove signers', async function () {
  128. const signers = [signerECDSA2.address];
  129. // Successfully removes an already added signer
  130. const signersArrayBefore = await this.mock.getSigners(0, MAX_UINT64).then(s => s.map(ethers.getAddress));
  131. await expect(this.mock.$_removeSigners(signers))
  132. .to.emit(this.mock, 'ERC7913SignerRemoved')
  133. .withArgs(signerECDSA2.address);
  134. const signersArrayAfter = await this.mock.getSigners(0, MAX_UINT64).then(s => s.map(ethers.getAddress));
  135. expect(signersArrayAfter.length).to.equal(signersArrayBefore.length - 1);
  136. expect(signersArrayAfter).to.not.include(ethers.getAddress(signerECDSA2.address));
  137. // Reverts removing a signer if it doesn't exist
  138. await expect(this.mock.$_removeSigners(signers))
  139. .to.be.revertedWithCustomError(this.mock, 'MultiSignerERC7913NonexistentSigner')
  140. .withArgs(...signers.map(s => s.toLowerCase()));
  141. // Reverts if removing a signer makes the threshold unreachable
  142. await expect(this.mock.$_removeSigners([signerECDSA1.address]))
  143. .to.be.revertedWithCustomError(this.mock, 'MultiSignerERC7913UnreachableThreshold')
  144. .withArgs(0, 1);
  145. });
  146. it('can change threshold', async function () {
  147. // Reachable threshold is set
  148. await expect(this.mock.$_setThreshold(2)).to.emit(this.mock, 'ERC7913ThresholdSet');
  149. // Unreachable threshold reverts
  150. await expect(this.mock.$_setThreshold(3))
  151. .to.revertedWithCustomError(this.mock, 'MultiSignerERC7913UnreachableThreshold')
  152. .withArgs(2, 3);
  153. // Zero threshold reverts
  154. await expect(this.mock.$_setThreshold(0)).to.revertedWithCustomError(
  155. this.mock,
  156. 'MultiSignerERC7913ZeroThreshold',
  157. );
  158. });
  159. it('rejects invalid signer format', async function () {
  160. const invalidSigner = '0x123456'; // Too short
  161. await expect(this.mock.$_addSigners([invalidSigner]))
  162. .to.be.revertedWithCustomError(this.mock, 'MultiSignerERC7913InvalidSigner')
  163. .withArgs(invalidSigner);
  164. });
  165. it('can read signers and threshold', async function () {
  166. await expect(
  167. this.mock.getSigners(0, MAX_UINT64).then(s => s.map(ethers.getAddress)),
  168. ).to.eventually.have.deep.members([signerECDSA1.address, signerECDSA2.address]);
  169. await expect(this.mock.threshold()).to.eventually.equal(1);
  170. });
  171. it('checks if an address is a signer', async function () {
  172. // Should return true for authorized signers
  173. await expect(this.mock.isSigner(signerECDSA1.address)).to.eventually.be.true;
  174. await expect(this.mock.isSigner(signerECDSA2.address)).to.eventually.be.true;
  175. // Should return false for unauthorized signers
  176. await expect(this.mock.isSigner(signerECDSA3.address)).to.eventually.be.false;
  177. await expect(this.mock.isSigner(signerECDSA4.address)).to.eventually.be.false;
  178. });
  179. });
  180. describe('Signature validation', function () {
  181. const TEST_MESSAGE = ethers.keccak256(ethers.toUtf8Bytes('Test message'));
  182. beforeEach(async function () {
  183. // Set up mock with authorized signers
  184. this.mock = await this.makeMock([signerECDSA1.address, signerECDSA2.address], 1);
  185. await this.mock.deploy();
  186. });
  187. it('rejects signatures from unauthorized signers', async function () {
  188. // Create signatures including an unauthorized signer
  189. const authorizedSignature = await signerECDSA1.signMessage(ethers.getBytes(TEST_MESSAGE));
  190. const unauthorizedSignature = await signerECDSA4.signMessage(ethers.getBytes(TEST_MESSAGE));
  191. // Prepare signers and signatures arrays
  192. const signers = [
  193. signerECDSA1.address,
  194. signerECDSA4.address, // Unauthorized signer
  195. ].sort((a, b) => (ethers.toBigInt(ethers.keccak256(a)) < ethers.toBigInt(ethers.keccak256(b)) ? -1 : 1));
  196. const signatures = signers.map(signer => {
  197. if (signer === signerECDSA1.address) return authorizedSignature;
  198. return unauthorizedSignature;
  199. });
  200. // Encode the multi-signature
  201. const multiSignature = ethers.AbiCoder.defaultAbiCoder().encode(['bytes[]', 'bytes[]'], [signers, signatures]);
  202. // Should fail because one signer is not authorized
  203. await expect(this.mock.$_rawSignatureValidation(TEST_MESSAGE, multiSignature)).to.eventually.be.false;
  204. });
  205. it('rejects invalid signatures from authorized signers', async function () {
  206. // Create a valid signature and an invalid one from authorized signers
  207. const validSignature = await signerECDSA1.signMessage(ethers.getBytes(TEST_MESSAGE));
  208. const invalidSignature = await signerECDSA2.signMessage(ethers.toUtf8Bytes('Different message')); // Wrong message
  209. // Prepare signers and signatures arrays
  210. const signers = [signerECDSA1.address, signerECDSA2.address].sort((a, b) =>
  211. ethers.toBigInt(ethers.keccak256(a)) < ethers.toBigInt(ethers.keccak256(b)) ? -1 : 1,
  212. );
  213. const signatures = signers.map(signer => {
  214. if (signer === signerECDSA1.address) return validSignature;
  215. return invalidSignature;
  216. });
  217. // Encode the multi-signature
  218. const multiSignature = ethers.AbiCoder.defaultAbiCoder().encode(['bytes[]', 'bytes[]'], [signers, signatures]);
  219. // Should fail because one signature is invalid
  220. await expect(this.mock.$_rawSignatureValidation(TEST_MESSAGE, multiSignature)).to.eventually.be.false;
  221. });
  222. it('rejects signatures from unsorted signers', async function () {
  223. // Create a valid signature and an invalid one from authorized signers
  224. const validSignature1 = await signerECDSA1.signMessage(ethers.getBytes(TEST_MESSAGE));
  225. const validSignature2 = await signerECDSA2.signMessage(ethers.getBytes(TEST_MESSAGE));
  226. // Prepare signers and signatures arrays
  227. const signers = [signerECDSA1.address, signerECDSA2.address].sort((a, b) =>
  228. ethers.toBigInt(ethers.keccak256(a)) < ethers.toBigInt(ethers.keccak256(b)) ? -1 : 1,
  229. );
  230. const unsortedSigners = signers.reverse();
  231. const signatures = unsortedSigners.map(signer => {
  232. if (signer === signerECDSA1.address) return validSignature1;
  233. return validSignature2;
  234. });
  235. // Encode the multi-signature
  236. const multiSignature = ethers.AbiCoder.defaultAbiCoder().encode(
  237. ['bytes[]', 'bytes[]'],
  238. [unsortedSigners, signatures],
  239. );
  240. // Should fail because signers are not sorted
  241. await expect(this.mock.$_rawSignatureValidation(TEST_MESSAGE, multiSignature)).to.eventually.be.false;
  242. });
  243. it('rejects signatures when signers.length != signatures.length', async function () {
  244. // Create a valid signature and an invalid one from authorized signers
  245. const validSignature1 = await signerECDSA1.signMessage(ethers.getBytes(TEST_MESSAGE));
  246. // Prepare signers and signatures arrays
  247. const signers = [signerECDSA1.address, signerECDSA2.address];
  248. const signatures = [validSignature1];
  249. // Encode the multi-signature
  250. const multiSignature = ethers.AbiCoder.defaultAbiCoder().encode(['bytes[]', 'bytes[]'], [signers, signatures]);
  251. // Should fail because signers and signatures arrays have different lengths
  252. await expect(this.mock.$_rawSignatureValidation(TEST_MESSAGE, multiSignature)).to.eventually.be.false;
  253. });
  254. it('rejects duplicated signers', async function () {
  255. // Create a valid signature
  256. const validSignature = await signerECDSA1.signMessage(ethers.getBytes(TEST_MESSAGE));
  257. // Prepare signers and signatures arrays
  258. const signers = [signerECDSA1.address, signerECDSA1.address];
  259. const signatures = [validSignature, validSignature];
  260. // Encode the multi-signature
  261. const multiSignature = ethers.AbiCoder.defaultAbiCoder().encode(['bytes[]', 'bytes[]'], [signers, signatures]);
  262. // Should fail because of duplicated signers
  263. await expect(this.mock.$_rawSignatureValidation(TEST_MESSAGE, multiSignature)).to.eventually.be.false;
  264. });
  265. });
  266. });