Skip to content

Commit 54251ec

Browse files
Add ERC7579SelectorExecutor and ERC7579MultisigStorage (#162)
Co-authored-by: Bence Háromi <[email protected]>
1 parent 742a9bb commit 54251ec

File tree

7 files changed

+950
-0
lines changed

7 files changed

+950
-0
lines changed

contracts/account/README.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ This directory includes contracts to build accounts for ERC-4337. These include:
1010
* {ERC7821}: Minimal batch executor implementation contracts. Useful to enable easy batch execution for smart contracts.
1111
* {ERC7579Executor}: An executor module that enables executing calls from accounts where the it's installed.
1212
* {ERC7579DelayedExecutor}: An executor module that adds a delay before executing an account operation.
13+
* {ERC7579SelectorExecutor}: An executor module that restricts execution to specific function selectors.
1314
* {ERC7579Validator}: Abstract validator module for ERC-7579 accounts that provides base implementation for signature validation.
1415
* {ERC7579Signature}: Implementation of {ERC7579Validator} using ERC-7913 signature verification for address-less cryptographic keys and account signatures.
1516
* {ERC7579Multisig}: An extension of {ERC7579Validator} that enables validation using ERC-7913 signer keys.
1617
* {ERC7579MultisigWeighted}: An extension of {ERC7579Multisig} that allows different weights to be assigned to signers.
1718
* {ERC7579MultisigConfirmation}: An extension of {ERC7579Multisig} that requires each signer to provide a confirmation signature.
19+
* {ERC7579MultisigStorage}: An extension of {ERC7579Multisig} that allows storing presigned approvals in storage.
1820
* {PaymasterCore}: An ERC-4337 paymaster implementation that includes the core logic to validate and pay for user operations.
1921
* {PaymasterERC20}: A paymaster that allows users to pay for user operations using ERC-20 tokens.
2022
* {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.
@@ -41,6 +43,8 @@ This directory includes contracts to build accounts for ERC-4337. These include:
4143

4244
{{ERC7579DelayedExecutor}}
4345

46+
{{ERC7579SelectorExecutor}}
47+
4448
=== Validators
4549

4650
{{ERC7579Validator}}
@@ -53,6 +57,8 @@ This directory includes contracts to build accounts for ERC-4337. These include:
5357

5458
{{ERC7579MultisigConfirmation}}
5559

60+
{{ERC7579MultisigStorage}}
61+
5662
== Paymaster
5763

5864
{{PaymasterCore}}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.27;
3+
4+
import {ERC7579Multisig} from "./ERC7579Multisig.sol";
5+
import {ERC7913Utils} from "../../utils/cryptography/ERC7913Utils.sol";
6+
7+
/**
8+
* @dev Extension of {ERC7579Multisig} that allows storing presigned approvals in storage.
9+
*
10+
* This module extends the multisignature module to allow signers to presign operations,
11+
* which are then stored in a mapping and can be used during validation. This enables
12+
* more flexible multisignature workflows where signatures can be collected over time
13+
* without requiring all signers to be online simultaneously.
14+
*
15+
* When validating signatures, if a signature is empty, it indicates a presignature
16+
* and the validation will check the storage mapping instead of cryptographic verification.
17+
*/
18+
abstract contract ERC7579MultisigStorage is ERC7579Multisig {
19+
using ERC7913Utils for bytes;
20+
21+
/// @dev Emitted when a signer signs a hash
22+
event ERC7579MultisigStoragePresigned(address indexed account, bytes32 indexed hash, bytes signer);
23+
24+
mapping(address account => mapping(bytes signer => mapping(bytes32 hash => bool))) private _presigned;
25+
26+
/// @dev Returns whether a signer has presigned a specific hash for the account
27+
function presigned(address account, bytes memory signer, bytes32 hash) public view virtual returns (bool) {
28+
return _presigned[account][signer][hash];
29+
}
30+
31+
/**
32+
* @dev Allows a signer to presign a hash by providing a valid signature.
33+
* The signature will be verified and if valid, the presignature will be stored.
34+
*
35+
* Emits {ERC7579MultisigStoragePresigned} if the signature is valid and the hash is not already
36+
* signed, otherwise acts as a no-op.
37+
*
38+
* NOTE: Does not check if the signer is authorized for the account. Valid signatures from
39+
* invalid signers won't be executable. See {_validateSignatures} for more details.
40+
*/
41+
function presign(address account, bytes calldata signer, bytes32 hash, bytes calldata signature) public virtual {
42+
if (!presigned(account, signer, hash) && signer.isValidSignatureNow(hash, signature)) {
43+
_presigned[account][signer][hash] = true;
44+
emit ERC7579MultisigStoragePresigned(account, hash, signer);
45+
}
46+
}
47+
48+
/**
49+
* @dev See {ERC7579Multisig-_validateSignatures}.
50+
*
51+
* If a signature is empty, it indicates a presignature and the validation will check the storage mapping
52+
* instead of cryptographic verification. See {sign} for more details.
53+
*/
54+
function _validateSignatures(
55+
address account,
56+
bytes32 hash,
57+
bytes[] memory signingSigners,
58+
bytes[] memory signatures
59+
) internal view virtual override returns (bool valid) {
60+
uint256 signersLength = signingSigners.length;
61+
62+
// Check validity of presigned signatures
63+
uint256 presignedCount = 0;
64+
for (uint256 i = 0; i < signersLength; i++) {
65+
if (signatures[i].length == 0) {
66+
// Presigned signature
67+
if (!isSigner(account, signingSigners[i]) || !presigned(account, signingSigners[i], hash)) {
68+
return false;
69+
}
70+
presignedCount++;
71+
}
72+
}
73+
74+
// Filter out presigned signatures
75+
uint256 regular = signersLength - presignedCount;
76+
bytes[] memory _signingSigners = new bytes[](regular);
77+
bytes[] memory _signatures = new bytes[](regular);
78+
79+
uint256 regularIndex = 0;
80+
for (uint256 i = 0; i < signersLength; i++) {
81+
if (signatures[i].length != 0) {
82+
_signingSigners[regularIndex] = signingSigners[i];
83+
_signatures[regularIndex] = signatures[i];
84+
regularIndex++;
85+
}
86+
}
87+
88+
return super._validateSignatures(account, hash, _signingSigners, _signatures);
89+
}
90+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.27;
3+
4+
import {IERC7579Module, MODULE_TYPE_EXECUTOR} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
5+
import {ERC7579Executor} from "./ERC7579Executor.sol";
6+
import {ERC7579Utils} from "@openzeppelin/contracts/account/utils/draft-ERC7579Utils.sol";
7+
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
8+
9+
/**
10+
* @dev Implementation of an {ERC7579Executor} that allows authorizing specific function selectors
11+
* that can be executed on the account.
12+
*
13+
* This module provides a way to restrict which functions can be executed on the account by
14+
* maintaining a set of allowed function selectors. Only calls to functions with selectors
15+
* in the set will be allowed to execute.
16+
*/
17+
abstract contract ERC7579SelectorExecutor is ERC7579Executor {
18+
using EnumerableSet for EnumerableSet.Bytes32Set;
19+
20+
/// @dev Emitted when a selector is added to the set
21+
event ERC7579ExecutorSelectorAuthorized(address indexed account, bytes4 selector);
22+
23+
/// @dev Emitted when a selector is removed from the set
24+
event ERC7579ExecutorSelectorRemoved(address indexed account, bytes4 selector);
25+
26+
/// @dev Error thrown when attempting to execute a non-authorized selector
27+
error ERC7579ExecutorSelectorNotAuthorized(bytes4 selector);
28+
29+
/// @dev Mapping from account to set of authorized selectors
30+
mapping(address account => EnumerableSet.Bytes32Set) private _authorizedSelectors;
31+
32+
/// @dev Returns whether a selector is authorized for the specified account
33+
function isAuthorized(address account, bytes4 selector) public view virtual returns (bool) {
34+
return _authorizedSelectors[account].contains(selector);
35+
}
36+
37+
/**
38+
* @dev Returns the set of authorized selectors for the specified account.
39+
*
40+
* WARNING: This operation copies the entire selectors set to memory, which
41+
* can be expensive or may result in unbounded computation.
42+
*/
43+
function selectors(address account) public view virtual returns (bytes4[] memory) {
44+
bytes32[] memory bytes32Selectors = _authorizedSelectors[account].values();
45+
bytes4[] memory selectors_ = new bytes4[](bytes32Selectors.length);
46+
for (uint256 i = 0; i < bytes32Selectors.length; i++) {
47+
selectors_[i] = bytes4(bytes32Selectors[i]);
48+
}
49+
return selectors_;
50+
}
51+
52+
/**
53+
* @dev Sets up the module's initial configuration when installed by an account.
54+
* The initData should be encoded as: `abi.encode(bytes4[] selectors)`
55+
*/
56+
function onInstall(bytes calldata initData) public virtual override {
57+
if (initData.length > 0) {
58+
bytes4[] memory selectors_ = abi.decode(initData, (bytes4[]));
59+
_addSelectors(msg.sender, selectors_);
60+
}
61+
}
62+
63+
/**
64+
* @dev Cleans up module's configuration when uninstalled from an account.
65+
* Clears all selectors.
66+
*
67+
* WARNING: This function has unbounded gas costs and may become uncallable if the set grows too large.
68+
* See {EnumerableSetExtended-clear}.
69+
*/
70+
function onUninstall(bytes calldata /* data */) public virtual override {
71+
_authorizedSelectors[msg.sender].clear();
72+
}
73+
74+
/// @dev Adds `selectors` to the set for the calling account
75+
function addSelectors(bytes4[] memory newSelectors) public virtual {
76+
_addSelectors(msg.sender, newSelectors);
77+
}
78+
79+
/// @dev Removes a selector from the set for the calling account
80+
function removeSelectors(bytes4[] memory oldSelectors) public virtual {
81+
_removeSelectors(msg.sender, oldSelectors);
82+
}
83+
84+
/// @dev Internal version of {addSelectors} that takes an `account` as argument
85+
function _addSelectors(address account, bytes4[] memory newSelectors) internal virtual {
86+
uint256 newSelectorsLength = newSelectors.length;
87+
for (uint256 i = 0; i < newSelectorsLength; i++) {
88+
if (_authorizedSelectors[account].add(newSelectors[i])) {
89+
emit ERC7579ExecutorSelectorAuthorized(account, newSelectors[i]);
90+
} // no-op if the selector is already in the set
91+
}
92+
}
93+
94+
/// @dev Internal version of {removeSelectors} that takes an `account` as argument
95+
function _removeSelectors(address account, bytes4[] memory oldSelectors) internal virtual {
96+
uint256 oldSelectorsLength = oldSelectors.length;
97+
for (uint256 i = 0; i < oldSelectorsLength; i++) {
98+
if (_authorizedSelectors[account].remove(oldSelectors[i])) {
99+
emit ERC7579ExecutorSelectorRemoved(account, oldSelectors[i]);
100+
} // no-op if the selector is not in the set
101+
}
102+
}
103+
104+
/**
105+
* @dev See {ERC7579Executor-_validateExecution}.
106+
* Validates that the selector (first 4 bytes of the actual callData) is authorized before execution.
107+
*/
108+
function _validateExecution(
109+
address account,
110+
bytes32 /* salt */,
111+
bytes32 /* mode */,
112+
bytes calldata data
113+
) internal virtual override returns (bytes calldata) {
114+
// Decode ERC7579 single execution calldata to extract the actual function callData
115+
(, , bytes calldata callData) = ERC7579Utils.decodeSingle(data);
116+
117+
bytes4 selector = bytes4(callData[0:4]);
118+
require(isAuthorized(account, selector), ERC7579ExecutorSelectorNotAuthorized(selector));
119+
120+
return data;
121+
}
122+
}

contracts/mocks/account/modules/ERC7579MultisigMocks.sol

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {ERC7579Validator} from "../../../account/modules/ERC7579Validator.sol";
88
import {ERC7579Multisig} from "../../../account/modules/ERC7579Multisig.sol";
99
import {ERC7579MultisigWeighted} from "../../../account/modules/ERC7579MultisigWeighted.sol";
1010
import {ERC7579MultisigConfirmation} from "../../../account/modules/ERC7579MultisigConfirmation.sol";
11+
import {ERC7579MultisigStorage} from "../../../account/modules/ERC7579MultisigStorage.sol";
1112
import {MODULE_TYPE_EXECUTOR, IERC7579Hook} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
1213
import {Mode} from "@openzeppelin/contracts/account/utils/draft-ERC7579Utils.sol";
1314

@@ -106,3 +107,35 @@ abstract contract ERC7579MultisigConfirmationExecutorMock is ERC7579Executor, ER
106107
return _hashTypedDataV4(keccak256(abi.encode(EXECUTE_OPERATION, account, salt, mode, executionCalldata)));
107108
}
108109
}
110+
111+
abstract contract ERC7579MultisigStorageExecutorMock is EIP712, ERC7579Executor, ERC7579MultisigStorage {
112+
bytes32 private constant EXECUTE_OPERATION =
113+
keccak256("ExecuteOperation(address account,bytes32 mode,bytes executionCalldata,bytes32 salt)");
114+
115+
function isModuleType(uint256 moduleTypeId) public pure override(ERC7579Executor, ERC7579Validator) returns (bool) {
116+
return ERC7579Executor.isModuleType(moduleTypeId) || ERC7579Executor.isModuleType(moduleTypeId);
117+
}
118+
119+
// Data encoding: [uint16(executionCalldataLength), executionCalldata, signature]
120+
function _validateExecution(
121+
address account,
122+
bytes32 salt,
123+
bytes32 mode,
124+
bytes calldata data
125+
) internal view override returns (bytes calldata) {
126+
uint16 executionCalldataLength = uint16(bytes2(data[0:2])); // First 2 bytes are the length
127+
bytes calldata executionCalldata = data[2:2 + executionCalldataLength]; // Next bytes are the calldata
128+
bytes32 typeHash = _getExecuteTypeHash(account, salt, mode, executionCalldata);
129+
require(_rawERC7579Validation(account, typeHash, data[2 + executionCalldataLength:])); // Remaining bytes are the signature
130+
return executionCalldata;
131+
}
132+
133+
function _getExecuteTypeHash(
134+
address account,
135+
bytes32 salt,
136+
bytes32 mode,
137+
bytes calldata executionCalldata
138+
) internal view returns (bytes32) {
139+
return _hashTypedDataV4(keccak256(abi.encode(EXECUTE_OPERATION, account, salt, mode, executionCalldata)));
140+
}
141+
}

docs/modules/ROOT/pages/account-modules.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,4 @@ For additional functionality, developers can use:
126126

127127
* xref:api:account.adoc#ERC7579MultisigWeighted[`ERC7579MultisigWeighted`] to assign different weights to signers
128128
* xref:api:account.adoc#ERC7579MultisigConfirmation[`ERC7579MultisigConfirmation`] to implement a confirmation system that verifies signatures when adding signers
129+
* xref:api:account.adoc#ERC7579MultisigStorage[`ERC7579MultisigStorage`] to allow guardians to presign recovery operations for more flexible coordination

0 commit comments

Comments
 (0)