123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 |
- const { ethers, predeploy } = require('hardhat');
- const { expect } = require('chai');
- const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
- const { getDomain } = require('../helpers/eip712');
- const { ERC4337Helper } = require('../helpers/erc4337');
- const { NonNativeSigner, P256SigningKey, RSASHA256SigningKey, MultiERC7913SigningKey } = require('../helpers/signers');
- const { MAX_UINT64 } = require('../helpers/constants');
- const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
- const { shouldBehaveLikeERC1271 } = require('../utils/cryptography/ERC1271.behavior');
- const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
- const { PackedUserOperation } = require('../helpers/eip712-types');
- // Prepare signers in advance (RSA are long to initialize)
- const signerECDSA1 = ethers.Wallet.createRandom();
- const signerECDSA2 = ethers.Wallet.createRandom();
- const signerECDSA3 = ethers.Wallet.createRandom();
- const signerECDSA4 = ethers.Wallet.createRandom(); // Unauthorized signer
- const signerP256 = new NonNativeSigner(P256SigningKey.random());
- const signerRSA = new NonNativeSigner(RSASHA256SigningKey.random());
- // Minimal fixture common to the different signer verifiers
- async function fixture() {
- // EOAs and environment
- const [beneficiary, other] = await ethers.getSigners();
- const target = await ethers.deployContract('CallReceiverMock');
- // ERC-7913 verifiers
- const verifierP256 = await ethers.deployContract('ERC7913P256Verifier');
- const verifierRSA = await ethers.deployContract('ERC7913RSAVerifier');
- // ERC-4337 env
- const helper = new ERC4337Helper();
- await helper.wait();
- const entrypointDomain = await getDomain(predeploy.entrypoint.v08);
- const domain = { name: 'AccountMultiSigner', version: '1', chainId: entrypointDomain.chainId }; // Missing verifyingContract
- const makeMock = (signers, threshold) =>
- helper.newAccount('$AccountMultiSignerMock', [signers, threshold, 'AccountMultiSigner', '1']).then(mock => {
- domain.verifyingContract = mock.address;
- return mock;
- });
- // Sign user operations using MultiERC7913SigningKey
- const signUserOp = function (userOp) {
- return this.signer
- .signTypedData(entrypointDomain, { PackedUserOperation }, userOp.packed)
- .then(signature => Object.assign(userOp, { signature }));
- };
- const invalidSig = function () {
- return this.signer.signMessage('invalid');
- };
- return {
- helper,
- verifierP256,
- verifierRSA,
- domain,
- target,
- beneficiary,
- other,
- makeMock,
- signUserOp,
- invalidSig,
- };
- }
- describe('AccountMultiSigner', function () {
- beforeEach(async function () {
- Object.assign(this, await loadFixture(fixture));
- });
- describe('Multi ECDSA signers with threshold=1', function () {
- beforeEach(async function () {
- this.signer = new NonNativeSigner(new MultiERC7913SigningKey([signerECDSA1]));
- this.mock = await this.makeMock([signerECDSA1.address], 1);
- });
- shouldBehaveLikeAccountCore();
- shouldBehaveLikeAccountHolder();
- shouldBehaveLikeERC1271({ erc7739: true });
- shouldBehaveLikeERC7821();
- });
- describe('Multi ECDSA signers with threshold=2', function () {
- beforeEach(async function () {
- this.signer = new NonNativeSigner(new MultiERC7913SigningKey([signerECDSA1, signerECDSA2]));
- this.mock = await this.makeMock([signerECDSA1.address, signerECDSA2.address], 2);
- });
- shouldBehaveLikeAccountCore();
- shouldBehaveLikeAccountHolder();
- shouldBehaveLikeERC1271({ erc7739: true });
- shouldBehaveLikeERC7821();
- });
- describe('Mixed signers with threshold=2', function () {
- beforeEach(async function () {
- // Create signers array with all three types
- signerP256.bytes = ethers.concat([
- this.verifierP256.target,
- signerP256.signingKey.publicKey.qx,
- signerP256.signingKey.publicKey.qy,
- ]);
- signerRSA.bytes = ethers.concat([
- this.verifierRSA.target,
- ethers.AbiCoder.defaultAbiCoder().encode(
- ['bytes', 'bytes'],
- [signerRSA.signingKey.publicKey.e, signerRSA.signingKey.publicKey.n],
- ),
- ]);
- this.signer = new NonNativeSigner(new MultiERC7913SigningKey([signerECDSA1, signerP256, signerRSA]));
- this.mock = await this.makeMock([signerECDSA1.address, signerP256.bytes, signerRSA.bytes], 2);
- });
- shouldBehaveLikeAccountCore();
- shouldBehaveLikeAccountHolder();
- shouldBehaveLikeERC1271({ erc7739: true });
- shouldBehaveLikeERC7821();
- });
- describe('Signer management', function () {
- beforeEach(async function () {
- this.signer = new NonNativeSigner(new MultiERC7913SigningKey([signerECDSA1, signerECDSA2]));
- this.mock = await this.makeMock([signerECDSA1.address, signerECDSA2.address], 1);
- await this.mock.deploy();
- });
- it('can add signers', async function () {
- const signers = [signerECDSA3.address];
- // Successfully adds a signer
- const signersArrayBefore = await this.mock.getSigners(0, MAX_UINT64).then(s => s.map(ethers.getAddress));
- await expect(this.mock.$_addSigners(signers))
- .to.emit(this.mock, 'ERC7913SignerAdded')
- .withArgs(signerECDSA3.address);
- const signersArrayAfter = await this.mock.getSigners(0, MAX_UINT64).then(s => s.map(ethers.getAddress));
- expect(signersArrayAfter.length).to.equal(signersArrayBefore.length + 1);
- expect(signersArrayAfter).to.include(ethers.getAddress(signerECDSA3.address));
- // Reverts if the signer was already added
- await expect(this.mock.$_addSigners(signers))
- .to.be.revertedWithCustomError(this.mock, 'MultiSignerERC7913AlreadyExists')
- .withArgs(...signers.map(s => s.toLowerCase()));
- });
- it('can remove signers', async function () {
- const signers = [signerECDSA2.address];
- // Successfully removes an already added signer
- const signersArrayBefore = await this.mock.getSigners(0, MAX_UINT64).then(s => s.map(ethers.getAddress));
- await expect(this.mock.$_removeSigners(signers))
- .to.emit(this.mock, 'ERC7913SignerRemoved')
- .withArgs(signerECDSA2.address);
- const signersArrayAfter = await this.mock.getSigners(0, MAX_UINT64).then(s => s.map(ethers.getAddress));
- expect(signersArrayAfter.length).to.equal(signersArrayBefore.length - 1);
- expect(signersArrayAfter).to.not.include(ethers.getAddress(signerECDSA2.address));
- // Reverts removing a signer if it doesn't exist
- await expect(this.mock.$_removeSigners(signers))
- .to.be.revertedWithCustomError(this.mock, 'MultiSignerERC7913NonexistentSigner')
- .withArgs(...signers.map(s => s.toLowerCase()));
- // Reverts if removing a signer makes the threshold unreachable
- await expect(this.mock.$_removeSigners([signerECDSA1.address]))
- .to.be.revertedWithCustomError(this.mock, 'MultiSignerERC7913UnreachableThreshold')
- .withArgs(0, 1);
- });
- it('can change threshold', async function () {
- // Reachable threshold is set
- await expect(this.mock.$_setThreshold(2)).to.emit(this.mock, 'ERC7913ThresholdSet');
- // Unreachable threshold reverts
- await expect(this.mock.$_setThreshold(3))
- .to.revertedWithCustomError(this.mock, 'MultiSignerERC7913UnreachableThreshold')
- .withArgs(2, 3);
- // Zero threshold reverts
- await expect(this.mock.$_setThreshold(0)).to.revertedWithCustomError(
- this.mock,
- 'MultiSignerERC7913ZeroThreshold',
- );
- });
- it('rejects invalid signer format', async function () {
- const invalidSigner = '0x123456'; // Too short
- await expect(this.mock.$_addSigners([invalidSigner]))
- .to.be.revertedWithCustomError(this.mock, 'MultiSignerERC7913InvalidSigner')
- .withArgs(invalidSigner);
- });
- it('can read signers and threshold', async function () {
- await expect(
- this.mock.getSigners(0, MAX_UINT64).then(s => s.map(ethers.getAddress)),
- ).to.eventually.have.deep.members([signerECDSA1.address, signerECDSA2.address]);
- await expect(this.mock.threshold()).to.eventually.equal(1);
- });
- it('checks if an address is a signer', async function () {
- // Should return true for authorized signers
- await expect(this.mock.isSigner(signerECDSA1.address)).to.eventually.be.true;
- await expect(this.mock.isSigner(signerECDSA2.address)).to.eventually.be.true;
- // Should return false for unauthorized signers
- await expect(this.mock.isSigner(signerECDSA3.address)).to.eventually.be.false;
- await expect(this.mock.isSigner(signerECDSA4.address)).to.eventually.be.false;
- });
- });
- describe('Signature validation', function () {
- const TEST_MESSAGE = ethers.keccak256(ethers.toUtf8Bytes('Test message'));
- beforeEach(async function () {
- // Set up mock with authorized signers
- this.mock = await this.makeMock([signerECDSA1.address, signerECDSA2.address], 1);
- await this.mock.deploy();
- });
- it('rejects signatures from unauthorized signers', async function () {
- // Create signatures including an unauthorized signer
- const authorizedSignature = await signerECDSA1.signMessage(ethers.getBytes(TEST_MESSAGE));
- const unauthorizedSignature = await signerECDSA4.signMessage(ethers.getBytes(TEST_MESSAGE));
- // Prepare signers and signatures arrays
- const signers = [
- signerECDSA1.address,
- signerECDSA4.address, // Unauthorized signer
- ].sort((a, b) => (ethers.toBigInt(ethers.keccak256(a)) < ethers.toBigInt(ethers.keccak256(b)) ? -1 : 1));
- const signatures = signers.map(signer => {
- if (signer === signerECDSA1.address) return authorizedSignature;
- return unauthorizedSignature;
- });
- // Encode the multi-signature
- const multiSignature = ethers.AbiCoder.defaultAbiCoder().encode(['bytes[]', 'bytes[]'], [signers, signatures]);
- // Should fail because one signer is not authorized
- await expect(this.mock.$_rawSignatureValidation(TEST_MESSAGE, multiSignature)).to.eventually.be.false;
- });
- it('rejects invalid signatures from authorized signers', async function () {
- // Create a valid signature and an invalid one from authorized signers
- const validSignature = await signerECDSA1.signMessage(ethers.getBytes(TEST_MESSAGE));
- const invalidSignature = await signerECDSA2.signMessage(ethers.toUtf8Bytes('Different message')); // Wrong message
- // Prepare signers and signatures arrays
- const signers = [signerECDSA1.address, signerECDSA2.address].sort((a, b) =>
- ethers.toBigInt(ethers.keccak256(a)) < ethers.toBigInt(ethers.keccak256(b)) ? -1 : 1,
- );
- const signatures = signers.map(signer => {
- if (signer === signerECDSA1.address) return validSignature;
- return invalidSignature;
- });
- // Encode the multi-signature
- const multiSignature = ethers.AbiCoder.defaultAbiCoder().encode(['bytes[]', 'bytes[]'], [signers, signatures]);
- // Should fail because one signature is invalid
- await expect(this.mock.$_rawSignatureValidation(TEST_MESSAGE, multiSignature)).to.eventually.be.false;
- });
- it('rejects signatures from unsorted signers', async function () {
- // Create a valid signature and an invalid one from authorized signers
- const validSignature1 = await signerECDSA1.signMessage(ethers.getBytes(TEST_MESSAGE));
- const validSignature2 = await signerECDSA2.signMessage(ethers.getBytes(TEST_MESSAGE));
- // Prepare signers and signatures arrays
- const signers = [signerECDSA1.address, signerECDSA2.address].sort((a, b) =>
- ethers.toBigInt(ethers.keccak256(a)) < ethers.toBigInt(ethers.keccak256(b)) ? -1 : 1,
- );
- const unsortedSigners = signers.reverse();
- const signatures = unsortedSigners.map(signer => {
- if (signer === signerECDSA1.address) return validSignature1;
- return validSignature2;
- });
- // Encode the multi-signature
- const multiSignature = ethers.AbiCoder.defaultAbiCoder().encode(
- ['bytes[]', 'bytes[]'],
- [unsortedSigners, signatures],
- );
- // Should fail because signers are not sorted
- await expect(this.mock.$_rawSignatureValidation(TEST_MESSAGE, multiSignature)).to.eventually.be.false;
- });
- it('rejects signatures when signers.length != signatures.length', async function () {
- // Create a valid signature and an invalid one from authorized signers
- const validSignature1 = await signerECDSA1.signMessage(ethers.getBytes(TEST_MESSAGE));
- // Prepare signers and signatures arrays
- const signers = [signerECDSA1.address, signerECDSA2.address];
- const signatures = [validSignature1];
- // Encode the multi-signature
- const multiSignature = ethers.AbiCoder.defaultAbiCoder().encode(['bytes[]', 'bytes[]'], [signers, signatures]);
- // Should fail because signers and signatures arrays have different lengths
- await expect(this.mock.$_rawSignatureValidation(TEST_MESSAGE, multiSignature)).to.eventually.be.false;
- });
- it('rejects duplicated signers', async function () {
- // Create a valid signature
- const validSignature = await signerECDSA1.signMessage(ethers.getBytes(TEST_MESSAGE));
- // Prepare signers and signatures arrays
- const signers = [signerECDSA1.address, signerECDSA1.address];
- const signatures = [validSignature, validSignature];
- // Encode the multi-signature
- const multiSignature = ethers.AbiCoder.defaultAbiCoder().encode(['bytes[]', 'bytes[]'], [signers, signatures]);
- // Should fail because of duplicated signers
- await expect(this.mock.$_rawSignatureValidation(TEST_MESSAGE, multiSignature)).to.eventually.be.false;
- });
- });
- });
|