Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions contracts/mocks/account/modules/ERC7579MultisigMocks.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ abstract contract ERC7579MultisigExecutorMock is EIP712, ERC7579Executor, ERC757
bytes32 mode,
bytes calldata data
) internal view override returns (bytes calldata) {
uint16 executionCalldataLength = uint16(uint256(bytes32(data[0:2]))); // First 2 bytes are the length
uint16 executionCalldataLength = uint16(bytes2(data[0:2])); // First 2 bytes are the length
bytes calldata executionCalldata = data[2:2 + executionCalldataLength]; // Next bytes are the calldata
bytes32 typeHash = _getExecuteTypeHash(account, salt, mode, executionCalldata);
require(_rawERC7579Validation(account, typeHash, data[2 + executionCalldataLength:])); // Remaining bytes are the signature
Expand All @@ -39,7 +39,7 @@ abstract contract ERC7579MultisigExecutorMock is EIP712, ERC7579Executor, ERC757
bytes32 mode,
bytes calldata executionCalldata
) internal view returns (bytes32) {
return _hashTypedDataV4(keccak256(abi.encode(EXECUTE_OPERATION, account, mode, executionCalldata, salt)));
return _hashTypedDataV4(keccak256(abi.encode(EXECUTE_OPERATION, account, salt, mode, executionCalldata)));
}
}

Expand All @@ -58,7 +58,7 @@ abstract contract ERC7579MultisigWeightedExecutorMock is EIP712, ERC7579Executor
bytes32 mode,
bytes calldata data
) internal view override returns (bytes calldata) {
uint16 executionCalldataLength = uint16(uint256(bytes32(data[0:2]))); // First 2 bytes are the length
uint16 executionCalldataLength = uint16(bytes2(data[0:2])); // First 2 bytes are the length
bytes calldata executionCalldata = data[2:2 + executionCalldataLength]; // Next bytes are the calldata
bytes32 typeHash = _getExecuteTypeHash(account, salt, mode, executionCalldata);
require(_rawERC7579Validation(account, typeHash, data[2 + executionCalldataLength:])); // Remaining bytes are the signature
Expand All @@ -71,7 +71,7 @@ abstract contract ERC7579MultisigWeightedExecutorMock is EIP712, ERC7579Executor
bytes32 mode,
bytes calldata executionCalldata
) internal view returns (bytes32) {
return _hashTypedDataV4(keccak256(abi.encode(EXECUTE_OPERATION, account, mode, executionCalldata, salt)));
return _hashTypedDataV4(keccak256(abi.encode(EXECUTE_OPERATION, account, salt, mode, executionCalldata)));
}
}

Expand All @@ -90,7 +90,7 @@ abstract contract ERC7579MultisigConfirmationExecutorMock is ERC7579Executor, ER
bytes32 mode,
bytes calldata data
) internal view override returns (bytes calldata) {
uint16 executionCalldataLength = uint16(uint256(bytes32(data[0:2]))); // First 2 bytes are the length
uint16 executionCalldataLength = uint16(bytes2(data[0:2])); // First 2 bytes are the length
bytes calldata executionCalldata = data[2:2 + executionCalldataLength]; // Next bytes are the calldata
bytes32 typeHash = _getExecuteTypeHash(account, salt, mode, executionCalldata);
require(_rawERC7579Validation(account, typeHash, data[2 + executionCalldataLength:])); // Remaining bytes are the signature
Expand All @@ -103,6 +103,6 @@ abstract contract ERC7579MultisigConfirmationExecutorMock is ERC7579Executor, ER
bytes32 mode,
bytes calldata executionCalldata
) internal view returns (bytes32) {
return _hashTypedDataV4(keccak256(abi.encode(EXECUTE_OPERATION, account, mode, executionCalldata, salt)));
return _hashTypedDataV4(keccak256(abi.encode(EXECUTE_OPERATION, account, salt, mode, executionCalldata)));
}
}
15 changes: 15 additions & 0 deletions contracts/mocks/docs/account/MyAccountERC7579.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// contracts/MyAccountERC7579.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {MODULE_TYPE_VALIDATOR} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
import {AccountERC7579} from "../../../account/extensions/AccountERC7579.sol";

