Skip to content

Commit 9b73593

Browse files
committed
Simplify ERC-7579 validator modules (#159)
1 parent a023191 commit 9b73593

13 files changed

+109
-123
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
- `PaymasterERC20Guarantor`: Add extension of `PaymasterERC20` that enables third parties to guarantee user operations by prefunding gas costs upfront, with repayment handling for successful operations.
1616
- `ERC7579Validator`: Add abstract validator module for ERC-7579 accounts that provides base implementation for signature validation.
17-
- `ERC7579SignatureValidator`: Add implementation of `ERC7579Validator` that enables ERC-7579 accounts to integrate with address-less cryptographic keys through ERC-7913 signature verification.
17+
- `ERC7579Signature`: Add implementation of `ERC7579Validator` that enables ERC-7579 accounts to integrate with address-less cryptographic keys and account signatures through ERC-7913 signature verification.
1818

1919
## 29-04-2025
2020

contracts/account/README.adoc

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ This directory includes contracts to build accounts for ERC-4337. These include:
88
* {AccountERC7579}: An extension of `Account` that implements support for ERC-7579 modules.
99
* {AccountERC7579Hooked}: An extension of `AccountERC7579` with support for a single hook module (type 4).
1010
* {ERC7821}: Minimal batch executor implementation contracts. Useful to enable easy batch execution for smart contracts.
11-
* {ERC7579Validator}: Abstract validator module for ERC-7579 accounts that provides base implementation for signature validation.
12-
* {ERC7579SignatureValidator}: Implementation of ERC7579Validator using ERC-7913 signature verification for address-less cryptographic keys.
13-
* {ERC7579Multisig}: An abstract multisig module for ERC-7579 accounts using ERC-7913 signer keys.
14-
* {ERC7579MultisigWeighted}: An abstract weighted multisig module that allows different weights to be assigned to signers.
15-
* {ERC7579MultisigConfirmation}: An abstract confirmation-based multisig module that each signer to provide a confirmation signature.
1611
* {ERC7579Executor}: An executor module that enables executing calls from accounts where the it's installed.
1712
* {ERC7579DelayedExecutor}: An executor module that adds a delay before executing an account operation.
13+
* {ERC7579Validator}: Abstract validator module for ERC-7579 accounts that provides base implementation for signature validation.
14+
* {ERC7579Signature}: Implementation of {ERC7579Validator} using ERC-7913 signature verification for address-less cryptographic keys and account signatures.
15+
* {ERC7579Multisig}: An extension of {ERC7579Validator} that enables validation using ERC-7913 signer keys.
16+
* {ERC7579MultisigWeighted}: An extension of {ERC7579Multisig} that allows different weights to be assigned to signers.
17+
* {ERC7579MultisigConfirmation}: An extension of {ERC7579Multisig} that requires each signer to provide a confirmation signature.
1818
* {PaymasterCore}: An ERC-4337 paymaster implementation that includes the core logic to validate and pay for user operations.
1919
* {PaymasterERC20}: A paymaster that allows users to pay for user operations using ERC-20 tokens.
2020
* {PaymasterERC20Guarantor}: A paymaster that enables third parties to guarantee user operations by pre-funding gas costs, with the option for users to repay or for guarantors to absorb the cost.
@@ -35,12 +35,6 @@ This directory includes contracts to build accounts for ERC-4337. These include:
3535

3636
== Modules
3737

38-
{{ERC7579Multisig}}
39-
40-
{{ERC7579MultisigWeighted}}
41-
42-
{{ERC7579MultisigConfirmation}}
43-
4438
=== Executors
4539

4640
{{ERC7579Executor}}
@@ -51,7 +45,13 @@ This directory includes contracts to build accounts for ERC-4337. These include:
5145

5246
{{ERC7579Validator}}
5347

54-
{{ERC7579SignatureValidator}}
48+
{{ERC7579Signature}}
49+
50+
{{ERC7579Multisig}}
51+
52+
{{ERC7579MultisigWeighted}}
53+
54+
{{ERC7579MultisigConfirmation}}
5555

5656
== Paymaster
5757

contracts/account/modules/ERC7579Multisig.sol

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,23 @@ pragma solidity ^0.8.27;
33

44
import {ERC7913Utils} from "../../utils/cryptography/ERC7913Utils.sol";
55
import {EnumerableSetExtended} from "../../utils/structs/EnumerableSetExtended.sol";
6-
import {IERC7579Module} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
76
import {Mode} from "@openzeppelin/contracts/account/utils/draft-ERC7579Utils.sol";
7+
import {ERC7579Validator} from "./ERC7579Validator.sol";
88

99
/**
10-
* @dev Implementation of an {IERC7579Module} that uses ERC-7913 signers for multisignature
10+
* @dev Implementation of an {ERC7579Validator} that uses ERC-7913 signers for multisignature
1111
* validation.
1212
*
1313
* This module provides a base implementation for multisignature validation that can be
14-
* attached to any function through the {_validateMultisignature} internal function. The signers
14+
* attached to any function through the {_rawERC7579Validation} internal function. The signers
1515
* are represented using the ERC-7913 format, which concatenates a verifier address and
1616
* a key: `verifier || key`.
1717
*
18-
* Example implementation:
19-
*
20-
* ```solidity
21-
* function execute(
22-
* address account,
23-
* Mode mode,
24-
* bytes calldata executionCalldata,
25-
* bytes32 salt,
26-
* bytes calldata signature
27-
* ) public virtual {
28-
* require(_validateMultisignature(account, hash, signature));
29-
* // ... rest of execute logic
30-
* }
31-
* ```
32-
*
33-
* Example use case:
34-
*
3518
* A smart account with this module installed can require multiple signers to approve
3619
* operations before they are executed, such as requiring 3-of-5 guardians to approve
3720
* a social recovery operation.
3821
*/
39-
abstract contract ERC7579Multisig is IERC7579Module {
22+
abstract contract ERC7579Multisig is ERC7579Validator {
4023
using EnumerableSetExtended for EnumerableSetExtended.BytesSet;
4124
using ERC7913Utils for bytes32;
4225
using ERC7913Utils for bytes;
@@ -178,11 +161,11 @@ abstract contract ERC7579Multisig is IERC7579Module {
178161
* Where `signingSigners` are the authorized signers and signatures are their corresponding
179162
* signatures of the operation `hash`.
180163
*/
181-
function _validateMultisignature(
164+
function _rawERC7579Validation(
182165
address account,
183166
bytes32 hash,
184167
bytes calldata signature
185-
) internal view virtual returns (bool) {
168+
) internal view virtual override returns (bool) {
186169
(bytes[] memory signingSigners, bytes[] memory signatures) = abi.decode(signature, (bytes[], bytes[]));
187170
return
188171
_validateThreshold(account, signingSigners) &&

contracts/account/modules/ERC7579SignatureValidator.sol renamed to contracts/account/modules/ERC7579Signature.sol

Lines changed: 14 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,53 +10,22 @@ import {IERC7579Module} from "@openzeppelin/contracts/interfaces/draft-IERC7579.
1010
* @dev Implementation of {ERC7579Validator} module using ERC-7913 signature verification.
1111
*
1212
* This validator allows ERC-7579 accounts to integrate with address-less cryptographic keys
13-
* through the ERC-7913 signature verification system. Each account can store its own ERC-7913
14-
* formatted signer (a concatenation of a verifier address and a key: `verifier || key`).
13+
* and account signatures through the ERC-7913 signature verification system. Each account
14+
* can store its own ERC-7913 formatted signer (a concatenation of a verifier address and a
15+
* key: `verifier || key`).
1516
*
1617
* This enables accounts to use signature schemes without requiring each key to have its own
17-
* Ethereum address.
18-
*
19-
* The validator implements two key functions from ERC-7579:
20-
*
21-
* * `validateUserOp`: Validates ERC-4337 user operations using ERC-7913 signatures
22-
* * `isValidSignatureWithSender`: Implements ERC-1271 signature verification via ERC-7913
23-
*
24-
* Example usage with an account:
25-
*
26-
* ```solidity
27-
* contract MyAccount is Account, AccountERC7579 {
28-
* function initialize(address validator, bytes memory signerData) public initializer {
29-
* // Install the validator module
30-
* bytes memory initData = abi.encode(signerData);
31-
* _installModule(MODULE_TYPE_VALIDATOR, validator, initData);
32-
* }
33-
* }
34-
* ```
35-
*
36-
* Example of validator installation with a P256 key:
37-
*
38-
* ```solidity
39-
* // Address of the P256 verifier contract
40-
* address p256verifier = 0x123...;
41-
*
42-
* // P256 public key bytes
43-
* bytes memory p256PublicKey = 0x456...;
44-
*
45-
* // Combine into ERC-7913 signer format
46-
* bytes memory signerData = bytes.concat(abi.encodePacked(p256verifier), p256PublicKey);
47-
*
48-
* // Initialize the account with the validator and signer
49-
* account.initialize(address(new ERC7579SignatureValidator()), signerData);
50-
* ```
18+
* Ethereum address.A smart account with this module installed can keep an emergency key as a
19+
* backup.
5120
*/
52-
contract ERC7579SignatureValidator is ERC7579Validator {
21+
contract ERC7579Signature is ERC7579Validator {
5322
mapping(address account => bytes signer) private _signers;
5423

5524
/// @dev Emitted when the signer is set.
56-
event ERC7579SignatureValidatorSignerSet(address indexed account, bytes signer);
25+
event ERC7579SignatureSignerSet(address indexed account, bytes signer);
5726

5827
/// @dev Thrown when the signer length is less than 20 bytes.
59-
error ERC7579SignatureValidatorInvalidSignerLength();
28+
error ERC7579SignatureInvalidSignerLength();
6029

6130
/// @dev Return the ERC-7913 signer (i.e. `verifier || key`).
6231
function signer(address account) public view virtual returns (bytes memory) {
@@ -88,30 +57,30 @@ contract ERC7579SignatureValidator is ERC7579Validator {
8857

8958
/// @dev Sets the ERC-7913 signer (i.e. `verifier || key`) for the calling account.
9059
function setSigner(bytes memory signer_) public virtual {
91-
require(signer_.length >= 20, ERC7579SignatureValidatorInvalidSignerLength());
60+
require(signer_.length >= 20, ERC7579SignatureInvalidSignerLength());
9261
_setSigner(msg.sender, signer_);
9362
}
9463

9564
/// @dev Internal version of {setSigner} that takes an `account` as argument without validating `signer_`.
9665
function _setSigner(address account, bytes memory signer_) internal virtual {
9766
_signers[account] = signer_;
98-
emit ERC7579SignatureValidatorSignerSet(account, signer_);
67+
emit ERC7579SignatureSignerSet(account, signer_);
9968
}
10069

10170
/**
102-
* @dev See {ERC7579Validator-_rawSignatureValidationWithSender}.
71+
* @dev See {ERC7579Validator-_rawERC7579Validation}.
10372
*
10473
* Validates a `signature` using ERC-7913 verification.
10574
*
10675
* This base implementation ignores the `sender` parameter and validates using
10776
* the account's stored signer. Derived contracts can override this to implement
10877
* custom validation logic based on the sender.
10978
*/
110-
function _rawSignatureValidationWithSender(
111-
address /* sender */,
79+
function _rawERC7579Validation(
80+
address account,
11281
bytes32 hash,
11382
bytes calldata signature
11483
) internal view virtual override returns (bool) {
115-
return ERC7913Utils.isValidSignatureNow(signer(msg.sender), hash, signature);
84+
return ERC7913Utils.isValidSignatureNow(signer(account), hash, signature);
11685
}
11786
}

contracts/account/modules/ERC7579Validator.sol

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import {IERC7579Module, IERC7579Validator, MODULE_TYPE_VALIDATOR} from "@openzep
66
import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
77
import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
88
import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
9+
910
/**
1011
* @dev Abstract validator module for ERC-7579 accounts.
1112
*
1213
* This contract provides the base implementation for signature validation in ERC-7579 accounts.
13-
* Developers must implement the onInstall, onUninstall, and {_rawSignatureValidationWithSender}
14+
* Developers must implement the onInstall, onUninstall, and {_rawERC7579Validation}
1415
* functions in derived contracts to define the specific signature validation logic.
1516
*
1617
* Example usage:
@@ -25,15 +26,31 @@ import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
2526
* // Uninstall logic here
2627
* }
2728
*
28-
* function _rawSignatureValidationWithSender(
29-
* address sender,
29+
* function _rawERC7579Validation(
30+
* address account,
3031
* bytes32 hash,
3132
* bytes calldata signature
3233
* ) internal view override returns (bool) {
3334
* // Signature validation logic here
3435
* }
3536
* }
3637
* ```
38+
*
39+
* Developers can restrict other operations by using the internal {_rawERC7579Validation}.
40+
* Example usage:
41+
*
42+
* ```solidity
43+
* function execute(
44+
* address account,
45+
* Mode mode,
46+
* bytes calldata executionCalldata,
47+
* bytes32 salt,
48+
* bytes calldata signature
49+
* ) public virtual {
50+
* require(_rawERC7579Validation(account, hash, signature));
51+
* // ... rest of execute logic
52+
* }
53+
* ```
3754
*/
3855
abstract contract ERC7579Validator is IERC7579Module, IERC7579Validator {
3956
/// @inheritdoc IERC7579Module
@@ -47,33 +64,37 @@ abstract contract ERC7579Validator is IERC7579Module, IERC7579Validator {
4764
bytes32 userOpHash
4865
) public view virtual returns (uint256) {
4966
return
50-
_rawSignatureValidationWithSender(msg.sender, userOpHash, userOp.signature)
67+
_rawERC7579Validation(msg.sender, userOpHash, userOp.signature)
5168
? ERC4337Utils.SIG_VALIDATION_SUCCESS
5269
: ERC4337Utils.SIG_VALIDATION_FAILED;
5370
}
5471

55-
/// @inheritdoc IERC7579Validator
72+
/**
73+
* @dev See {IERC7579Validator-isValidSignatureWithSender}.
74+
*
75+
* Ignores the `sender` parameter and validates using {_rawERC7579Validation}.
76+
* Consider overriding this function to implement custom validation logic
77+
* based on the original sender.
78+
*/
5679
function isValidSignatureWithSender(
57-
address sender,
80+
address /* sender */,
5881
bytes32 hash,
5982
bytes calldata signature
6083
) public view virtual returns (bytes4) {
6184
return
62-
_rawSignatureValidationWithSender(sender, hash, signature)
85+
_rawERC7579Validation(msg.sender, hash, signature)
6386
? IERC1271.isValidSignature.selector
6487
: bytes4(0xffffffff);
6588
}
6689

6790
/**
68-
* @dev Internal version of {isValidSignatureWithSender} to be implemented by derived contracts.
91+
* @dev Validation algorithm.
6992
*
70-
* WARNING: Signature validation is a critical security function for smart accounts as it
71-
* determines whether operations can be executed on the account. Implementations must carefully
72-
* handle cryptographic verification to prevent unauthorized access. Thorough security review and
73-
* testing are required before deployment.
93+
* WARNING: Validation is a critical security function. Implementations must carefully
94+
* handle cryptographic verification to prevent unauthorized access.
7495
*/
75-
function _rawSignatureValidationWithSender(
76-
address sender,
96+
function _rawERC7579Validation(
97+
address account,
7798
bytes32 hash,
7899
bytes calldata signature
79100
) internal view virtual returns (bool);

contracts/mocks/account/modules/ERC7579MultisigMocks.sol

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pragma solidity ^0.8.27;
44

55
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
66
import {ERC7579Executor} from "../../../account/modules/ERC7579Executor.sol";
7+
import {ERC7579Validator} from "../../../account/modules/ERC7579Validator.sol";
78
import {ERC7579Multisig} from "../../../account/modules/ERC7579Multisig.sol";
89
import {ERC7579MultisigWeighted} from "../../../account/modules/ERC7579MultisigWeighted.sol";
910
import {ERC7579MultisigConfirmation} from "../../../account/modules/ERC7579MultisigConfirmation.sol";
@@ -14,6 +15,10 @@ abstract contract ERC7579MultisigExecutorMock is EIP712, ERC7579Executor, ERC757
1415
bytes32 private constant EXECUTE_OPERATION =
1516
keccak256("ExecuteOperation(address account,bytes32 mode,bytes executionCalldata,bytes32 salt)");
1617

18+
function isModuleType(uint256 moduleTypeId) public pure override(ERC7579Executor, ERC7579Validator) returns (bool) {
19+
return ERC7579Executor.isModuleType(moduleTypeId) || ERC7579Executor.isModuleType(moduleTypeId);
20+
}
21+
1722
// Data encoding: [uint16(executionCalldataLength), executionCalldata, signature]
1823
function _validateExecution(
1924
address account,
@@ -24,7 +29,7 @@ abstract contract ERC7579MultisigExecutorMock is EIP712, ERC7579Executor, ERC757
2429
uint16 executionCalldataLength = uint16(uint256(bytes32(data[0:2]))); // First 2 bytes are the length
2530
bytes calldata executionCalldata = data[2:2 + executionCalldataLength]; // Next bytes are the calldata
2631
bytes32 typeHash = _getExecuteTypeHash(account, salt, mode, executionCalldata);
27-
require(_validateMultisignature(account, typeHash, data[2 + executionCalldataLength:])); // Remaining bytes are the signature
32+
require(_rawERC7579Validation(account, typeHash, data[2 + executionCalldataLength:])); // Remaining bytes are the signature
2833
return executionCalldata;
2934
}
3035

@@ -42,6 +47,10 @@ abstract contract ERC7579MultisigWeightedExecutorMock is EIP712, ERC7579Executor
4247
bytes32 private constant EXECUTE_OPERATION =
4348
keccak256("ExecuteOperation(address account,bytes32 mode,bytes executionCalldata,bytes32 salt)");
4449

50+
function isModuleType(uint256 moduleTypeId) public pure override(ERC7579Executor, ERC7579Validator) returns (bool) {
51+
return ERC7579Executor.isModuleType(moduleTypeId) || ERC7579Executor.isModuleType(moduleTypeId);
52+
}
53+
4554
// Data encoding: [uint16(executionCalldataLength), executionCalldata, signature]
4655
function _validateExecution(
4756
address account,
@@ -52,7 +61,7 @@ abstract contract ERC7579MultisigWeightedExecutorMock is EIP712, ERC7579Executor
5261
uint16 executionCalldataLength = uint16(uint256(bytes32(data[0:2]))); // First 2 bytes are the length
5362
bytes calldata executionCalldata = data[2:2 + executionCalldataLength]; // Next bytes are the calldata
5463
bytes32 typeHash = _getExecuteTypeHash(account, salt, mode, executionCalldata);
55-
require(_validateMultisignature(account, typeHash, data[2 + executionCalldataLength:])); // Remaining bytes are the signature
64+
require(_rawERC7579Validation(account, typeHash, data[2 + executionCalldataLength:])); // Remaining bytes are the signature
5665
return executionCalldata;
5766
}
5867

@@ -70,6 +79,10 @@ abstract contract ERC7579MultisigConfirmationExecutorMock is ERC7579Executor, ER
7079
bytes32 private constant EXECUTE_OPERATION =
7180
keccak256("ExecuteOperation(address account,bytes32 mode,bytes executionCalldata,bytes32 salt)");
7281

82+
function isModuleType(uint256 moduleTypeId) public pure override(ERC7579Executor, ERC7579Validator) returns (bool) {
83+
return ERC7579Executor.isModuleType(moduleTypeId) || ERC7579Executor.isModuleType(moduleTypeId);
84+
}
85+
7386
// Data encoding: [uint16(executionCalldataLength), executionCalldata, signature]
7487
function _validateExecution(
7588
address account,
@@ -80,7 +93,7 @@ abstract contract ERC7579MultisigConfirmationExecutorMock is ERC7579Executor, ER
8093
uint16 executionCalldataLength = uint16(uint256(bytes32(data[0:2]))); // First 2 bytes are the length
8194
bytes calldata executionCalldata = data[2:2 + executionCalldataLength]; // Next bytes are the calldata
8295
bytes32 typeHash = _getExecuteTypeHash(account, salt, mode, executionCalldata);
83-
require(_validateMultisignature(account, typeHash, data[2 + executionCalldataLength:])); // Remaining bytes are the signature
96+
require(_rawERC7579Validation(account, typeHash, data[2 + executionCalldataLength:])); // Remaining bytes are the signature
8497
return executionCalldata;
8598
}
8699

test/account/examples/AccountERC7702WithModulesMock.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ async function fixture() {
2323
await setBalance(eoa.address, ethers.WeiPerEther);
2424

2525
// ERC-7579 validator module
26-
const validator = await ethers.deployContract('$ERC7579SignatureValidator');
26+
const validator = await ethers.deployContract('$ERC7579Signature');
2727

2828
// ERC-4337 account
2929
const helper = new ERC4337Helper();

test/account/extensions/AccountERC7579.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ async function fixture() {
2222
const anotherTarget = await ethers.deployContract('CallReceiverMockExtended');
2323

2424
// ERC-7579 signature validator
25-
const erc7579Validator = await ethers.deployContract('$ERC7579SignatureValidator');
25+
const erc7579Validator = await ethers.deployContract('$ERC7579Signature');
2626

2727
// ERC-7913 verifiers
2828
const verifierP256 = await ethers.deployContract('ERC7913P256Verifier');

0 commit comments

Comments
 (0)