Skip to content

Commit 317e673

Browse files
ernestognwarr00gonzaotc
authored
Add ERC-7579 modules docs and EIP-7702 note in accounts docs (#157)
Co-authored-by: Arr00 <[email protected]> Co-authored-by: Gonzalo Othacehe <[email protected]>
1 parent 524341c commit 317e673

File tree

8 files changed

+287
-6
lines changed

8 files changed

+287
-6
lines changed

contracts/mocks/account/modules/ERC7579MultisigMocks.sol

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ abstract contract ERC7579MultisigExecutorMock is EIP712, ERC7579Executor, ERC757
2626
bytes32 mode,
2727
bytes calldata data
2828
) internal view override returns (bytes calldata) {
29-
uint16 executionCalldataLength = uint16(uint256(bytes32(data[0:2]))); // First 2 bytes are the length
29+
uint16 executionCalldataLength = uint16(bytes2(data[0:2])); // First 2 bytes are the length
3030
bytes calldata executionCalldata = data[2:2 + executionCalldataLength]; // Next bytes are the calldata
3131
bytes32 typeHash = _getExecuteTypeHash(account, salt, mode, executionCalldata);
3232
require(_rawERC7579Validation(account, typeHash, data[2 + executionCalldataLength:])); // Remaining bytes are the signature
@@ -39,7 +39,7 @@ abstract contract ERC7579MultisigExecutorMock is EIP712, ERC7579Executor, ERC757
3939
bytes32 mode,
4040
bytes calldata executionCalldata
4141
) internal view returns (bytes32) {
42-
return _hashTypedDataV4(keccak256(abi.encode(EXECUTE_OPERATION, account, mode, executionCalldata, salt)));
42+
return _hashTypedDataV4(keccak256(abi.encode(EXECUTE_OPERATION, account, salt, mode, executionCalldata)));
4343
}
4444
}
4545

@@ -58,7 +58,7 @@ abstract contract ERC7579MultisigWeightedExecutorMock is EIP712, ERC7579Executor
5858
bytes32 mode,
5959
bytes calldata data
6060
) internal view override returns (bytes calldata) {
61-
uint16 executionCalldataLength = uint16(uint256(bytes32(data[0:2]))); // First 2 bytes are the length
61+
uint16 executionCalldataLength = uint16(bytes2(data[0:2])); // First 2 bytes are the length
6262
bytes calldata executionCalldata = data[2:2 + executionCalldataLength]; // Next bytes are the calldata
6363
bytes32 typeHash = _getExecuteTypeHash(account, salt, mode, executionCalldata);
6464
require(_rawERC7579Validation(account, typeHash, data[2 + executionCalldataLength:])); // Remaining bytes are the signature
@@ -71,7 +71,7 @@ abstract contract ERC7579MultisigWeightedExecutorMock is EIP712, ERC7579Executor
7171
bytes32 mode,
7272
bytes calldata executionCalldata
7373
) internal view returns (bytes32) {
74-
return _hashTypedDataV4(keccak256(abi.encode(EXECUTE_OPERATION, account, mode, executionCalldata, salt)));
74+
return _hashTypedDataV4(keccak256(abi.encode(EXECUTE_OPERATION, account, salt, mode, executionCalldata)));
7575
}
7676
}
7777

