Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
22 changes: 11 additions & 11 deletions contracts/account/modules/ERC7579DelayedExecutor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ abstract contract ERC7579DelayedExecutor is ERC7579Executor {
bytes32 /* salt */,
bytes32 /* mode */,
bytes calldata data
) internal view virtual override returns (bool valid, bytes calldata executionCalldata) {
) internal virtual override returns (bool valid, bytes calldata executionCalldata) {
return (true, data); // Anyone can execute, the state validation of the operation is enough
}

Expand All @@ -287,12 +287,12 @@ abstract contract ERC7579DelayedExecutor is ERC7579Executor {
* ```solidity
* function _validateCancel(
* address account,
* bytes32 salt,
* bytes32 mode,
* bytes calldata data,
* bytes32 salt
* ) internal view override returns (bool) {
* bytes calldata data
* ) internal override returns (bool) {
* bool isAuthorized = ...; // custom logic to check authorization
* return isAuthorized || super._validateCancel(account, mode, data, salt);
* return isAuthorized || super._validateCancel(account, salt, mode, data);
* }
*```
*/
Expand All @@ -301,7 +301,7 @@ abstract contract ERC7579DelayedExecutor is ERC7579Executor {
bytes32 /* salt */,
bytes32 /* mode */,
bytes calldata /* data */
) internal view virtual returns (bool) {
) internal virtual returns (bool) {
return account == msg.sender;
}

Expand All @@ -315,12 +315,12 @@ abstract contract ERC7579DelayedExecutor is ERC7579Executor {
* ```solidity
* function _validateSchedule(
* address account,
* bytes32 salt,
* bytes32 mode,
* bytes calldata data,
* bytes32 salt
* ) internal view override returns (bool) {
* bytes calldata data
* ) internal override returns (bool) {
* bool isAuthorized = ...; // custom logic to check authorization
* return isAuthorized || super._validateSchedule(account, mode, data, salt);
* return isAuthorized || super._validateSchedule(account, salt, mode, data);
* }
*```
*/
Expand All @@ -329,7 +329,7 @@ abstract contract ERC7579DelayedExecutor is ERC7579Executor {
bytes32 /* salt */,
bytes32 /* mode */,
bytes calldata /* data */
) internal view virtual returns (bool) {
) internal virtual returns (bool) {
return account == msg.sender;
}

Expand Down
4 changes: 2 additions & 2 deletions contracts/account/modules/ERC7579Executor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ abstract contract ERC7579Executor is IERC7579Module {
* bytes32 salt,
* bytes32 mode,
* bytes calldata data
* ) internal view override returns (bool valid, bytes calldata executionCalldata) {
* ) internal override returns (bool valid, bytes calldata executionCalldata) {
* /// ...
* return isAuthorized; // custom logic to check authorization
* }
Expand All @@ -73,7 +73,7 @@ abstract contract ERC7579Executor is IERC7579Module {
bytes32 salt,
bytes32 mode,
bytes calldata data
) internal view virtual returns (bool valid, bytes calldata executionCalldata);
) internal virtual returns (bool valid, bytes calldata executionCalldata);

/**
* @dev Internal version of {execute}. Emits {ERC7579ExecutorOperationExecuted} event.
Expand Down
14 changes: 14 additions & 0 deletions contracts/mocks/docs/account/MyAccountERC7579.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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 MyAccount is Initializable, AccountERC7579 {
function initializeAccount(address validator, bytes calldata validatorData) public initializer {
_installModule(MODULE_TYPE_VALIDATOR, validator, validatorData);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// contracts/MyERC7579DelayedSocialRecovery.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.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)");

// Data encoding: [uint16(executorArgsLength), executorArgs, uint16(multisigArgsLength), multisigArgs]
function onInstall(bytes calldata data) public override(ERC7579DelayedExecutor, ERC7579Multisig) {
uint16 executorArgsLength = uint16(uint256(bytes32(data[0:2]))); // First 2 bytes are the length
bytes calldata executorArgs = data[2:2 + executorArgsLength]; // Next bytes are the args
uint16 multisigArgsLength = uint16(uint256(bytes32(data[2 + executorArgsLength:]))); // Next 2 bytes are the length
bytes calldata multisigArgs = data[2 + executorArgsLength + 2:2 + executorArgsLength + 2 + 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 override returns (bool) {
uint16 executionCalldataLength = uint16(uint256(bytes32(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
return
_validateMultisignature(account, _getExecuteTypeHash(account, salt, mode, executionCalldata), signature) ||
super._validateSchedule(account, salt, mode, data);
}

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)));
}
}
16 changes: 16 additions & 0 deletions contracts/mocks/docs/account/modules/MyERC7579Modules.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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";

abstract contract MyERC7579RecoveryValidator is ERC7579Validator {}

abstract contract MyERC7579RecoveryExecutor is ERC7579Executor {}

abstract contract MyERC7579RecoveryFallback is IERC7579Module {}

abstract contract MyERC7579RecoveryHook is IERC7579Hook {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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 {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)");

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

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]
115 changes: 115 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,115 @@
= Account Modules

Smart accounts built with https://eips.ethereum.org/EIPS/eip-7579[ERC-7579] support provide a standardized way to extend account functionality through modules. This architecture allows accounts to support various features that are compatible with a wide variety of account implementations. See https://erc7579.com/accounts[compatible accounts].

== 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 as long as they are compatible with ERC-7579 modules.

=== Accounts

OpenZeppelin offers an implementation of an xref:api:accounts.adoc#AccountERC7579[`AccountERC7579`] contract that allows installing modules compliant with this standard. Also, there's an xref:api:accounts.adoc#AccountERC7579Hooked[`AccountERC7579Hooked`] variant that allows installing a hook. 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:accounts.adoc#AccountERC7579Hooked[`AccountERC7579Hooked`] only supports a single hook. However, 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 at a smart contract called _module_. 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 are not mutually exclusive, which means that you could, for example, combine an executor module with hooks, so that it shares both types. These could be useful if developers want to enforce behaviors on an account, like keeping ERC20 approvals or avoid removing certain permissions.

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

==== Setting up a module

The library provides with _standard composable modules_ to build your own. These are thought as building blocks that expose an internal API for developers to build functionalities. Composing them would produce a rich set of variants for developers without forcing unnecessary features they don't want to.

To get started, you may like to start off from xref:api:account.adoc#ERC7579Executor[ERC7579Executor] or xref:api:account.adoc#ERC7579Validator[ERC7579Validator], which include an opinionated base layer which can easily be combined with other abstract modules. In the case of hooks or fallback handlers, they're more straight forward to implement out 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 using specific bit patterns. The `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;
----

TIP: Use the https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/account/utils/draft-ERC7579Utils.sol[ERC7579Utils] library to encode and decode ERC-7579 modes and their components.

===== 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 is a method to regain access to an account by relying on trusted parties or "guardians" who verify the user's identity and help restore access, typically without needing a centralized authority.

Although commonly cited as a single solution, social recovery is a whole design space. It includes multiple nuanced design decisions like _delay configuration_, _expiration_, _support of different guardian types_, _cancellation windows_, and even _confirmations_ to make sure guardians are capable to execute a recovery procedure. These functionalities are difficult to agreed upon in a standard way given the tradeoffs between configurations (e.g. should guardians have different weights?).

In practice, a simple approach to social recovery is to coordinate multiple signatures from the guardians who can execute a recovery. Fortunately, the library includes an xref:api.accounts.adoc#ERC7579Executor[`ERC7579Executor`] that you can configure by implementing the xref:api.accounts.adoc#ERC7579Executor-_validateExecution[`_validateExecution`] function. In combination with xref:api.accounts.adoc#ERC7579Multisig[ERC7579Multisig], it's possible to create a simple _social_ executor that supports generic keys following https://eips.ethereum.org/EIPS/eip-7913[ERC-7913]:

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

This approach, although lightweight, this approach lacks of more configuration options like delays or expirations. For these purposes, developers can use the xref:api.accounts.adoc#ERC7579DelayedExecutor[`ERC7579DelayedExecutor`] contract instead, that includes support for scheduling execution requests with an execution delay and cancellations:

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

Developers can also use xref:api.accounts.adoc#ERC7579MultisigWeighted[ERC7579MultisigWeighted] to assign flexible weights to each signers. Also, they could implement a confirmation system that verifies a signature when signers are added to an executor using xref:api.accounts.adoc#ERC7579MultisigConfirmation[ERC7579MultisigConfirmation]