contract MyAccountERC7579 is Initializable, AccountERC7579 {
function initializeAccount(address validator, bytes calldata validatorData) public initializer {
// Install a validator module to handle signature verification
_installModule(MODULE_TYPE_VALIDATOR, validator, validatorData);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// contracts/MyERC7579DelayedSocialRecovery.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ERC7579Executor} from "../../../../account/modules/ERC7579Executor.sol";
import {ERC7579Validator} from "../../../../account/modules/ERC7579Validator.sol";
import {Calldata} from "@openzeppelin/contracts/utils/Calldata.sol";
import {ERC7579DelayedExecutor} from "../../../../account/modules/ERC7579DelayedExecutor.sol";
import {ERC7579Multisig} from "../../../../account/modules/ERC7579Multisig.sol";

abstract contract MyERC7579DelayedSocialRecovery is EIP712, ERC7579DelayedExecutor, ERC7579Multisig {
bytes32 private constant RECOVER_TYPEHASH =
keccak256("Recover(address account,bytes32 salt,bytes32 mode,bytes executionCalldata)");

function isModuleType(uint256 moduleTypeId) public pure override(ERC7579Executor, ERC7579Validator) returns (bool) {
return ERC7579Executor.isModuleType(moduleTypeId) || ERC7579Executor.isModuleType(moduleTypeId);
}

// Data encoding: [uint16(executorArgsLength), executorArgs, uint16(multisigArgsLength), multisigArgs]
function onInstall(bytes calldata data) public override(ERC7579DelayedExecutor, ERC7579Multisig) {
uint16 executorArgsLength = uint16(bytes2(data[0:2])); // First 2 bytes are the length
bytes calldata executorArgs = data[2:2 + executorArgsLength]; // Next bytes are the args
uint16 multisigArgsLength = uint16(bytes2(data[2 + executorArgsLength:4 + executorArgsLength])); // Next 2 bytes are the length
bytes calldata multisigArgs = data[4 + executorArgsLength:4 + executorArgsLength + multisigArgsLength]; // Next bytes are the args

ERC7579DelayedExecutor.onInstall(executorArgs);
ERC7579Multisig.onInstall(multisigArgs);
}

function onUninstall(bytes calldata) public override(ERC7579DelayedExecutor, ERC7579Multisig) {
ERC7579DelayedExecutor.onUninstall(Calldata.emptyBytes());
ERC7579Multisig.onUninstall(Calldata.emptyBytes());
}

// Data encoding: [uint16(executionCalldataLength), executionCalldata, signature]
function _validateSchedule(
address account,
bytes32 salt,
bytes32 mode,
bytes calldata data
) internal view override {
uint16 executionCalldataLength = uint16(bytes2(data[0:2])); // First 2 bytes are the length
bytes calldata executionCalldata = data[2:2 + executionCalldataLength]; // Next bytes are the calldata
bytes calldata signature = data[2 + executionCalldataLength:]; // Remaining bytes are the signature
require(_rawERC7579Validation(account, _getExecuteTypeHash(account, salt, mode, executionCalldata), signature));
}

function _getExecuteTypeHash(
address account,
bytes32 salt,
bytes32 mode,
bytes calldata executionCalldata
) internal view returns (bytes32) {
return _hashTypedDataV4(keccak256(abi.encode(RECOVER_TYPEHASH, account, salt, mode, executionCalldata)));
}
}
20 changes: 20 additions & 0 deletions contracts/mocks/docs/account/modules/MyERC7579Modules.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// contracts/MyERC7579Modules.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {IERC7579Module, IERC7579Hook} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
import {ERC7579Executor} from "../../../../account/modules/ERC7579Executor.sol";
import {ERC7579Validator} from "../../../../account/modules/ERC7579Validator.sol";

// Basic validator module
abstract contract MyERC7579RecoveryValidator is ERC7579Validator {}

// Basic executor module
abstract contract MyERC7579RecoveryExecutor is ERC7579Executor {}

// Basic fallback handler
abstract contract MyERC7579RecoveryFallback is IERC7579Module {}

// Basic hook
abstract contract MyERC7579RecoveryHook is IERC7579Hook {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// contracts/MyERC7579SocialRecovery.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ERC7579Executor} from "../../../../account/modules/ERC7579Executor.sol";
import {ERC7579Validator} from "../../../../account/modules/ERC7579Validator.sol";
import {ERC7579Multisig} from "../../../../account/modules/ERC7579Multisig.sol";

abstract contract MyERC7579SocialRecovery is EIP712, ERC7579Executor, ERC7579Multisig, Nonces {
bytes32 private constant RECOVER_TYPEHASH =
keccak256("Recover(address account,bytes32 salt,uint256 nonce,bytes32 mode,bytes executionCalldata)");

function isModuleType(uint256 moduleTypeId) public pure override(ERC7579Executor, ERC7579Validator) returns (bool) {
return ERC7579Executor.isModuleType(moduleTypeId) || ERC7579Executor.isModuleType(moduleTypeId);
}

// Data encoding: [uint16(executionCalldataLength), executionCalldata, signature]
function _validateExecution(
address account,
bytes32 salt,
bytes32 mode,
bytes calldata data
) internal override returns (bytes calldata) {
uint16 executionCalldataLength = uint16(bytes2(data[0:2])); // First 2 bytes are the length
bytes calldata executionCalldata = data[2:2 + executionCalldataLength]; // Next bytes are the calldata
bytes calldata signature = data[2 + executionCalldataLength:]; // Remaining bytes are the signature
require(_rawERC7579Validation(account, _getExecuteTypeHash(account, salt, mode, executionCalldata), signature));
return executionCalldata;
}

function _getExecuteTypeHash(
address account,
bytes32 salt,
bytes32 mode,
bytes calldata executionCalldata
) internal returns (bytes32) {
return
_hashTypedDataV4(
keccak256(abi.encode(RECOVER_TYPEHASH, account, salt, _useNonce(account), mode, executionCalldata))
);
}
}
1 change: 1 addition & 0 deletions docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
** xref:accounts.adoc[Accounts]
*** xref:eoa-delegation.adoc[EOA Delegation]
*** xref:multisig.adoc[Multisig]
*** xref:account-modules.adoc[Modules]
** xref:paymasters.adoc[Paymasters]
* xref:crosschain.adoc[Crosschain]
* xref:utilities.adoc[Utilities]
128 changes: 128 additions & 0 deletions docs/modules/ROOT/pages/account-modules.adoc
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure where and exactly how, but I would like to briefly mention the benefits of implicitly using ERC-7913 building blocks on the Social Recovery example, since I think, and this is very opinionated, that it is a valuable feature that makes a compelling reason enough to choose this over other implementations, on top of the great composability and extensibility provided. Wdyt? @ernestognw

Copy link
Member Author

@ernestognw ernestognw May 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think standards should be sold. I mean that a good standard solves an issue cleanly, and I think this is the case. The recommendation should be "just use ERC-7913" because it's simply the best way to approach support for arbitrary authorization types.

Instead, maybe users would benefit from understanding how to bring ERC-7913 to their use cases. They should be guided about when to use each variant (SingerERC7913, MultisignerERC7913, ERC7579Multisig, ERC7913Utils, ERC7913RSAVerifier, ERC7913ZKVerifier, ERC7913P256Verifier and beyond)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I get what you say. Is it that the reasons of why using an ERC7579Multisig aren't explained? That'd be true.

I just updated with a bit more context in 8980e0c

Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
= Account Modules

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].

== ERC-7579

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.

=== Accounts

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:

[source,solidity]
----
include::api:example$account/MyAccountERC7579.sol[]
----

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.

=== Modules

Functionality is added to accounts through encapsulated functionality deployed as smart contracts called _modules_. The standard defines four primary module types:

* *Validator modules (type 1)*: Handle signature verification and user operation validation
* *Executor modules (type 2)*: Execute operations on behalf of the account
* *Fallback modules (type 3)*: Handle fallback calls for specific function selectors
* *Hook modules (type 4)*: Execute logic before and after operations

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.

See https://erc7579.com/modules[popular module implementations].

==== Building Custom Modules

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.

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:

[source,solidity]
----
include::api:example$account/modules/MyERC7579Modules.sol[]
----

TIP: Explore these abstract ERC-7579 modules in the xref:api:account.adoc#modules[API Reference].

==== Execution Modes

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:

[source,solidity]
----
// Parts of an execution mode
type Mode is bytes32;
type CallType is bytes1;
type ExecType is bytes1;
type ModeSelector is bytes4;
type ModePayload is bytes22;
----

===== Call Types

Call types determine the kind of execution:

[%header,cols="1,1,3"]
|===
|Type |Value |Description
|`CALLTYPE_SINGLE` |`0x00` |A single `call` execution
|`CALLTYPE_BATCH` |`0x01` |A batch of `call` executions
|`CALLTYPE_DELEGATECALL` |`0xFF` |A `delegatecall` execution
|===

===== Execution Types

Execution types determine how failures are handled:

[%header,cols="1,1,3"]
|===
|Type |Value |Description
|`EXECTYPE_DEFAULT` |`0x00` |Reverts on failure
|`EXECTYPE_TRY` |`0x01` |Does not revert on failure, emits an event instead
|===

==== Execution Data Format

The execution data format varies depending on the call type:

* For single calls: `abi.encodePacked(target, value, callData)`
* For batched calls: `abi.encode(Execution[])` where `Execution` is a struct containing `target`, `value`, and `callData`
* For delegate calls: `abi.encodePacked(target, callData)`

== Examples

=== Social Recovery

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.

Social recovery is not a single solution but a design space with multiple configuration options:

* Delay configuration
* Expiration settings
* Different guardian types
* Cancellation windows
* Confirmation requirements

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.

Combined with an xref:api:account.adoc#ERC7579Executor[`ERC7579Executor`], it provides a basic foundation that can be extended with more sophisticated features:

[source,solidity]
----
include::api:example$account/modules/MyERC7579SocialRecovery.sol[]
----

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:

[source,solidity]
----
include::api:example$account/modules/MyERC7579DelayedSocialRecovery.sol[]
----

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.

These implementations demonstrate how to build progressively more secure social recovery mechanisms, from basic multi-signature recovery to time-delayed recovery with cancellation capabilities.

For additional functionality, developers can use:

* xref:api:account.adoc#ERC7579MultisigWeighted[`ERC7579MultisigWeighted`] to assign different weights to signers
* xref:api:account.adoc#ERC7579MultisigConfirmation[`ERC7579MultisigConfirmation`] to implement a confirmation system that verifies signatures when adding signers
16 changes: 16 additions & 0 deletions docs/modules/ROOT/pages/accounts.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,19 @@ A common security practice to prevent user operation https://mirror.xyz/curiousa
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.

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.

=== EIP-7702 Delegation

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]).

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.

TIP: Learn more about delegating to an ERC-7702 account in our xref:eoa-delegation.adoc[EOA Delegation] section.

=== ERC-7579 Modules

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.

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.

TIP: Learn more in our xref:account-modules.adoc[account modules] section.
Loading