ERC-7579: Minimal Modular Smart Accounts Ethereum Improvement Proposals AllCoreNetworkingInterfaceERCMetaInformational ⚠️ Draft Standards Track: ERC ERC-7579: Minimal Modular Smart Accounts Modular smart account interfaces and behavior for interoperability with minimal restrictions for accounts and modules Authors zeroknots (@zeroknots), Konrad Kopp (@kopy-kat), Taek Lee (@leekt), Fil Makarov (@filmakarov), Elim Poon (@yaonam), Lyu Min (@rockmin216) Created 2023-12-14 Discussion Link https://ethereum-magicians.org/t/erc-7579-minimal-modular-smart-accounts/17336 Requires EIP-165, EIP-1271, EIP-2771, EIP-4337 Table of Contents Abstract Motivation Specification Definitions Account Modules Rationale Minimal approach Extensions Specifications Backwards Compatibility Already deployed smart accounts Reference Implementation Security Considerations Copyright Abstract This proposal outlines the minimally required interfaces and behavior for modular smart accounts and modules to ensure interoperability across implementations. For accounts, the standard specifies execution, config and fallback interfaces as well as compliance to ERC-165 and ERC-1271. For modules, the standard specifies a core interface, module types and type-specific interfaces. Motivation Contract accounts are gaining adoption with many accounts being built using a modular architecture. These modular contract accounts (hereafter smart accounts) move functionality into external contracts (modules) in order to increase the speed and potential of innovation, to future-proof themselves and to allow customizability by developers and users. However, currently these smart accounts are built in vastly different ways, creating module fragmentation and vendor lock-in. There are several reasons for why standardizing smart accounts is very beneficial to the ecosystem, including: Interoperability for modules to be used across different smart accounts Interoperability for smart accounts to be used across different wallet applications and sdks Preventing significant vendor lock-in for smart account users However, it is highly important that this standardization is done with minimal impact on the implementation logic of the accounts, so that smart account vendors can continue to innovate, while also allowing a flourishing, multi-account-compatible module ecosystem. As a result, the goal of this standard is to define the smart account and module interfaces and behavior that is as minimal as possible while ensuring interoperability between accounts and modules. Specification The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174. Definitions Smart account - An ERC-4337 compliant smart contract account that has a modular architecture. Module - A smart contract with self-contained smart account functionality. Validator: A module used during the ERC-4337 validation flow to determine if a UserOperation is valid. Executor: A module that can execute transactions on behalf of the smart account via a callback. Fallback Handler: A module that can extend the fallback functionality of a smart account. Entrypoint - A trusted singleton contract according to ERC-4337 specifications. Account Validation This standard does not dictate how validator selection is implemented. However, should a smart account encode validator selection mechanisms in ERC-4337 UserOperation fields (i.e. userOp.signature), the smart account MUST sanitize the affected values before invoking the validator. The smart account’s validateUserOp function SHOULD return the return value of the validator. Execution Behavior To comply with this standard, smart accounts MUST implement the two interfaces below. If an account implementation elects to not support any of the execution methods, it MUST implement the function, but revert instead of complying with the specifications below. This is in order to avoid unpredictable behavior with fallbacks. interface IExecution { /** * @dev packing batched transactions in a struct saves gas on both ERC-4337 and Executor Module flows */ struct Execution { address target; uint256 value; bytes callData; } /** * MUST execute a `call` to the target with the provided data and value * MUST allow ERC-4337 Entrypoint to be the sender and MAY allow `msg.sender == address(this)` * MUST revert if the call was not successful */ function execute(address target, uint256 value, bytes callData) external returns (bytes memory result); /** * MUST execute a `call` to all the targets with the provided data and value for each target * MUST allow ERC-4337 Entrypoint to be the sender and MAY allow `msg.sender == address(this)` * MUST revert if any call was not successful */ function executeBatch(Execution[] calldata executions) external returns (bytes memory result); /** * MUST execute a `call` to the target with the provided data and value * MUST only allow installed executors to call this function * MUST revert if the call was not successful */ function executeFromExecutor(address target, uint256 value, bytes callData) external returns (bytes memory result); /** * MUST execute a `call` to all the targets with the provided data and value for each target * MUST only allow installed executors to call this function * MUST revert if any call was not successful */ function executeBatchFromExecutor(Execution[] calldata executions) external returns (bytes memory result); } /** * @dev implementing delegatecall execution on a smart account must be considered carefully and is not recommended in most cases */ interface IExecutionUnsafe { /** * MUST execute a `delegatecall` to the target with the provided data * MUST allow ERC-4337 Entrypoint to be the sender and MAY allow `msg.sender == address(this)` * MUST revert if the call was not successful */ function executeDelegateCall(address target, bytes data) external returns (bytes memory result); /** * MUST execute a `delegatecall` to the target with the provided data and value * MUST only allow installed executors to call this function * MUST revert if the call was not successful */ function executeDelegateCallFromExecutor(address target, bytes data) external returns (bytes memory result); } Account configurations To comply with this standard, smart accounts MUST implement the entire interface below. If an account implementation elects to not support any of the configuration methods, it MUST revert, in order to avoid unpredictable behavior with fallbacks. When installing or uninstalling a module on a smart account, the smart account: MUST enforce authorization control on the relevant install or uninstall function for the module type MUST call the relevant onInstall or onUninstall function on the module MUST pass the sanitized initialisation data to the module MUST emit the relevant event for the module type When storing an installed module, the smart account MUST ensure that there is a way to differentiate between module types. For example, the smart account should be able to implement access control that only allows installed executors, but not other installed modules, to call the executeFromExecutor function. interface IAccountConfig { // VALIDATORS // Functions function installValidator(address validator, bytes calldata data) external; function uninstallValidator(address validator, bytes calldata data) external; function isValidatorInstalled(address validator) external view returns (bool); // Events event InstallValidator(address validator); event UninstallValidator(address validator); // EXECUTORS // Functions function installExecutor(address executor, bytes calldata data) external; function uninstallExecutor(address executor, bytes calldata data) external; function isExecutorInstalled(address executor) external view returns (bool); // Events event InstallExecutor(address executor); event UninstallExecutor(address executor); // FALLBACK HANDLERS // Functions function installFallback(address fallbackHandler, bytes calldata data) external; function uninstallFallback(address fallbackHandler, bytes calldata data) external; function isFallbackInstalled(address fallbackHandler) external view returns (bool); // Events event InstallFallbackHandler(address fallbackHandler); event UninstallFallbackHandler(address fallbackHandler); } Hooks Hooks are an OPTIONAL extension of this standard. Smart accounts MAY use hooks to execute custom logic and checks before and/or after the smart accounts performs a single or batched execution. To comply with this OPTIONAL extension, a smart account MUST implement the entire interface below and it: MUST enforce authorization control on the relevant install or uninstall function for hooks MUST call the onInstall or onUninstall function on the module when installing or uninstalling a hook MUST pass the sanitized initialisation data to the module when installing or uninstalling a hook MUST emit the relevant event for the hook MUST call the preCheck function on a single and batched execution and on every install function MAY call the preCheck function on uninstall functions MUST call the postCheck function after a single or batched execution as well as every install function MAY call the postCheck function on uninstall functions interface IAccountConfig_Hook { // HOOKS // Functions function installHook(address hook, bytes calldata data) external; function uninstallHook(address hook, bytes calldata data) external; function isHookInstalled(address hook) external view returns (bool); // Events event InstallHook(address hook); event UninstallHook(address hook); } ERC-1271 Forwarding The smart account MUST implement the ERC-1271 interface. The isValidSignature function calls MAY be forwarded to a validator. If ERC-1271 forwarding is implemented, the validator MUST be called with isValidSignatureWithSender(address sender, bytes32 hash, bytes signature), where the sender is the msg.sender of the call to the smart account. Should the smart account implement any validator selection encoding in the bytes signature parameter, the smart account MUST sanitize the parameter, before forwarding it to the validator. The smart account’s ERC-1271 isValidSignature function SHOULD return the return value of the validator that the request was forwarded to. Fallback Smart accounts MAY implement a fallback function that forwards the call to a fallback handler. If the smart account has a fallback handler installed, it: MUST implement authorization control MUST use call to invoke the fallback handler MUST utilize ERC-2771 to add the original msg.sender to the calldata sent to the fallback handler ERC-165 Smart accounts MUST implement ERC-165. However, for every interface function that reverts instead of implementing the functionality, the smart account MUST return false for the corresponding interface id. Modules This standard separates modules into the following different types that each has a unique and incremental identifier, which SHOULD be used by accounts, modules and other entities to identify the module type: Validation (type id: 1) Execution (type id: 2) Fallback (type id: 3) Hooks (type id: 4) Note: A single module can be of multiple types. Modules MUST implement the following interface, which is used by smart accounts to install and uninstall modules: interface IModule { function onInstall(bytes calldata data) external; function onUninstall(bytes calldata data) external; function isModuleType(uint256 typeID) external view returns(bool); } Modules MUST revert if onInstall or onUninstall was unsuccessful. Validators Validators MUST implement the IModule and the IValidator interface and have module type id: 1. interface IValidator { /** * MUST validate that the signature is a valid signature of the userOpHash, and SHOULD return ERC-4337's SIG_VALIDATION_FAILED (and not revert) on signature mismatch */ function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash) external returns (uint256); /** * @dev `sender` is the address that sent the ERC-1271 request to the smart account * * MUST return the ERC-1271 `MAGIC_VALUE` if the signature is valid * MUST NOT modify state */ function isValidSignatureWithSender(address sender, bytes32 hash, bytes calldata signature) external view returns (bytes4); } Executors Executors MUST implement the IModule interface and have module type id: 2. Fallback Handlers Fallback handlers MUST implement the IModule interface and have module type id: 3. Fallback handlers that implement authorization control, MUST NOT rely on msg.sender for authorization control but MUST use ERC-2771 _msgSender() instead. Hooks Hooks MUST implement the IModule and the IHook interface and have module type id: 4. interface IHook { /** * MAY return arbitrary data in the `hookData` return value */ function preCheck(address msgSender, bytes calldata msgData) external returns (bytes memory hookData); /** * MAY validate the `hookData` to validate transaction context of the `preCheck` function */ function postCheck(bytes calldata hookData) external returns (bool success); } Rationale Minimal approach Smart accounts are a new concept and we are still learning about the best ways to build them. Therefore, we should not be too opinionated about how they are built. Instead, we should define the most minimal interfaces that allow for interoperability between smart accounts and modules to be used across different account implementations. Our approach has been twofold: Take learnings from existing smart accounts that have been used in production and from building interoperability layers between them Ensure that the interfaces are as minimal and open to alternative architectures as possible Extensions While we want to be minimal, we also want to allow for innovation and opinionated features. Some of these features might also need to be standardized (for similar reasons as the core interfaces) even if not all smart accounts will implement them. To ensure that this is possible, we suggest for future standardization efforts to be done as extensions to this standard. This means that the core interfaces will not change, but that new interfaces can be added as extensions. These should be proposed as separate ERCs, for example with the title [FEATURE] Extension for ERC-7579. Specifications Multiple execution functions The ERC-4337 validation phase validates the UserOperation. Modular validation requires the validation module to know the specific function being validated, especially for session key based Validators. It needs to know: The function called by Entrypoint on the account. The target address if it is an execution function. Whether it is a call or delegatecall. Whether it is a single or batched transaction. The function signature used in the interaction with the external contract. For a flourishing module ecosystem, compatibility across accounts is crucial. However, if smart accounts implement custom execute functions with different parameters and calldata offsets, it becomes much harder to build reusable modules across accounts. Differentiating module types Not differentiating between module types could present a security issue when enforcing authorization control. For example, if a smart account treats validators and executors as the same type of module, it could allow a validator to execute arbitrary transactions on behalf of the smart account. Dependence on ERC-4337 This standard has a strict dependency on ERC-4337 for the validation flow. However, it is likely that smart account builders will want to build modular accounts in the future that do not use ERC-4337 but, for example, a native account abstraction implementation on a rollup. Once this starts to happen, the proposed upgrade path for this standard is to move the ERC-4337 dependency into an extension (ie a separate ERC) and to make it optional for smart accounts to implement. If it is required to standardize the validation flow for different account abstraction implementations, then these requirements could also be moved into separate extensions. The reason this is not done from the start is that currently, the only modular accounts that are being built are using ERC-4337. Therefore, it makes sense to standardize the interfaces for these accounts first and to move the ERC-4337 dependency into an extension once there is a need for it. This is to maximize learnings about how modular accounts would look like when built on different account abstraction implementations. Backwards Compatibility Already deployed smart accounts Smart accounts that have already been deployed will most likely be able to implement this standard. If they are deployed as proxies, it is possible to upgrade to a new account implementation that is compliant with this standard. If they are deployed as non-upgradeable contracts, it might still be possible to become compliant, for example by adding a compliant adapter as a fallback handler, if this is supported. Reference Implementation A full interface of a smart account can be found in IMSA.sol. Security Considerations Needs more discussion. Some initial considerations: Implementing delegatecall executions on a smart account must be considered carefully. Note that smart accounts implementing delegatecall must ensure that the target contract is safe, otherwise security vulnerabilities are to be expected. The onInstall and onUninstall functions on modules may lead to unexpected callbacks (i.e. reentrancy). Account implementations should consider this by implementing adequate protection routines. Furthermore, modules could maliciously revert on onUninstall to stop the account from uninstalling a module and removing it from the account. For modules types where only a single module is active at one time (e.g. fallback handlers), calling install* on a new module will not properly uninstall the previous module, unless this is properly implemented. This could lead to unexpected behavior if the old module is then added again with left over state. Insufficient authorization control in fallback handlers can lead to unauthorized executions. Malicious Hooks may revert on preCheck or postCheck, adding untrusted hooks may lead to a denial of service of the account. Currently account configuration functions (e.g. installValidator) are designed for single operations. An account could allow these to be called from address(this), creating the possibility to batch configuration operations. However, if an account implements greater authorization control for these functions since they are more sensitive, then these measures can be bypassed by nesting calls to configuration options in calls to self. Copyright Copyright and related rights waived via CC0. Citation Please cite this document as: zeroknots (@zeroknots), Konrad Kopp (@kopy-kat), Taek Lee (@leekt), Fil Makarov (@filmakarov), Elim Poon (@yaonam), Lyu Min (@rockmin216), "ERC-7579: Minimal Modular Smart Accounts [DRAFT]," Ethereum Improvement Proposals, no. 7579, December 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7579. Ethereum Improvement Proposals Ethereum Improvement Proposals ethereum/EIPs Ethereum Improvement Proposals (EIPs) describe standards for the Ethereum platform, including core protocol specifications, client APIs, and contract standards.