Add ERC7913 signers and utilities (#5659)
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
This commit is contained in:
116
test/account/AccountERC7913.test.js
Normal file
116
test/account/AccountERC7913.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
321
test/account/AccountMultiSigner.test.js
Normal file
321
test/account/AccountMultiSigner.test.js
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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 };
|
||||
|
||||
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user