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,5 @@
---
'openzeppelin-solidity': minor
---
`ERC7913P256Verifier` and `ERC7913RSAVerifier`: Ready to use ERC-7913 verifiers that implement key verification for P256 (secp256r1) and RSA keys.

View File

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---
`SignerERC7913`: Abstract signer that verifies signatures using the ERC-7913 workflow.

View File

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---
`MultiSignerERC7913`: Implementation of `AbstractSigner` that supports multiple ERC-7913 signers with a threshold-based signature verification system.

View File

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---
`SignatureChecker`: Add support for ERC-7913 signatures alongside existing ECDSA and ERC-1271 signature verification.

View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @dev Signature verifier interface.
*/
interface IERC7913SignatureVerifier {
/**
* @dev Verifies `signature` as a valid signature of `hash` by `key`.
*
* MUST return the bytes4 magic value IERC7913SignatureVerifier.verify.selector if the signature is valid.
* SHOULD return 0xffffffff or revert if the signature is not valid.
* SHOULD return 0xffffffff or revert if the key is empty
*/
function verify(bytes calldata key, bytes32 hash, bytes calldata signature) external view returns (bytes4);
}

View File

@ -17,6 +17,8 @@ import {SignerECDSA} from "../../utils/cryptography/signers/SignerECDSA.sol";
import {SignerP256} from "../../utils/cryptography/signers/SignerP256.sol"; import {SignerP256} from "../../utils/cryptography/signers/SignerP256.sol";
import {SignerRSA} from "../../utils/cryptography/signers/SignerRSA.sol"; import {SignerRSA} from "../../utils/cryptography/signers/SignerRSA.sol";
import {SignerERC7702} from "../../utils/cryptography/signers/SignerERC7702.sol"; import {SignerERC7702} from "../../utils/cryptography/signers/SignerERC7702.sol";
import {SignerERC7913} from "../../utils/cryptography/signers/SignerERC7913.sol";
import {MultiSignerERC7913} from "../../utils/cryptography/signers/MultiSignerERC7913.sol";
abstract contract AccountMock is Account, ERC7739, ERC7821, ERC721Holder, ERC1155Holder { abstract contract AccountMock is Account, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
/// Validates a user operation with a boolean signature. /// Validates a user operation with a boolean signature.
@ -136,3 +138,34 @@ abstract contract AccountERC7579HookedMock is AccountERC7579Hooked {
_installModule(MODULE_TYPE_VALIDATOR, validator, initData); _installModule(MODULE_TYPE_VALIDATOR, validator, initData);
} }
} }
abstract contract AccountMultiSignerMock is Account, MultiSignerERC7913, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
constructor(bytes[] memory signers, uint64 threshold) {
_addSigners(signers);
_setThreshold(threshold);
}
/// @inheritdoc ERC7821
function _erc7821AuthorizedExecutor(
address caller,
bytes32 mode,
bytes calldata executionData
) internal view virtual override returns (bool) {
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
}
}
abstract contract AccountERC7913Mock is Account, SignerERC7913, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
constructor(bytes memory _signer) {
_setSigner(_signer);
}
/// @inheritdoc ERC7821
function _erc7821AuthorizedExecutor(
address caller,
bytes32 mode,
bytes calldata executionData
) internal view virtual override returns (bool) {
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
}
}

View File

@ -17,6 +17,8 @@ A collection of contracts and libraries that implement various signature validat
* {ERC7739}: An abstract contract to validate signatures following the rehashing scheme from {ERC7739Utils}. * {ERC7739}: An abstract contract to validate signatures following the rehashing scheme from {ERC7739Utils}.
* {SignerECDSA}, {SignerP256}, {SignerRSA}: Implementations of an {AbstractSigner} with specific signature validation algorithms. * {SignerECDSA}, {SignerP256}, {SignerRSA}: Implementations of an {AbstractSigner} with specific signature validation algorithms.
* {SignerERC7702}: Implementation of {AbstractSigner} that validates signatures using the contract's own address as the signer, useful for delegated accounts following EIP-7702. * {SignerERC7702}: Implementation of {AbstractSigner} that validates signatures using the contract's own address as the signer, useful for delegated accounts following EIP-7702.
* {SignerERC7913}, {MultiSignerERC7913}: Implementations of {AbstractSigner} that validate signatures based on ERC-7913. Including a simple multisignature scheme.
* {ERC7913P256Verifier}, {ERC7913RSAVerifier}: Ready to use ERC-7913 signature verifiers for P256 and RSA keys.
== Utils == Utils
@ -51,3 +53,13 @@ A collection of contracts and libraries that implement various signature validat
{{SignerRSA}} {{SignerRSA}}
{{SignerERC7702}} {{SignerERC7702}}
{{SignerERC7913}}
{{MultiSignerERC7913}}
== Verifiers
{{ERC7913P256Verifier}}
{{ERC7913RSAVerifier}}