@@ -90,7 +90,7 @@ abstract contract ERC7579MultisigConfirmationExecutorMock is ERC7579Executor, ER
9090
bytes32 mode,
9191
bytes calldata data
9292
) internal view override returns (bytes calldata) {
93-
uint16 executionCalldataLength = uint16(uint256(bytes32(data[0:2]))); // First 2 bytes are the length
93+
uint16 executionCalldataLength = uint16(bytes2(data[0:2])); // First 2 bytes are the length
9494
bytes calldata executionCalldata = data[2:2 + executionCalldataLength]; // Next bytes are the calldata
9595
bytes32 typeHash = _getExecuteTypeHash(account, salt, mode, executionCalldata);
9696
require(_rawERC7579Validation(account, typeHash, data[2 + executionCalldataLength:])); // Remaining bytes are the signature
@@ -103,6 +103,6 @@ abstract contract ERC7579MultisigConfirmationExecutorMock is ERC7579Executor, ER
103103
bytes32 mode,
104104
bytes calldata executionCalldata
105105
) internal view returns (bytes32) {
106-
return _hashTypedDataV4(keccak256(abi.encode(EXECUTE_OPERATION, account, mode, executionCalldata, salt)));
106+
return _hashTypedDataV4(keccak256(abi.encode(EXECUTE_OPERATION, account, salt, mode, executionCalldata)));
107107
}
108108
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// contracts/MyAccountERC7579.sol
2+
// SPDX-License-Identifier: MIT
3+
pragma solidity ^0.8.27;
4+
5+
import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
6+
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
7+
import {MODULE_TYPE_VALIDATOR} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
8+
import {AccountERC7579} from "../../../account/extensions/AccountERC7579.sol";
9+
10+
contract MyAccountERC7579 is Initializable, AccountERC7579 {
11+
function initializeAccount(address validator, bytes calldata validatorData) public initializer {
12+
// Install a validator module to handle signature verification
13+
_installModule(MODULE_TYPE_VALIDATOR, validator, validatorData);
14+
}
15+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// contracts/MyERC7579DelayedSocialRecovery.sol
2+
// SPDX-License-Identifier: MIT
3+
pragma solidity ^0.8.27;
4+
5+
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
6+
import {ERC7579Executor} from "../../../../account/modules/ERC7579Executor.sol";
7+
import {ERC7579Validator} from "../../../../account/modules/ERC7579Validator.sol";
8+
import {Calldata} from "@openzeppelin/contracts/utils/Calldata.sol";
9+
import {ERC7579DelayedExecutor} from "../../../../account/modules/ERC7579DelayedExecutor.sol";
10+
import {ERC7579Multisig} from "../../../../account/modules/ERC7579Multisig.sol";
11+
12+
abstract contract MyERC7579DelayedSocialRecovery is EIP712, ERC7579DelayedExecutor, ERC7579Multisig {
13+
bytes32 private constant RECOVER_TYPEHASH =
14+
keccak256("Recover(address account,bytes32 salt,bytes32 mode,bytes executionCalldata)");
15+
16+
function isModuleType(uint256 moduleTypeId) public pure override(ERC7579Executor, ERC7579Validator) returns (bool) {
17+
return ERC7579Executor.isModuleType(moduleTypeId) || ERC7579Executor.isModuleType(moduleTypeId);
18+
}
19+
20+
// Data encoding: [uint16(executorArgsLength), executorArgs, uint16(multisigArgsLength), multisigArgs]
21+
function onInstall(bytes calldata data) public override(ERC7579DelayedExecutor, ERC7579Multisig) {
22+
uint16 executorArgsLength = uint16(bytes2(data[0:2])); // First 2 bytes are the length
23+
bytes calldata executorArgs = data[2:2 + executorArgsLength]; // Next bytes are the args
24+
uint16 multisigArgsLength = uint16(bytes2(data[2 + executorArgsLength:4 + executorArgsLength])); // Next 2 bytes are the length
25+
bytes calldata multisigArgs = data[4 + executorArgsLength:4 + executorArgsLength + multisigArgsLength]; // Next bytes are the args
26+
27+
ERC7579DelayedExecutor.onInstall(executorArgs);
28+
ERC7579Multisig.onInstall(multisigArgs);
29+
}
30+
31+
function onUninstall(bytes calldata) public override(ERC7579DelayedExecutor, ERC7579Multisig) {
32+
ERC7579DelayedExecutor.onUninstall(Calldata.emptyBytes());
33+
ERC7579Multisig.onUninstall(Calldata.emptyBytes());
34+
}
35+
36+
// Data encoding: [uint16(executionCalldataLength), executionCalldata, signature]
37+
function _validateSchedule(
38+
address account,
39+
bytes32 salt,
40+
bytes32 mode,
41+
bytes calldata data
42+
) internal view override {
43+
uint16 executionCalldataLength = uint16(bytes2(data[0:2])); // First 2 bytes are the length
44+
bytes calldata executionCalldata = data[2:2 + executionCalldataLength]; // Next bytes are the calldata
45+
bytes calldata signature = data[2 + executionCalldataLength:]; // Remaining bytes are the signature
46+
require(_rawERC7579Validation(account, _getExecuteTypeHash(account, salt, mode, executionCalldata), signature));
47+
}
48+
49+
function _getExecuteTypeHash(
50+
address account,
51+
bytes32 salt,
52+
bytes32 mode,
53+
bytes calldata executionCalldata
54+
) internal view returns (bytes32) {
55+
return _hashTypedDataV4(keccak256(abi.encode(RECOVER_TYPEHASH, account, salt, mode, executionCalldata)));
56+
}
57+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// contracts/MyERC7579Modules.sol
2+
// SPDX-License-Identifier: MIT
3+
pragma solidity ^0.8.27;
4+
5+
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
6+
import {IERC7579Module, IERC7579Hook} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
7+
import {ERC7579Executor} from "../../../../account/modules/ERC7579Executor.sol";
8+
import {ERC7579Validator} from "../../../../account/modules/ERC7579Validator.sol";
9+
10+
// Basic validator module
11+
abstract contract MyERC7579RecoveryValidator is ERC7579Validator {}
12+
13+
// Basic executor module
14+
abstract contract MyERC7579RecoveryExecutor is ERC7579Executor {}
15+
16+
// Basic fallback handler
17+
abstract contract MyERC7579RecoveryFallback is IERC7579Module {}
18+
19+
// Basic hook
20+
abstract contract MyERC7579RecoveryHook is IERC7579Hook {}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// contracts/MyERC7579SocialRecovery.sol
2+
// SPDX-License-Identifier: MIT
3+
pragma solidity ^0.8.27;
4+
5+
import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";
6+
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
7+
import {ERC7579Executor} from "../../../../account/modules/ERC7579Executor.sol";
8+
import {ERC7579Validator} from "../../../../account/modules/ERC7579Validator.sol";
9+
import {ERC7579Multisig} from "../../../../account/modules/ERC7579Multisig.sol";
10+
11+
abstract contract MyERC7579SocialRecovery is EIP712, ERC7579Executor, ERC7579Multisig, Nonces {
12+
bytes32 private constant RECOVER_TYPEHASH =
13+
keccak256("Recover(address account,bytes32 salt,uint256 nonce,bytes32 mode,bytes executionCalldata)");
14+
15+
function isModuleType(uint256 moduleTypeId) public pure override(ERC7579Executor, ERC7579Validator) returns (bool) {
16+
return ERC7579Executor.isModuleType(moduleTypeId) || ERC7579Executor.isModuleType(moduleTypeId);
17+
}
18+
19+
// Data encoding: [uint16(executionCalldataLength), executionCalldata, signature]
20+
function _validateExecution(
21+
address account,
22+
bytes32 salt,
23+
bytes32 mode,
24+
bytes calldata data
25+
) internal override returns (bytes calldata) {
26+
uint16 executionCalldataLength = uint16(bytes2(data[0:2])); // First 2 bytes are the length
27+
bytes calldata executionCalldata = data[2:2 + executionCalldataLength]; // Next bytes are the calldata
28+
bytes calldata signature = data[2 + executionCalldataLength:]; // Remaining bytes are the signature
29+
require(_rawERC7579Validation(account, _getExecuteTypeHash(account, salt, mode, executionCalldata), signature));
30+
return executionCalldata;
31+
}
32+
33+
function _getExecuteTypeHash(
34+
address account,
35+
bytes32 salt,
36+
bytes32 mode,
37+
bytes calldata executionCalldata
38+
) internal returns (bytes32) {
39+
return
40+
_hashTypedDataV4(
41+
keccak256(abi.encode(RECOVER_TYPEHASH, account, salt, _useNonce(account), mode, executionCalldata))
42+
);
43+
}
44+
}

docs/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
** xref:accounts.adoc[Accounts]
44
*** xref:eoa-delegation.adoc[EOA Delegation]
55
*** xref:multisig.adoc[Multisig]
6+
*** xref:account-modules.adoc[Modules]
67
** xref:paymasters.adoc[Paymasters]
78
* xref:crosschain.adoc[Crosschain]
89
* xref:utilities.adoc[Utilities]
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
= Account Modules
2+
3+
Smart accounts built with https://eips.ethereum.org/EIPS/eip-7579[ERC-7579] provide a standardized way to extend account functionality through modules (i.e. smart contract instances). This architecture allows accounts to support various features that are compatible with a wide variety of account implementations. See https://erc7579.com/modules[compatible modules].
4+
5+
== ERC-7579
6+
7+
ERC-7579 defines a standardized interface for modular smart accounts. This standard enables accounts to install, uninstall, and interact with modules that extend their capabilities in a composable manner with different account implementations.
8+
9+
=== Accounts
10+
11+
OpenZeppelin offers an implementation of an xref:api:account.adoc#AccountERC7579[`AccountERC7579`] contract that allows installing modules compliant with this standard. There's also an xref:api:account.adoc#AccountERC7579Hooked[`AccountERC7579Hooked`] variant that supports installation of hooks. Like xref:accounts.adoc#handling_initialization[most accounts], an instance should define an initializer function where the first module that controls the account will be set:
12+
13+
[source,solidity]
14+
----
15+
include::api:example$account/MyAccountERC7579.sol[]
16+
----
17+
18+
NOTE: For simplicity, the xref:api:account.adoc#AccountERC7579Hooked[`AccountERC7579Hooked`] only supports a single hook. A common workaround is to install a https://github.com/rhinestonewtf/core-modules/blob/7afffccb44d73dbaca2481e7b92bce0621ea6449/src/HookMultiPlexer/HookMultiPlexer.sol[single hook with a multiplexer pattern] to extend the functionality to multiple hooks.
19+
20+
=== Modules
21+
22+
Functionality is added to accounts through encapsulated functionality deployed as smart contracts called _modules_. The standard defines four primary module types:
23+
24+
* *Validator modules (type 1)*: Handle signature verification and user operation validation
25+
* *Executor modules (type 2)*: Execute operations on behalf of the account
26+
* *Fallback modules (type 3)*: Handle fallback calls for specific function selectors
27+
* *Hook modules (type 4)*: Execute logic before and after operations
28+
29+
Modules can implement multiple types simultaneously, which means you could combine an executor module with hooks to enforce behaviors on an account, such as maintaining ERC-20 approvals or preventing the removal of certain permissions.
30+
31+
See https://erc7579.com/modules[popular module implementations].
32+
33+
==== Building Custom Modules
34+
35+
The library provides _standard composable modules_ as building blocks with an internal API for developers. By combining these components, you can create a rich set of variants without including unnecessary features.
36+
37+
A good starting point is the xref:api:account.adoc#ERC7579Executor[`ERC7579Executor`] or xref:api:account.adoc#ERC7579Validator[`ERC7579Validator`], which include an opinionated base layer easily combined with other abstract modules. Hooks and fallback handlers are more straightforward to implement directly from interfaces:
38+
39+
[source,solidity]
40+
----
41+
include::api:example$account/modules/MyERC7579Modules.sol[]
42+
----
43+
44+
TIP: Explore these abstract ERC-7579 modules in the xref:api:account.adoc#modules[API Reference].
45+
46+
==== Execution Modes
47+
48+
ERC-7579 supports various execution modes, which are encoded as a `bytes32` value. The https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/account/utils/draft-ERC7579Utils.sol[`ERC7579Utils`] library provides utility functions to work with these modes:
49+
50+
[source,solidity]
51+
----
52+
// Parts of an execution mode
53+
type Mode is bytes32;
54+
type CallType is bytes1;
55+
type ExecType is bytes1;
56+
type ModeSelector is bytes4;
57+
type ModePayload is bytes22;
58+
----
59+
60+
===== Call Types
61+
62+
Call types determine the kind of execution:
63+
64+
[%header,cols="1,1,3"]
65+
|===
66+
|Type |Value |Description
67+
|`CALLTYPE_SINGLE` |`0x00` |A single `call` execution
68+
|`CALLTYPE_BATCH` |`0x01` |A batch of `call` executions
69+
|`CALLTYPE_DELEGATECALL` |`0xFF` |A `delegatecall` execution
70+
|===
71+
72+
===== Execution Types
73+
74+
Execution types determine how failures are handled:
75+
76+
[%header,cols="1,1,3"]
77+
|===
78+
|Type |Value |Description
79+
|`EXECTYPE_DEFAULT` |`0x00` |Reverts on failure
80+
|`EXECTYPE_TRY` |`0x01` |Does not revert on failure, emits an event instead
81+
|===
82+
83+
==== Execution Data Format
84+
85+
The execution data format varies depending on the call type:
86+
87+
* For single calls: `abi.encodePacked(target, value, callData)`
88+
* For batched calls: `abi.encode(Execution[])` where `Execution` is a struct containing `target`, `value`, and `callData`
89+
* For delegate calls: `abi.encodePacked(target, callData)`
90+
91+
== Examples
92+
93+
=== Social Recovery
94+
95+
Social recovery allows an account to be recovered when access is lost by relying on trusted parties ("guardians") who verify the user's identity and help restore access.
96+
97+
Social recovery is not a single solution but a design space with multiple configuration options:
98+
99+
* Delay configuration
100+
* Expiration settings
101+
* Different guardian types
102+
* Cancellation windows
103+
* Confirmation requirements
104+
105+
To support _different guardian types_, we can leverage ERC-7913 as discussed in the xref:multisig.adoc#beyond_standard_signature_verification[multisig] section. For ERC-7579 modules, this is implemented through the xref:api:account.adoc#ERC7579Multisig[`ERC7579Multisig`] validator.
106+
107+
Combined with an xref:api:account.adoc#ERC7579Executor[`ERC7579Executor`], it provides a basic foundation that can be extended with more sophisticated features:
108+
109+
[source,solidity]
110+
----
111+
include::api:example$account/modules/MyERC7579SocialRecovery.sol[]
112+
----
113+
114+
For enhanced security, you can extend this foundation with scheduling, delays, and cancellations using xref:api:account.adoc#ERC7579DelayedExecutor[`ERC7579DelayedExecutor`]. This allows guardians to schedule recovery operations with a time delay, providing a security window to detect and cancel suspicious recovery attempts before they execute:
115+
116+
[source,solidity]
117+
----
118+
include::api:example$account/modules/MyERC7579DelayedSocialRecovery.sol[]
119+
----
120+
121+
NOTE: The delayed executor's signature validation doesn't require a nonce since operations are uniquely identified by their xref:api:account.adoc#ERC7579DelayedExecutor-hashOperation-address-bytes32-bytes32-bytes-[operation id] and cannot be scheduled twice.
122+
123+
These implementations demonstrate how to build progressively more secure social recovery mechanisms, from basic multi-signature recovery to time-delayed recovery with cancellation capabilities.
124+
125+
For additional functionality, developers can use:
126+
127+
* xref:api:account.adoc#ERC7579MultisigWeighted[`ERC7579MultisigWeighted`] to assign different weights to signers
128+
* xref:api:account.adoc#ERC7579MultisigConfirmation[`ERC7579MultisigConfirmation`] to implement a confirmation system that verifies signatures when adding signers

docs/modules/ROOT/pages/accounts.adoc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,3 +289,19 @@ A common security practice to prevent user operation https://mirror.xyz/curiousa
289289
The problem with this approach is that the user might be prompted by the wallet provider to sign an https://x.com/howydev/status/1780353754333634738[obfuscated message], which is a phishing vector that may lead to a user losing its assets.
290290

291291
To prevent this, developers may use xref:api:account#ERC7739Signer[`ERC7739Signer`], a utility that implements https://docs.openzeppelin.com/contracts/5.x/api/interfaces#IERC1271[`IERC1271`] for smart contract signatures with a defensive rehashing mechanism based on a https://github.com/frangio/eip712-wrapper-for-eip1271[nested EIP-712 approach] to wrap the signature request in a context where there's clearer information for the end user.
292+
293+
=== EIP-7702 Delegation
294+
295+
https://eips.ethereum.org/EIPS/eip-7702[EIP-7702] lets EOAs delegate to smart contracts while keeping their original signing key. This creates a hybrid account that works like an EOA for signing but has smart contract features. Protocols don't need major changes to support EIP-7702 since they already handle both EOAs and smart contracts (see https://docs.openzeppelin.com/contracts/5.x/api/utils#SignatureChecker[SignatureChecker]).
296+
297+
The signature verification stays compatible: delegated EOAs are treated as contracts using ERC-1271, making it easy to redelegate to a contract with ERC-1271 support with little overhead by reusing the validation mechanism of the account.
298+
299+
TIP: Learn more about delegating to an ERC-7702 account in our xref:eoa-delegation.adoc[EOA Delegation] section.
300+
301+
=== ERC-7579 Modules
302+
303+
Smart accounts have evolved to embrace modularity as a design principle, with popular implementations like https://erc7579.com/#supporters[Safe, Pimlico, Rhinestone, Etherspot and many others] agreeing on ERC-7579 as the standard for module interoperability. This standardization enables accounts to extend their functionality through external contracts while maintaining compatibility across different implementations.
304+
305+
OpenZeppelin Contracts provides both the building blocks for creating ERC-7579-compliant modules and an xref:api:account.adoc#AccountERC7579[`AccountERC7579`] implementation that supports installing and managing these modules.
306+
307+
TIP: Learn more in our xref:account-modules.adoc[account modules] section.

0 commit comments

Comments
 (0)