Add ERC7913 signers and utilities (#5659)

Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
This commit is contained in:
Ernesto García
2025-06-05 09:22:26 -06:00
committed by GitHub
parent 8bff2a72d9
commit 1d9400e053
17 changed files with 1374 additions and 6 deletions

View File

@ -0,0 +1,116 @@
const { ethers, entrypoint } = require('hardhat');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { getDomain } = require('../helpers/eip712');
const { ERC4337Helper } = require('../helpers/erc4337');
const { NonNativeSigner, P256SigningKey, RSASHA256SigningKey } = require('../helpers/signers');
const { PackedUserOperation } = require('../helpers/eip712-types');
const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
const { shouldBehaveLikeERC1271 } = require('../utils/cryptography/ERC1271.behavior');
const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
// Prepare signer in advance (RSA are long to initialize)
const signerECDSA = ethers.Wallet.createRandom();
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(entrypoint.v08);
const domain = { name: 'AccountERC7913', version: '1', chainId: entrypointDomain.chainId }; // Missing verifyingContract,
const makeMock = signer =>
helper.newAccount('$AccountERC7913Mock', ['AccountERC7913', '1', signer]).then(mock => {
domain.verifyingContract = mock.address;
return mock;
});
const signUserOp = function (userOp) {
return this.signer
.signTypedData(entrypointDomain, { PackedUserOperation }, userOp.packed)
.then(signature => Object.assign(userOp, { signature }));
};
return {
helper,
verifierP256,
verifierRSA,
domain,
target,
beneficiary,
other,
makeMock,
signUserOp,
};
}
describe('AccountERC7913', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
// Using ECDSA key as verifier
describe('ECDSA key', function () {
beforeEach(async function () {
this.signer = signerECDSA;
this.mock = await this.makeMock(this.signer.address);
});
shouldBehaveLikeAccountCore();
shouldBehaveLikeAccountHolder();
shouldBehaveLikeERC1271({ erc7739: true });
shouldBehaveLikeERC7821();
});
// Using P256 key with an ERC-7913 verifier
describe('P256 key', function () {
beforeEach(async function () {
this.signer = signerP256;
this.mock = await this.makeMock(
ethers.concat([
this.verifierP256.target,
this.signer.signingKey.publicKey.qx,
this.signer.signingKey.publicKey.qy,
]),
);
});
shouldBehaveLikeAccountCore();
shouldBehaveLikeAccountHolder();
shouldBehaveLikeERC1271({ erc7739: true });
shouldBehaveLikeERC7821();
});
// Using RSA key with an ERC-7913 verifier
describe('RSA key', function () {
beforeEach(async function () {
this.signer = signerRSA;
this.mock = await this.makeMock(
ethers.concat([
this.verifierRSA.target,
ethers.AbiCoder.defaultAbiCoder().encode(
['bytes', 'bytes'],
[this.signer.signingKey.publicKey.e, this.signer.signingKey.publicKey.n],
),
]),
);
});
shouldBehaveLikeAccountCore();
shouldBehaveLikeAccountHolder();
shouldBehaveLikeERC1271({ erc7739: true });
shouldBehaveLikeERC7821();
});
});

View File

@ -0,0 +1,321 @@
const { ethers, entrypoint } = 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(entrypoint.v08);
const domain = { name: 'AccountMultiSigner', version: '1', chainId: entrypointDomain.chainId }; // Missing verifyingContract
const makeMock = (signers, threshold) =>
helper.newAccount('$AccountMultiSignerMock', ['AccountMultiSigner', '1', signers, threshold]).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',
);
});
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;
});
});
});

View File