View File

@ -5,19 +5,29 @@ pragma solidity ^0.8.24;
import {ECDSA} from "./ECDSA.sol"; import {ECDSA} from "./ECDSA.sol";
import {IERC1271} from "../../interfaces/IERC1271.sol"; import {IERC1271} from "../../interfaces/IERC1271.sol";
import {IERC7913SignatureVerifier} from "../../interfaces/IERC7913.sol";
import {Bytes} from "../../utils/Bytes.sol";
/** /**
* @dev Signature verification helper that can be used instead of `ECDSA.recover` to seamlessly support both ECDSA * @dev Signature verification helper that can be used instead of `ECDSA.recover` to seamlessly support:
* signatures from externally owned accounts (EOAs) as well as ERC-1271 signatures from smart contract wallets like *
* Argent and Safe Wallet (previously Gnosis Safe). * * ECDSA signatures from externally owned accounts (EOAs)
* * ERC-1271 signatures from smart contract wallets like Argent and Safe Wallet (previously Gnosis Safe)
* * ERC-7913 signatures from keys that do not have an Ethereum address of their own
*
* See https://eips.ethereum.org/EIPS/eip-1271[ERC-1271] and https://eips.ethereum.org/EIPS/eip-7913[ERC-7913].
*/ */
library SignatureChecker { library SignatureChecker {
using Bytes for bytes;
/** /**
* @dev Checks if a signature is valid for a given signer and data hash. If the signer has code, the * @dev Checks if a signature is valid for a given signer and data hash. If the signer has code, the
* signature is validated against it using ERC-1271, otherwise it's validated using `ECDSA.recover`. * signature is validated against it using ERC-1271, otherwise it's validated using `ECDSA.recover`.
* *
* NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus * NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus
* change through time. It could return true at block N and false at block N+1 (or the opposite). * change through time. It could return true at block N and false at block N+1 (or the opposite).
*
* NOTE: For an extended version of this function that supports ERC-7913 signatures, see {isValidERC7913SignatureNow}.
*/ */
function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) { function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) {
if (signer.code.length == 0) { if (signer.code.length == 0) {
@ -47,4 +57,79 @@ library SignatureChecker {
result.length >= 32 && result.length >= 32 &&
abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)); abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector));
} }
/**
* @dev Verifies a signature for a given ERC-7913 signer and hash.
*
* The signer is a `bytes` object that is the concatenation of an address and optionally a key:
* `verifier || key`. A signer must be at least 20 bytes long.
*
* Verification is done as follows:
*
* * If `signer.length < 20`: verification fails
* * If `signer.length == 20`: verification is done using {isValidSignatureNow}
* * Otherwise: verification is done using {IERC7913SignatureVerifier}
*
* NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus
* change through time. It could return true at block N and false at block N+1 (or the opposite).
*/
function isValidERC7913SignatureNow(
bytes memory signer,
bytes32 hash,
bytes memory signature
) internal view returns (bool) {
if (signer.length < 20) {
return false;
} else if (signer.length == 20) {
return isValidSignatureNow(address(bytes20(signer)), hash, signature);
} else {
(bool success, bytes memory result) = address(bytes20(signer)).staticcall(
abi.encodeCall(IERC7913SignatureVerifier.verify, (signer.slice(20), hash, signature))
);
return (success &&
result.length >= 32 &&
abi.decode(result, (bytes32)) == bytes32(IERC7913SignatureVerifier.verify.selector));
}
}
/**
* @dev Verifies multiple ERC-7913 `signatures` for a given `hash` using a set of `signers`.
* Returns `false` if the number of signers and signatures is not the same.
*
* The signers should be ordered by their `keccak256` hash to ensure efficient duplication check. Unordered
* signers are supported, but the uniqueness check will be more expensive.
*
* NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus
* change through time. It could return true at block N and false at block N+1 (or the opposite).
*/
function areValidERC7913SignaturesNow(
bytes32 hash,
bytes[] memory signers,
bytes[] memory signatures
) internal view returns (bool) {
if (signers.length != signatures.length) return false;
bytes32 lastId = bytes32(0);
for (uint256 i = 0; i < signers.length; ++i) {
bytes memory signer = signers[i];
// If one of the signatures is invalid, reject the batch
if (!isValidERC7913SignatureNow(signer, hash, signatures[i])) return false;
bytes32 id = keccak256(signer);
// If the current signer ID is greater than all previous IDs, then this is a new signer.
if (lastId < id) {
lastId = id;
} else {
// If this signer id is not greater than all the previous ones, verify that it is not a duplicate of a previous one
// This loop is never executed if the signers are ordered by id.
for (uint256 j = 0; j < i; ++j) {
if (id == keccak256(signers[j])) return false;
}
}
}
return true;
}
} }

