-
Notifications
You must be signed in to change notification settings - Fork 24
Add ERC-7579 modules docs and EIP-7702 note in accounts docs #157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
876e140
a9b6b25
f5d4e77
8c31ad9
da7ed55
7519741
315e8ff
ed8cf29
936fc52
4021923
9f815d9
2940a4f
1b29bf6
f394bf3
6de4389
1d536a4
ab77541
8980e0c
acd14be
53c0913
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 MyAccount 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(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 | ||
arr00 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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(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 | ||
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))); | ||
} | ||
} |
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(uint256(bytes32(data[0:2]))); // First 2 bytes are the length | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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)) | ||
); | ||
} | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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,126 @@ | ||
= 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/accounts[compatible modules]. | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
== 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: | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
[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 | ||
|
||
A simple first approach combines multiple signatures from guardians with execution capabilities. This 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: | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
[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.account#ERC7579DelayedExecutor-hashOperation-address-bytes32-bytes32-bytes-[operation id] and cannot be scheduled twice. | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* xref:api:account.adoc#ERC7579MultisigConfirmation[ERC7579MultisigConfirmation] to implement a confirmation system that verifies signatures when adding signers | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
Uh oh!
There was an error while loading. Please reload this page.