@ -1,4 +1,5 @@
const {
AbiCoder,
AbstractSigner,
Signature,
TypedDataEncoder,
@ -13,6 +14,7 @@ const {
hexlify,
sha256,
toBeHex,
keccak256,
} = require('ethers');
const { secp256r1 } = require('@noble/curves/p256');
const { generateKeyPairSync, privateEncrypt } = require('crypto');
@ -144,4 +146,39 @@ class RSASHA256SigningKey extends RSASigningKey {
}
}
module.exports = { NonNativeSigner, P256SigningKey, RSASigningKey, RSASHA256SigningKey };
class MultiERC7913SigningKey {
// this is a sorted array of objects that contain {signer, weight}
#signers;
constructor(signers) {
assertArgument(
Array.isArray(signers) && signers.length > 0,
'signers must be a non-empty array',
'signers',
signers.length,
);
// Sorting is done at construction so that it doesn't have to be done in sign()
this.#signers = signers.sort((s1, s2) => keccak256(s1.bytes ?? s1.address) - keccak256(s2.bytes ?? s2.address));
}
get signers() {
return this.#signers;
}
sign(digest /*: BytesLike*/ /*: Signature*/) {
assertArgument(dataLength(digest) === 32, 'invalid digest length', 'digest', digest);
return {
serialized: AbiCoder.defaultAbiCoder().encode(
['bytes[]', 'bytes[]'],
[
this.#signers.map(signer => signer.bytes ?? signer.address),
this.#signers.map(signer => signer.signingKey.sign(digest).serialized),
],
),
};
}
}
module.exports = { NonNativeSigner, P256SigningKey, RSASigningKey, RSASHA256SigningKey, MultiERC7913SigningKey };

View File