View File

@ -0,0 +1,238 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
import {AbstractSigner} from "./AbstractSigner.sol";
import {SignatureChecker} from "../SignatureChecker.sol";
import {EnumerableSet} from "../../structs/EnumerableSet.sol";
/**
* @dev Implementation of {AbstractSigner} using multiple ERC-7913 signers with a threshold-based
* signature verification system.
*
* This contract allows managing a set of authorized signers and requires a minimum number of
* signatures (threshold) to approve operations. It uses ERC-7913 formatted signers, which
* makes it natively compatible with ECDSA and ERC-1271 signers.
*
* Example of usage:
*
* ```solidity
* contract MyMultiSignerAccount is Account, MultiSignerERC7913, Initializable {
* constructor() EIP712("MyMultiSignerAccount", "1") {}
*
* function initialize(bytes[] memory signers, uint64 threshold) public initializer {
* _addSigners(signers);
* _setThreshold(threshold);
* }
*
* function addSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
* _addSigners(signers);
* }
*
* function removeSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
* _removeSigners(signers);
* }
*
* function setThreshold(uint64 threshold) public onlyEntryPointOrSelf {
* _setThreshold(threshold);
* }
* }
* ```
*
* IMPORTANT: Failing to properly initialize the signers and threshold either during construction
* (if used standalone) or during initialization (if used as a clone) may leave the contract
* either front-runnable or unusable.
*/
abstract contract MultiSignerERC7913 is AbstractSigner {
using EnumerableSet for EnumerableSet.BytesSet;
using SignatureChecker for *;
EnumerableSet.BytesSet private _signers;
uint64 private _threshold;
/// @dev Emitted when a signer is added.
event ERC7913SignerAdded(bytes indexed signers);
/// @dev Emitted when a signers is removed.
event ERC7913SignerRemoved(bytes indexed signers);
/// @dev Emitted when the threshold is updated.
event ERC7913ThresholdSet(uint64 threshold);
/// @dev The `signer` already exists.
error MultiSignerERC7913AlreadyExists(bytes signer);
/// @dev The `signer` does not exist.
error MultiSignerERC7913NonexistentSigner(bytes signer);
/// @dev The `signer` is less than 20 bytes long.
error MultiSignerERC7913InvalidSigner(bytes signer);
/// @dev The `threshold` is unreachable given the number of `signers`.
error MultiSignerERC7913UnreachableThreshold(uint64 signers, uint64 threshold);
/**
* @dev Returns a slice of the set of authorized signers.
*
* Using `start = 0` and `end = type(uint64).max` will return the entire set of signers.
*
* WARNING: Depending on the `start` and `end`, this operation can copy a large amount of data to memory, which
* can be expensive. This is designed for view accessors queried without gas fees. Using it in state-changing
* functions may become uncallable if the slice grows too large.
*/
function getSigners(uint64 start, uint64 end) public view virtual returns (bytes[] memory) {
return _signers.values(start, end);
}
/// @dev Returns whether the `signer` is an authorized signer.
function isSigner(bytes memory signer) public view virtual returns (bool) {
return _signers.contains(signer);
}
/// @dev Returns the minimum number of signers required to approve a multisignature operation.
function threshold() public view virtual returns (uint64) {
return _threshold;
}
/**
* @dev Adds the `newSigners` to those allowed to sign on behalf of this contract.
* Internal version without access control.
*
* Requirements:
*
* * Each of `newSigners` must be at least 20 bytes long. Reverts with {MultiSignerERC7913InvalidSigner} if not.
* * Each of `newSigners` must not be authorized. See {isSigner}. Reverts with {MultiSignerERC7913AlreadyExists} if so.
*/
function _addSigners(bytes[] memory newSigners) internal virtual {
for (uint256 i = 0; i < newSigners.length; ++i) {
bytes memory signer = newSigners[i];
require(signer.length >= 20, MultiSignerERC7913InvalidSigner(signer));
require(_signers.add(signer), MultiSignerERC7913AlreadyExists(signer));
emit ERC7913SignerAdded(signer);
}
}
/**
* @dev Removes the `oldSigners` from the authorized signers. Internal version without access control.
*
* Requirements:
*
* * Each of `oldSigners` must be authorized. See {isSigner}. Otherwise {MultiSignerERC7913NonexistentSigner} is thrown.
* * See {_validateReachableThreshold} for the threshold validation.
*/
function _removeSigners(bytes[] memory oldSigners) internal virtual {
for (uint256 i = 0; i < oldSigners.length; ++i) {
bytes memory signer = oldSigners[i];
require(_signers.remove(signer), MultiSignerERC7913NonexistentSigner(signer));
emit ERC7913SignerRemoved(signer);
}
_validateReachableThreshold();
}
/**
* @dev Sets the signatures `threshold` required to approve a multisignature operation.
* Internal version without access control.
*
* Requirements:
*
* * See {_validateReachableThreshold} for the threshold validation.
*/
function _setThreshold(uint64 newThreshold) internal virtual {
_threshold = newThreshold;
_validateReachableThreshold();
emit ERC7913ThresholdSet(newThreshold);
}
/**
* @dev Validates the current threshold is reachable.
*
* Requirements:
*
* * The {signers}'s length must be `>=` to the {threshold}. Throws {MultiSignerERC7913UnreachableThreshold} if not.
*/
function _validateReachableThreshold() internal view virtual {
uint256 signersLength = _signers.length();
uint64 currentThreshold = threshold();
require(
signersLength >= currentThreshold,
MultiSignerERC7913UnreachableThreshold(
uint64(signersLength), // Safe cast. Economically impossible to overflow.
currentThreshold
)
);
}
/**
* @dev Decodes, validates the signature and checks the signers are authorized.
* See {_validateSignatures} and {_validateThreshold} for more details.
*
* Example of signature encoding:
*
* ```solidity
* // Encode signers (verifier || key)
* bytes memory signer1 = abi.encodePacked(verifier1, key1);
* bytes memory signer2 = abi.encodePacked(verifier2, key2);
*
* // Order signers by their id
* if (keccak256(signer1) > keccak256(signer2)) {
* (signer1, signer2) = (signer2, signer1);
* (signature1, signature2) = (signature2, signature1);
* }
*
* // Assign ordered signers and signatures
* bytes[] memory signers = new bytes[](2);
* bytes[] memory signatures = new bytes[](2);
* signers[0] = signer1;
* signatures[0] = signature1;
* signers[1] = signer2;
* signatures[1] = signature2;
*
* // Encode the multi signature
* bytes memory signature = abi.encode(signers, signatures);
* ```
*
* Requirements:
*
* * The `signature` must be encoded as `abi.encode(signers, signatures)`.
*/
function _rawSignatureValidation(
bytes32 hash,
bytes calldata signature
) internal view virtual override returns (bool) {
if (signature.length == 0) return false; // For ERC-7739 compatibility
(bytes[] memory signers, bytes[] memory signatures) = abi.decode(signature, (bytes[], bytes[]));
return _validateThreshold(signers) && _validateSignatures(hash, signers, signatures);
}
/**
* @dev Validates the signatures using the signers and their corresponding signatures.
* Returns whether whether the signers are authorized and the signatures are valid for the given hash.
*
* IMPORTANT: Sorting the signers by their `keccak256` hash will improve the gas efficiency of this function.
* See {SignatureChecker-areValidERC7913SignaturesNow} for more details.
*
* Requirements:
*
* * The `signatures` arrays must be at least as large as the `signers` arrays. Panics otherwise.
*/
function _validateSignatures(
bytes32 hash,
bytes[] memory signers,
bytes[] memory signatures
) internal view virtual returns (bool valid) {
for (uint256 i = 0; i < signers.length; ++i) {
if (!isSigner(signers[i])) {
return false;
}
}
return hash.areValidERC7913SignaturesNow(signers, signatures);
}
/**
* @dev Validates that the number of signers meets the {threshold} requirement.
* Assumes the signers were already validated. See {_validateSignatures} for more details.
*/
function _validateThreshold(bytes[] memory validatingSigners) internal view virtual returns (bool) {
return validatingSigners.length >= threshold();
}
}

