AccountMultiSignerWeighted.test.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  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 { PackedUserOperation } = require('../helpers/eip712-types');
  8. const { MAX_UINT64 } = require('../helpers/constants');
  9. const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
  10. const { shouldBehaveLikeERC1271 } = require('../utils/cryptography/ERC1271.behavior');
  11. const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
  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();
  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: 'AccountMultiSignerWeighted', version: '1', chainId: entrypointDomain.chainId }; // Missing verifyingContract
  32. const makeMock = (signers, weights, threshold) =>
  33. helper
  34. .newAccount('$AccountMultiSignerWeightedMock', [signers, weights, threshold, 'AccountMultiSignerWeighted', '1'])
  35. .then(mock => {
  36. domain.verifyingContract = mock.address;
  37. return mock;
  38. });
  39. // Sign user operations using NonNativeSigner with MultiERC7913SigningKey
  40. const signUserOp = function (userOp) {
  41. return this.signer
  42. .signTypedData(entrypointDomain, { PackedUserOperation }, userOp.packed)
  43. .then(signature => Object.assign(userOp, { signature }));
  44. };
  45. const invalidSig = function () {
  46. return this.signer.signMessage('invalid');
  47. };
  48. return {
  49. helper,
  50. verifierP256,
  51. verifierRSA,
  52. domain,
  53. target,
  54. beneficiary,
  55. other,
  56. makeMock,
  57. signUserOp,
  58. invalidSig,
  59. };
  60. }
  61. describe('AccountMultiSignerWeighted', function () {
  62. beforeEach(async function () {
  63. Object.assign(this, await loadFixture(fixture));
  64. });
  65. describe('Weighted signers with equal weights (1, 1, 1) and threshold=2', function () {
  66. beforeEach(async function () {
  67. this.signer = new NonNativeSigner(new MultiERC7913SigningKey([signerECDSA1, signerECDSA3])); // 2 accounts, weight 1+1=2
  68. this.mock = await this.makeMock([signerECDSA1.address, signerECDSA2.address, signerECDSA3.address], [1, 1, 1], 2);
  69. });
  70. shouldBehaveLikeAccountCore();
  71. shouldBehaveLikeAccountHolder();
  72. shouldBehaveLikeERC1271({ erc7739: true });
  73. shouldBehaveLikeERC7821();
  74. });
  75. describe('Weighted signers with varying weights (1, 2, 3) and threshold=3', function () {
  76. beforeEach(async function () {
  77. this.signer = new NonNativeSigner(new MultiERC7913SigningKey([signerECDSA1, signerECDSA2])); // 2 accounts, weight 1+2=3
  78. this.mock = await this.makeMock([signerECDSA1.address, signerECDSA2.address, signerECDSA3.address], [1, 2, 3], 3);
  79. });
  80. shouldBehaveLikeAccountCore();
  81. shouldBehaveLikeAccountHolder();
  82. shouldBehaveLikeERC1271({ erc7739: true });
  83. shouldBehaveLikeERC7821();
  84. });
  85. describe('Mixed weighted signers with threshold=4', function () {
  86. beforeEach(async function () {
  87. // Create signers array with all three types
  88. signerP256.bytes = ethers.concat([
  89. this.verifierP256.target,
  90. signerP256.signingKey.publicKey.qx,
  91. signerP256.signingKey.publicKey.qy,
  92. ]);
  93. signerRSA.bytes = ethers.concat([
  94. this.verifierRSA.target,
  95. ethers.AbiCoder.defaultAbiCoder().encode(
  96. ['bytes', 'bytes'],
  97. [signerRSA.signingKey.publicKey.e, signerRSA.signingKey.publicKey.n],
  98. ),
  99. ]);
  100. this.signer = new NonNativeSigner(new MultiERC7913SigningKey([signerP256, signerRSA])); // 2 accounts, weight 2+3=5
  101. this.mock = await this.makeMock(
  102. [signerECDSA1.address, signerP256.bytes, signerRSA.bytes],
  103. [1, 2, 3],
  104. 4, // Requires at least signer2 + signer3, or all three signers
  105. );
  106. });
  107. shouldBehaveLikeAccountCore();
  108. shouldBehaveLikeAccountHolder();
  109. shouldBehaveLikeERC1271({ erc7739: true });
  110. shouldBehaveLikeERC7821();
  111. });
  112. describe('Weight management', function () {
  113. const signer1 = signerECDSA1.address;
  114. const signer2 = signerECDSA2.address;
  115. const signer3 = signerECDSA3.address;
  116. const signer4 = signerECDSA4.address;
  117. beforeEach(async function () {
  118. this.mock = await this.makeMock([signer1, signer2, signer3], [1, 2, 3], 4);
  119. await this.mock.deploy();
  120. });
  121. it('can get signer weights', async function () {
  122. await expect(this.mock.signerWeight(signer1)).to.eventually.equal(1);
  123. await expect(this.mock.signerWeight(signer2)).to.eventually.equal(2);
  124. await expect(this.mock.signerWeight(signer3)).to.eventually.equal(3);
  125. });
  126. it('can update signer weights', async function () {
  127. // Successfully updates weights and emits event
  128. await expect(this.mock.$_setSignerWeights([signer1, signer2], [5, 6]))
  129. .to.emit(this.mock, 'ERC7913SignerWeightChanged')
  130. .withArgs(signer1, 5)
  131. .to.emit(this.mock, 'ERC7913SignerWeightChanged')
  132. .withArgs(signer2, 6);
  133. await expect(this.mock.signerWeight(signer1)).to.eventually.equal(5);
  134. await expect(this.mock.signerWeight(signer2)).to.eventually.equal(6);
  135. await expect(this.mock.signerWeight(signer3)).to.eventually.equal(3); // unchanged
  136. });
  137. it("no-op doesn't emit an event", async function () {
  138. await expect(this.mock.$_setSignerWeights([signer1], [1])).to.not.emit(this.mock, 'ERC7913SignerWeightChanged');
  139. });
  140. it('cannot set weight to non-existent signer', async function () {
  141. // Reverts when setting weight for non-existent signer
  142. await expect(this.mock.$_setSignerWeights([signer4], [1]))
  143. .to.be.revertedWithCustomError(this.mock, 'MultiSignerERC7913NonexistentSigner')
  144. .withArgs(signer4.toLowerCase());
  145. });
  146. it('cannot set weight to 0', async function () {
  147. // Reverts when setting weight to 0
  148. await expect(this.mock.$_setSignerWeights([signer1], [0]))
  149. .to.be.revertedWithCustomError(this.mock, 'MultiSignerERC7913WeightedInvalidWeight')
  150. .withArgs(signer1.toLowerCase(), 0);
  151. });
  152. it('requires signers and weights arrays to have same length', async function () {
  153. // Reverts when arrays have different lengths
  154. await expect(this.mock.$_setSignerWeights([signer1, signer2], [1])).to.be.revertedWithCustomError(
  155. this.mock,
  156. 'MultiSignerERC7913WeightedMismatchedLength',
  157. );
  158. await expect(this.mock.$_setSignerWeights([signer1], [1, 2])).to.be.revertedWithCustomError(
  159. this.mock,
  160. 'MultiSignerERC7913WeightedMismatchedLength',
  161. );
  162. });
  163. it('validates threshold is reachable when updating weights', async function () {
  164. // First, lower the weights so the sum is exactly 9 (just enough for threshold=9)
  165. await expect(this.mock.$_setSignerWeights([signer1, signer2, signer3], [2, 3, 4]))
  166. .to.emit(this.mock, 'ERC7913SignerWeightChanged')
  167. .withArgs(signer1, 2)
  168. .to.emit(this.mock, 'ERC7913SignerWeightChanged')
  169. .withArgs(signer2, 3)
  170. .to.emit(this.mock, 'ERC7913SignerWeightChanged')
  171. .withArgs(signer3, 4);
  172. // Increase threshold to 9
  173. await expect(this.mock.$_setThreshold(9)).to.emit(this.mock, 'ERC7913ThresholdSet').withArgs(9);
  174. // Now try to lower weights so their sum is less than the threshold
  175. await expect(this.mock.$_setSignerWeights([signer1, signer2, signer3], [2, 2, 2])).to.be.revertedWithCustomError(
  176. this.mock,
  177. 'MultiSignerERC7913UnreachableThreshold',
  178. );
  179. // Try to increase threshold to be larger than the total weight
  180. await expect(this.mock.$_setThreshold(10))
  181. .to.be.revertedWithCustomError(this.mock, 'MultiSignerERC7913UnreachableThreshold')
  182. .withArgs(9, 10);
  183. });
  184. it('reports default weight of 1 for signers without explicit weight', async function () {
  185. // Add a new signer without setting weight
  186. await this.mock.$_addSigners([signer4]);
  187. // Should have default weight of 1
  188. await expect(this.mock.signerWeight(signer4)).to.eventually.equal(1);
  189. });
  190. it('reports weight of 0 for invalid signers', async function () {
  191. // not authorized
  192. await expect(this.mock.signerWeight(signer4)).to.eventually.equal(0);
  193. });
  194. it('can get total weight of all signers', async function () {
  195. await expect(this.mock.totalWeight()).to.eventually.equal(6); // 1+2+3=6
  196. });
  197. it('totalWeight returns correct value when all signers have default weight of 1', async function () {
  198. // Deploy a new mock with all signers having default weight (1)
  199. const signers = [signerECDSA1.address, signerECDSA2.address, signerECDSA3.address];
  200. const defaultWeights = [1, 1, 1]; // All weights are 1 (default)
  201. const newMock = await this.makeMock(signers, defaultWeights, 2);
  202. await newMock.deploy();
  203. // totalWeight should return max(3, 3) = 3 when all weights are default
  204. await expect(newMock.totalWeight()).to.eventually.equal(3);
  205. // Clear custom weights to ensure we're using default weights
  206. await newMock.$_setSignerWeights(signers, [1, 1, 1]);
  207. // totalWeight should still be max(3, 3) = 3
  208. await expect(newMock.totalWeight()).to.eventually.equal(3);
  209. });
  210. it('_setSignerWeights correctly handles default weights when updating', async function () {
  211. await expect(this.mock.totalWeight()).to.eventually.equal(6); // 1+2+3=6
  212. // Set weight for signer1 from 1 (default) to 5
  213. await this.mock.$_setSignerWeights([signer1], [5]);
  214. await expect(this.mock.totalWeight()).to.eventually.equal(10); // 5+2+3=10
  215. // Reset signer1 to default weight (1)
  216. await this.mock.$_setSignerWeights([signer1], [1]);
  217. await expect(this.mock.totalWeight()).to.eventually.equal(6); // 1+2+3=6
  218. });
  219. it('updates total weight when adding and removing signers', async function () {
  220. await expect(this.mock.totalWeight()).to.eventually.equal(6); // 1+2+3=6
  221. // Add a new signer - should increase total weight by default weight (1)
  222. await this.mock.$_addSigners([signer4]);
  223. await expect(this.mock.totalWeight()).to.eventually.equal(7); // 1+2+3+1=7
  224. // Set weight to 5 - should increase total weight by 4
  225. await this.mock.$_setSignerWeights([signer4], [5]);
  226. await expect(this.mock.totalWeight()).to.eventually.equal(11); // 1+2+3+5=11
  227. // Remove signer - should decrease total weight by current weight (5)
  228. await this.mock.$_removeSigners([signer4]);
  229. await expect(this.mock.totalWeight()).to.eventually.equal(6); // 1+2+3=6
  230. });
  231. it('removing signers should not make threshold unreachable', async function () {
  232. // current threshold = 4, totalWeight = 1+2+3 = 6
  233. // After removing signer3, the threshold is unreachable because totalWeight = 1+2 = 3 but threshold = 4
  234. // [reverts]
  235. await expect(this.mock.$_removeSigners([signer3]))
  236. .to.be.revertedWithCustomError(this.mock, 'MultiSignerERC7913UnreachableThreshold')
  237. .withArgs(3, 4);
  238. // After removing signer1, the threshold is still reachable because totalWeight = 2+3 = 5 and threshold = 4
  239. // [does not revert]
  240. await expect(this.mock.$_removeSigners([signer1]))
  241. .to.emit(this.mock, 'ERC7913SignerRemoved')
  242. .withArgs(signer1)
  243. .to.not.emit(this.mock, 'ERC7913SignerWeightChanged');
  244. });
  245. it('should revert if total weight to overflow (_setSignerWeights)', async function () {
  246. await expect(this.mock.$_setSignerWeights([signer1, signer2, signer3], [1n, 1n, MAX_UINT64 - 1n]))
  247. .to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedUintDowncast')
  248. .withArgs(64, MAX_UINT64 + 1n);
  249. });
  250. it('should revert if total weight to overflow (_addSigner)', async function () {
  251. await this.mock.$_setSignerWeights([signer1, signer2, signer3], [1n, 1n, MAX_UINT64 - 2n]);
  252. await expect(this.mock.totalWeight()).to.eventually.equal(MAX_UINT64);
  253. await expect(this.mock.$_addSigners([signer4]))
  254. .to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedUintDowncast')
  255. .withArgs(64, MAX_UINT64 + 1n);
  256. });
  257. });
  258. });