Add ERC7913 signers and utilities (#5659)
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
This commit is contained in:
5
.changeset/nice-rings-wish.md
Normal file
5
.changeset/nice-rings-wish.md
Normal 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.
|
||||||
5
.changeset/quiet-kiwis-feel.md
Normal file
5
.changeset/quiet-kiwis-feel.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'openzeppelin-solidity': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
`SignerERC7913`: Abstract signer that verifies signatures using the ERC-7913 workflow.
|
||||||
5
.changeset/social-walls-obey.md
Normal file
5
.changeset/social-walls-obey.md
Normal 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.
|
||||||
5
.changeset/sour-pens-shake.md
Normal file
5
.changeset/sour-pens-shake.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'openzeppelin-solidity': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
`SignatureChecker`: Add support for ERC-7913 signatures alongside existing ECDSA and ERC-1271 signature verification.
|
||||||
17
contracts/interfaces/IERC7913.sol
Normal file
17
contracts/interfaces/IERC7913.sol
Normal 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);
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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}}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
238
contracts/utils/cryptography/signers/MultiSignerERC7913.sol
Normal file
238
contracts/utils/cryptography/signers/MultiSignerERC7913.sol
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
51
contracts/utils/cryptography/signers/SignerERC7913.sol
Normal file
51
contracts/utils/cryptography/signers/SignerERC7913.sol
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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.
|
||||||
|
|||||||
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 {
|
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 };
|
||||||
|
|||||||
@ -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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user