View File

@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {AbstractSigner} from "./AbstractSigner.sol";
import {SignatureChecker} from "../SignatureChecker.sol";
/**
* @dev Implementation of {AbstractSigner} using
* https://eips.ethereum.org/EIPS/eip-7913[ERC-7913] signature verification.
*
* For {Account} usage, a {_setSigner} function is provided to set the ERC-7913 formatted {signer}.
* Doing so is easier for a factory, who is likely to use initializable clones of this contract.
*
* The signer is a `bytes` object that concatenates a verifier address and a key: `verifier || key`.
*
* Example of usage:
*
* ```solidity
* contract MyAccountERC7913 is Account, SignerERC7913, Initializable {
* function initialize(bytes memory signer_) public initializer {
* _setSigner(signer_);
* }
* }
* ```
*
* IMPORTANT: Failing to call {_setSigner} either during construction (if used standalone)
* or during initialization (if used as a clone) may leave the signer either front-runnable or unusable.
*/
abstract contract SignerERC7913 is AbstractSigner {
bytes private _signer;
/// @dev Return the ERC-7913 signer (i.e. `verifier || key`).
function signer() public view virtual returns (bytes memory) {
return _signer;
}
/// @dev Sets the signer (i.e. `verifier || key`) with an ERC-7913 formatted signer.
function _setSigner(bytes memory signer_) internal {
_signer = signer_;
}
/// @dev Verifies a signature using {SignatureChecker-isValidERC7913SignatureNow} with {signer}, `hash` and `signature`.
function _rawSignatureValidation(
bytes32 hash,
bytes calldata signature
) internal view virtual override returns (bool) {
return SignatureChecker.isValidERC7913SignatureNow(signer(), hash, signature);
}
}

