diff --git a/contracts/mocks/account/modules/ERC7579MultisigMocks.sol b/contracts/mocks/account/modules/ERC7579MultisigMocks.sol index 57ac0a32..822d26b7 100644 --- a/contracts/mocks/account/modules/ERC7579MultisigMocks.sol +++ b/contracts/mocks/account/modules/ERC7579MultisigMocks.sol @@ -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 @@ -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))); } } @@ -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 @@ -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))); } } @@ -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 @@ -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))); } } diff --git a/contracts/mocks/docs/account/MyAccountERC7579.sol b/contracts/mocks/docs/account/MyAccountERC7579.sol new file mode 100644 index 00000000..a38adce4 --- /dev/null +++ b/contracts/mocks/docs/account/MyAccountERC7579.sol @@ -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); + } +} diff --git a/contracts/mocks/docs/account/modules/MyERC7579DelayedSocialRecovery.sol b/contracts/mocks/docs/account/modules/MyERC7579DelayedSocialRecovery.sol new file mode 100644 index 00000000..f3deae5f --- /dev/null +++ b/contracts/mocks/docs/account/modules/MyERC7579DelayedSocialRecovery.sol @@ -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))); + } +} diff --git a/contracts/mocks/docs/account/modules/MyERC7579Modules.sol b/contracts/mocks/docs/account/modules/MyERC7579Modules.sol new file mode 100644 index 00000000..47f44585 --- /dev/null +++ b/contracts/mocks/docs/account/modules/MyERC7579Modules.sol @@ -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 {} diff --git a/contracts/mocks/docs/account/modules/MyERC7579SocialRecovery.sol b/contracts/mocks/docs/account/modules/MyERC7579SocialRecovery.sol new file mode 100644 index 00000000..c0d7e4c0 --- /dev/null +++ b/contracts/mocks/docs/account/modules/MyERC7579SocialRecovery.sol @@ -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)) + ); + } +} diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index b9fb73c3..40037301 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -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] diff --git a/docs/modules/ROOT/pages/account-modules.adoc b/docs/modules/ROOT/pages/account-modules.adoc new file mode 100644 index 00000000..fd3590e6 --- /dev/null +++ b/docs/modules/ROOT/pages/account-modules.adoc @@ -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 diff --git a/docs/modules/ROOT/pages/accounts.adoc b/docs/modules/ROOT/pages/accounts.adoc index a7489450..f1fbc948 100644 --- a/docs/modules/ROOT/pages/accounts.adoc +++ b/docs/modules/ROOT/pages/accounts.adoc @@ -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.