@ -3,6 +3,7 @@ const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const precompile = require('../../helpers/precompiles');
const { P256SigningKey, NonNativeSigner } = require('../../helpers/signers');
const TEST_MESSAGE = ethers.id('OpenZeppelin');
const TEST_MESSAGE_HASH = ethers.hashMessage(TEST_MESSAGE);
@ -10,14 +11,19 @@ const TEST_MESSAGE_HASH = ethers.hashMessage(TEST_MESSAGE);
const WRONG_MESSAGE = ethers.id('Nope');
const WRONG_MESSAGE_HASH = ethers.hashMessage(WRONG_MESSAGE);
const aliceP256 = new NonNativeSigner(P256SigningKey.random());
const bobP256 = new NonNativeSigner(P256SigningKey.random());
async function fixture() {
const [signer, other] = await ethers.getSigners();
const [signer, extraSigner, other] = await ethers.getSigners();
const mock = await ethers.deployContract('$SignatureChecker');
const wallet = await ethers.deployContract('ERC1271WalletMock', [signer]);
const wallet2 = await ethers.deployContract('ERC1271WalletMock', [extraSigner]);
const malicious = await ethers.deployContract('ERC1271MaliciousMock');
const signature = await signer.signMessage(TEST_MESSAGE);
const verifier = await ethers.deployContract('ERC7913P256Verifier');
return { signer, other, mock, wallet, malicious, signature };
return { signer, other, extraSigner, mock, wallet, wallet2, malicious, signature, verifier };
}
describe('SignatureChecker (ERC1271)', function () {
@ -72,4 +78,316 @@ describe('SignatureChecker (ERC1271)', function () {
});
}
});
describe('ERC7913', function () {
describe('isValidERC7913SignatureNow', function () {
describe('with EOA signer', function () {
it('with matching signer and signature', async function () {
const eoaSigner = ethers.zeroPadValue(this.signer.address, 20);
const signature = await this.signer.signMessage(TEST_MESSAGE);
await expect(this.mock.$isValidERC7913SignatureNow(eoaSigner, TEST_MESSAGE_HASH, signature)).to.eventually.be
.true;
});
it('with invalid signer', async function () {
const eoaSigner = ethers.zeroPadValue(this.other.address, 20);
const signature = await this.signer.signMessage(TEST_MESSAGE);
await expect(this.mock.$isValidERC7913SignatureNow(eoaSigner, TEST_MESSAGE_HASH, signature)).to.eventually.be
.false;
});
it('with invalid signature', async function () {
const eoaSigner = ethers.zeroPadValue(this.signer.address, 20);
const signature = await this.signer.signMessage(TEST_MESSAGE);
await expect(this.mock.$isValidERC7913SignatureNow(eoaSigner, WRONG_MESSAGE_HASH, signature)).to.eventually.be
.false;
});
});
describe('with ERC-1271 wallet', function () {
it('with matching signer and signature', async function () {
const walletSigner = ethers.zeroPadValue(this.wallet.target, 20);
const signature = await this.signer.signMessage(TEST_MESSAGE);
await expect(this.mock.$isValidERC7913SignatureNow(walletSigner, TEST_MESSAGE_HASH, signature)).to.eventually
.be.true;
});
it('with invalid signer', async function () {
const walletSigner = ethers.zeroPadValue(this.mock.target, 20);
const signature = await this.signer.signMessage(TEST_MESSAGE);
await expect(this.mock.$isValidERC7913SignatureNow(walletSigner, TEST_MESSAGE_HASH, signature)).to.eventually
.be.false;
});
it('with invalid signature', async function () {
const walletSigner = ethers.zeroPadValue(this.wallet.target, 20);
const signature = await this.signer.signMessage(TEST_MESSAGE);
await expect(this.mock.$isValidERC7913SignatureNow(walletSigner, WRONG_MESSAGE_HASH, signature)).to.eventually
.be.false;
});
});
describe('with ERC-7913 verifier', function () {
it('with matching signer and signature', async function () {
const signer = ethers.concat([
this.verifier.target,
aliceP256.signingKey.publicKey.qx,
aliceP256.signingKey.publicKey.qy,
]);
const signature = await aliceP256.signMessage(TEST_MESSAGE);
await expect(this.mock.$isValidERC7913SignatureNow(signer, TEST_MESSAGE_HASH, signature)).to.eventually.be
.true;
});
it('with invalid verifier', async function () {
const signer = ethers.concat([
this.mock.target, // invalid verifier
aliceP256.signingKey.publicKey.qx,
aliceP256.signingKey.publicKey.qy,
]);
const signature = await aliceP256.signMessage(TEST_MESSAGE);
await expect(this.mock.$isValidERC7913SignatureNow(signer, TEST_MESSAGE_HASH, signature)).to.eventually.be
.false;
});
it('with invalid key', async function () {
const signer = ethers.concat([this.verifier.target, ethers.randomBytes(32)]);
const signature = await aliceP256.signMessage(TEST_MESSAGE);
await expect(this.mock.$isValidERC7913SignatureNow(signer, TEST_MESSAGE_HASH, signature)).to.eventually.be
.false;
});
it('with invalid signature', async function () {
const signer = ethers.concat([
this.verifier.target,
aliceP256.signingKey.publicKey.qx,
aliceP256.signingKey.publicKey.qy,
]);
const signature = ethers.randomBytes(65); // invalid (random) signature
await expect(this.mock.$isValidERC7913SignatureNow(signer, TEST_MESSAGE_HASH, signature)).to.eventually.be
.false;
});
it('with signer too short', async function () {
const signer = ethers.randomBytes(19); // too short
const signature = await aliceP256.signMessage(TEST_MESSAGE);
await expect(this.mock.$isValidERC7913SignatureNow(signer, TEST_MESSAGE_HASH, signature)).to.eventually.be
.false;
});
});
});
describe('areValidERC7913SignaturesNow', function () {
const sortSigners = (...signers) =>
signers.sort(({ signer: a }, { signer: b }) => ethers.keccak256(b) - ethers.keccak256(a));
it('should validate a single signature', async function () {
const signer = ethers.zeroPadValue(this.signer.address, 20);
const signature = await this.signer.signMessage(TEST_MESSAGE);
await expect(this.mock.$areValidERC7913SignaturesNow(TEST_MESSAGE_HASH, [signer], [signature])).to.eventually.be
.true;
});
it('should validate multiple signatures with different signer types', async function () {
const signers = sortSigners(
{
signer: ethers.zeroPadValue(this.signer.address, 20),
signature: await this.signer.signMessage(TEST_MESSAGE),
},
{
signer: ethers.zeroPadValue(this.wallet.target, 20),
signature: await this.signer.signMessage(TEST_MESSAGE),
},
{
signer: ethers.concat([
this.verifier.target,
aliceP256.signingKey.publicKey.qx,
aliceP256.signingKey.publicKey.qy,
]),
signature: await aliceP256.signMessage(TEST_MESSAGE),
},
);
await expect(
this.mock.$areValidERC7913SignaturesNow(
TEST_MESSAGE_HASH,
signers.map(({ signer }) => signer),
signers.map(({ signature }) => signature),
),
).to.eventually.be.true;
});
it('should validate multiple EOA signatures', async function () {
const signers = sortSigners(
{
signer: ethers.zeroPadValue(this.signer.address, 20),
signature: await this.signer.signMessage(TEST_MESSAGE),
},
{
signer: ethers.zeroPadValue(this.extraSigner.address, 20),
signature: await this.extraSigner.signMessage(TEST_MESSAGE),
},
);
await expect(
this.mock.$areValidERC7913SignaturesNow(
TEST_MESSAGE_HASH,
signers.map(({ signer }) => signer),
signers.map(({ signature }) => signature),
),
).to.eventually.be.true;
});
it('should validate multiple ERC-1271 wallet signatures', async function () {
const signers = sortSigners(
{
signer: ethers.zeroPadValue(this.wallet.target, 20),
signature: await this.signer.signMessage(TEST_MESSAGE),
},
{
signer: ethers.zeroPadValue(this.wallet2.target, 20),
signature: await this.extraSigner.signMessage(TEST_MESSAGE),
},
);
await expect(
this.mock.$areValidERC7913SignaturesNow(
TEST_MESSAGE_HASH,
signers.map(({ signer }) => signer),
signers.map(({ signature }) => signature),
),
).to.eventually.be.true;
});
it('should validate multiple ERC-7913 signatures (ordered by ID)', async function () {
const signers = sortSigners(
{
signer: ethers.concat([
this.verifier.target,
aliceP256.signingKey.publicKey.qx,
aliceP256.signingKey.publicKey.qy,
]),
signature: await aliceP256.signMessage(TEST_MESSAGE),
},
{
signer: ethers.concat([
this.verifier.target,
bobP256.signingKey.publicKey.qx,
bobP256.signingKey.publicKey.qy,
]),
signature: await bobP256.signMessage(TEST_MESSAGE),
},
);
await expect(
this.mock.$areValidERC7913SignaturesNow(
TEST_MESSAGE_HASH,
signers.map(({ signer }) => signer),
signers.map(({ signature }) => signature),
),
).to.eventually.be.true;
});
it('should validate multiple ERC-7913 signatures (unordered)', async function () {
const signers = sortSigners(
{
signer: ethers.concat([
this.verifier.target,
aliceP256.signingKey.publicKey.qx,
aliceP256.signingKey.publicKey.qy,
]),
signature: await aliceP256.signMessage(TEST_MESSAGE),
},
{
signer: ethers.concat([
this.verifier.target,
bobP256.signingKey.publicKey.qx,
bobP256.signingKey.publicKey.qy,
]),
signature: await bobP256.signMessage(TEST_MESSAGE),
},
).reverse(); // reverse
await expect(
this.mock.$areValidERC7913SignaturesNow(
TEST_MESSAGE_HASH,
signers.map(({ signer }) => signer),
signers.map(({ signature }) => signature),
),
).to.eventually.be.true;
});
it('should return false if any signature is invalid', async function () {
const signers = sortSigners(
{
signer: ethers.zeroPadValue(this.signer.address, 20),
signature: await this.signer.signMessage(TEST_MESSAGE),
},
{
signer: ethers.zeroPadValue(this.extraSigner.address, 20),
signature: await this.extraSigner.signMessage(WRONG_MESSAGE),
},
);
await expect(
this.mock.$areValidERC7913SignaturesNow(
TEST_MESSAGE_HASH,
signers.map(({ signer }) => signer),
signers.map(({ signature }) => signature),
),
).to.eventually.be.false;
});
it('should return false if there are duplicate signers', async function () {
const signers = sortSigners(
{
signer: ethers.zeroPadValue(this.signer.address, 20),
signature: await this.signer.signMessage(TEST_MESSAGE),
},
{
signer: ethers.zeroPadValue(this.signer.address, 20),
signature: await this.signer.signMessage(TEST_MESSAGE),
},
);
await expect(
this.mock.$areValidERC7913SignaturesNow(
TEST_MESSAGE_HASH,
signers.map(({ signer }) => signer),
signers.map(({ signature }) => signature),
),
).to.eventually.be.false;
});
it('should return false if signatures array length does not match signers array length', async function () {
const signers = sortSigners(
{
signer: ethers.zeroPadValue(this.signer.address, 20),
signature: await this.signer.signMessage(TEST_MESSAGE),
},
{
signer: ethers.zeroPadValue(this.extraSigner.address, 20),
signature: await this.extraSigner.signMessage(TEST_MESSAGE),
},
);
await expect(
this.mock.$areValidERC7913SignaturesNow(
TEST_MESSAGE_HASH,
signers.map(({ signer }) => signer),
signers.map(({ signature }) => signature).slice(1),
),
).to.eventually.be.false;
});
it('should pass with empty arrays', async function () {
await expect(this.mock.$areValidERC7913SignaturesNow(TEST_MESSAGE_HASH, [], [])).to.eventually.be.true;
});
});
});
});