View File

@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {P256} from "../../../utils/cryptography/P256.sol";
import {IERC7913SignatureVerifier} from "../../../interfaces/IERC7913.sol";
/**
* @dev ERC-7913 signature verifier that support P256 (secp256r1) keys.
*/
contract ERC7913P256Verifier is IERC7913SignatureVerifier {
/// @inheritdoc IERC7913SignatureVerifier
function verify(bytes calldata key, bytes32 hash, bytes calldata signature) public view virtual returns (bytes4) {
// Signature length may be 0x40 or 0x41.
if (key.length == 0x40 && signature.length >= 0x40) {
bytes32 qx = bytes32(key[0x00:0x20]);
bytes32 qy = bytes32(key[0x20:0x40]);
bytes32 r = bytes32(signature[0x00:0x20]);
bytes32 s = bytes32(signature[0x20:0x40]);
if (P256.verify(hash, r, s, qx, qy)) {
return IERC7913SignatureVerifier.verify.selector;
}
}
return 0xFFFFFFFF;
}
}

View File

@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {RSA} from "../../../utils/cryptography/RSA.sol";
import {IERC7913SignatureVerifier} from "../../../interfaces/IERC7913.sol";
/**
* @dev ERC-7913 signature verifier that support RSA keys.
*/
contract ERC7913RSAVerifier is IERC7913SignatureVerifier {
/// @inheritdoc IERC7913SignatureVerifier
function verify(bytes calldata key, bytes32 hash, bytes calldata signature) public view virtual returns (bytes4) {
(bytes memory e, bytes memory n) = abi.decode(key, (bytes, bytes));
return
RSA.pkcs1Sha256(abi.encodePacked(hash), signature, e, n)
? IERC7913SignatureVerifier.verify.selector
: bytes4(0xFFFFFFFF);
}
}

View File

@ -97,6 +97,80 @@ function _verify(
IMPORTANT: Always use keys of at least 2048 bits. Additionally, be aware that PKCS#1 v1.5 allows for replayability due to the possibility of arbitrary optional parameters. To prevent replay attacks, consider including an onchain nonce or unique identifier in the message. IMPORTANT: Always use keys of at least 2048 bits. Additionally, be aware that PKCS#1 v1.5 allows for replayability due to the possibility of arbitrary optional parameters. To prevent replay attacks, consider including an onchain nonce or unique identifier in the message.
=== Signature Verification
The xref:api:utils.adoc#SignatureChecker[`SignatureChecker`] library provides a unified interface for verifying signatures from different sources. It seamlessly supports:
* ECDSA signatures from externally owned accounts (EOAs)
* ERC-1271 signatures from smart contract wallets like Argent and Safe Wallet
* ERC-7913 signatures from keys that don't have their own Ethereum address
This allows developers to write signature verification code once and have it work across all these different signature types.
==== Basic Signature Verification
For standard signature verification that supports both EOAs and ERC-1271 contracts:
[source,solidity]
----
using SignatureChecker for address;
function _verifySignature(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) {
return SignatureChecker.isValidSignatureNow(signer, hash, signature);
}
----
The library automatically detects whether the signer is an EOA or a contract and uses the appropriate verification method.
==== ERC-1271 Contract Signatures
For smart contract wallets that implement ERC-1271, you can explicitly use:
[source,solidity]
----
function _verifyContractSignature(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) {
return SignatureChecker.isValidERC1271SignatureNow(signer, hash, signature);
}
----
==== ERC-7913 Extended Signatures
ERC-7913 extends signature verification to support keys that don't have their own Ethereum address. This is useful for integrating non-Ethereum cryptographic curves, hardware devices, or other identity systems.
A signer is represented as a `bytes` object that concatenates a verifier address and a key: `verifier || key`.
[source,solidity]
----
function _verifyERC7913Signature(bytes memory signer, bytes32 hash, bytes memory signature) internal view returns (bool) {
return SignatureChecker.isValidERC7913SignatureNow(signer, hash, signature);
}
----
The verification process works as follows:
* If `signer.length < 20`: verification fails
* If `signer.length == 20`: verification is done using standard signature checking
* Otherwise: verification is done using an ERC-7913 verifier
==== Batch Verification
For verifying multiple ERC-7913 signatures at once:
[source,solidity]
----
function _verifyMultipleSignatures(
bytes32 hash,
bytes[] memory signers,
bytes[] memory signatures
) internal view returns (bool) {
return SignatureChecker.areValidERC7913SignaturesNow(hash, signers, signatures);
}
----
This function will reject inputs that contain duplicated signers. Sorting the signers by their `keccak256` hash is recommended to minimize the gas cost.
This unified approach allows smart contracts to accept signatures from any supported source without needing to implement different verification logic for each type.
=== Verifying Merkle Proofs === Verifying Merkle Proofs
Developers can build a Merkle Tree off-chain, which allows for verifying that an element (leaf) is part of a set by using a Merkle Proof. This technique is widely used for creating whitelists (e.g., for airdrops) and other advanced use cases. Developers can build a Merkle Tree off-chain, which allows for verifying that an element (leaf) is part of a set by using a Merkle Proof. This technique is widely used for creating whitelists (e.g., for airdrops) and other advanced use cases.

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 { const {
AbiCoder,
AbstractSigner, AbstractSigner,
Signature, Signature,
TypedDataEncoder, TypedDataEncoder,
@ -13,6 +14,7 @@ const {
hexlify, hexlify,
sha256, sha256,
toBeHex, toBeHex,
keccak256,
} = require('ethers'); } = require('ethers');
const { secp256r1 } = require('@noble/curves/p256'); const { secp256r1 } = require('@noble/curves/p256');
const { generateKeyPairSync, privateEncrypt } = require('crypto'); 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 { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const precompile = require('../../helpers/precompiles'); const precompile = require('../../helpers/precompiles');
const { P256SigningKey, NonNativeSigner } = require('../../helpers/signers');
const TEST_MESSAGE = ethers.id('OpenZeppelin'); const TEST_MESSAGE = ethers.id('OpenZeppelin');
const TEST_MESSAGE_HASH = ethers.hashMessage(TEST_MESSAGE); 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 = ethers.id('Nope');
const WRONG_MESSAGE_HASH = ethers.hashMessage(WRONG_MESSAGE); const WRONG_MESSAGE_HASH = ethers.hashMessage(WRONG_MESSAGE);
const aliceP256 = new NonNativeSigner(P256SigningKey.random());
const bobP256 = new NonNativeSigner(P256SigningKey.random());
async function fixture() { async function fixture() {
const [signer, other] = await ethers.getSigners(); const [signer, extraSigner, other] = await ethers.getSigners();
const mock = await ethers.deployContract('$SignatureChecker'); const mock = await ethers.deployContract('$SignatureChecker');
const wallet = await ethers.deployContract('ERC1271WalletMock', [signer]); const wallet = await ethers.deployContract('ERC1271WalletMock', [signer]);
const wallet2 = await ethers.deployContract('ERC1271WalletMock', [extraSigner]);
const malicious = await ethers.deployContract('ERC1271MaliciousMock'); const malicious = await ethers.deployContract('ERC1271MaliciousMock');
const signature = await signer.signMessage(TEST_MESSAGE); 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 () { 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;
});
});
});
}); });