eip | title | description | author | discussions-to | status | type | category | created | requires |
---|---|---|---|---|---|---|---|---|---|
6900 |
Modular Smart Contract Accounts and Plugins |
Interfaces for composable contract accounts optionally supporting upgradability and introspection |
Adam Egyed (@adamegyed), Fangting Liu (@trinity-0111) |
Draft |
Standards Track |
ERC |
2023-04-18 |
4337 |
This proposal standardizes smart contract accounts and account plugins, which are smart contract interfaces that allow for composable logic within smart contract accounts. This proposal is compliant with ERC-4337, and takes inspiration from ERC-2535 when defining interfaces for updating and querying modular function implementations.
This modular approach splits account functionality into three categories, implements them in external contracts, and defines an expected execution flow from accounts.
One of the goals that ERC-4337 accomplishes is abstracting the logic for execution and validation to each smart contract account.
Many new features of accounts can be built by customizing the logic that goes into the validation and execution steps. Examples of such features include session keys, subscriptions, spending limits, and role-based access control. Currently, some of these features are implemented natively by specific smart contract accounts, and others are able to be implemented by plugin systems. Examples of proprietary plugin systems include Safe modules and ZeroDev plugins.
However, managing multiple account instances provides a worse user experience, fragmenting accounts across supported features and security configurations. Additionally, it requires plugin developers to choose which platforms to support, causing either platform lock-in or duplicated development effort.
We propose a standard that coordinates the implementation work between plugin developers and wallet developers. This standard defines a modular smart contract account capable of supporting all standard-conformant plugins. This allows users to have greater portability of their data, and for plugin developers to not have to choose specific account implementations to support.
We take inspiration from ERC-2535’s diamond pattern for routing execution based on function selectors, and create a similarly composable account. However, the standard does not require the multi-facet proxy pattern.
These plugins can contain execution logic, validation schemes, and hooks. Validation schemes define the circumstances under which the smart contract account will approve actions taken on its behalf, while hooks allow for pre- and post-execution controls.
Accounts adopting this standard will support modular, upgradable execution and validation logic. Defining this as a standard for smart contract accounts will make plugins easier to develop securely and will allow for greater interoperability.
Goals:
- Provide standards for how validation, execution, and hook functions for smart contract accounts should be written.
- Provide standards for how compliant accounts should add, update, remove, and inspect plugins.
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.
- An account (or smart contract account, SCA) is a smart contract that can be used to send transactions and hold digital assets. It implements the
IAccount
interface from ERC-4337. - A modular account (or modular smart contract account, MSCA) is an account that supports modular functions. There are three types of modular functions:
- Validation functions validate the caller’s authenticity and authority to the account.
- Execution functions execute any custom logic allowed by the account.
- Hooks execute custom logic and checks before and/or after an execution function.
- A validation function is a function that validates authentication and authorization of a caller to the account. There are two types of validation functions:
- User Operation Validator functions handle calls to
validateUserOp
and check the validity of an ERC-4337 user operation. - Runtime Validator functions run before an execution function when not called via a user operation, and enforce checks. Common checks include allowing execution only by an owner.
- User Operation Validator functions handle calls to
- An execution function is a smart contract function that defines the main execution step of a function for a modular account.
- The standard execute functions are two specific execution functions that are implemented natively by the modular account, and not on a plugin. These allow for open-ended execution.
- A hook is a smart contract function executed before or after another function, with the ability to modify state or cause the entire call to revert. There are four types of hooks.
- Pre User Operation Validation Hook functions run before user operation validators. These can enforce permissions on what actions a validator may perform via user operations.
- Pre Runtime Validation Hook functions run before runtime validators. These can enforce permissions on what actions a validator may perform via direct calls.
- Pre Execution Hook functions run before an execution function. They may optionally return data to be consumed the postHook.
- Post Execution Hook functions run after an execution function. They may optionally take returned data from preHook.
- Associated function refers to either a validation function or a hook.
- A plugin is a deployed smart contract that hosts any amount of the above three kinds of modular functions: execution functions, validation functions, or hooks.
The plugin function types specified above MUST have the following function signatures:
- User Operation Validator functions and Pre User Operation Validation Hooks:
function(UserOperation calldata, bytes32) external returns (uint256)
. The parameters sent by the account MUST be the user operation and user operation hash. The return value MUST represent packed validation data forauthorizer
,validUntil
, andvalidAfter
. The packing order MUST bevalidAfter
in the first 6 bytes,validUntil
in the next 6 bytes, andauthorizer
in the last 20 bytes. Pre User Operation Validation Hooks MUST NOT return anauthorizer
value other than 0 or 1. - Runtime Validator functions and Pre Runtime Validation Hooks:
function(address, uint256, bytes calldata) external
. The parameters sent by the account MUST be the caller address, the call value, and the calldata sent. To indicate that the entire call should revert, the function MUST revert. - Pre Execution Hooks:
function(address, uint256, bytes calldata) external returns (bytes memory)
. The parameters sent by the account MUST be the caller address, the call value, and the calldata sent. To indicate that the entire call should revert, the function MUST revert. The return value MUST contain any context to pass to a Post Execution Hook, if present. An empty bytes array MAY be returned. - Post Execution Hooks:
function(bytes calldata) external
. The parameter sent by the account MUST be the context returned by the associated Pre Execution Hook. To indicate that the entire call should revert, the function MUST revert. - Execution functions may have any function signature.
A modular account handles two kinds of calls: either from the Entrypoint
through ERC-4337, or through direct calls from externally owned accounts (EOAs) and other smart contracts. This standard supports both use cases.
A call to the smart contract account can be decomposed into 5 steps as shown in the diagram below. The validation steps (Steps 1 and 2) validate if the caller is allowed to call. The pre execution hook step (Step 3) can be used to do any pre execution checks or updates. It can also be used with the post execution hook step (Step 5) to perform additional actions or verification. The execution step (Step 4) performs a defined task or collection of tasks.
Each step is modular, supporting different implementations for each execution function, and composable, supporting multiple steps through hooks. Combined, these allow for open-ended programmable accounts.
Modular Smart Contract Accounts MUST implement the IAccount
interface from ERC-4337.
The following types are common across the following interfaces. Implementors MAY use the elementary value type instead of the type alias.
type FunctionReference is bytes24;
type HookGroupId is uint32;
struct Execution {
address target;
uint256 value;
bytes data;
}
Variables of the type FunctionReference
MUST be interpreted as an address in the first 20 bytes and a function selector in the remaining 4 bytes.
Plugin modification interface. Modular Smart Contract Accounts MAY implement this interface to support updates to plugins.
interface IPluginUpdate {
enum PluginAction {
ADD,
REPLACE,
REMOVE
}
enum ValidatorType {
USER_OP_VALIDATOR,
RUNTIME_VALIDATOR
}
enum HookType {
PRE_EXEC_HOOK,
POST_EXEC_HOOK,
PRE_USER_OP_VALIDATION_HOOK,
PRE_RUNTIME_VALIDATION_HOOK
}
struct ExecutionUpdate {
PluginAction action;
address pluginAddress;
bytes4[] executionSelectors;
ValidatorUpdate[] validatorUpdates;
}
struct ValidatorUpdate {
PluginAction action;
ValidatorType validatorType;
FunctionReference functionReference;
}
struct HookGroupUpdate {
PluginAction action;
HookGroupId hookGroupId;
bytes4[] executionSelectors;
}
struct HookUpdate {
PluginAction action;
HookGroupId hookGroupId;
HookType hookType;
FunctionReference functionReference;
}
event ExecutionPluginUpdate(ExecutionUpdate[] executionUpdates);
function updatePlugins(
ExecutionUpdate[] calldata executionUpdates,
HookUpdate[] calldata hookUpdates,
HookGroupUpdate[] calldata hookGroupUpdates,
Execution[] calldata initializationCalls
) external;
}
Plugin inspection interface. Modular Smart Contract Accounts MAY implement this interface to support visibility in plugin configuration on-chain.
interface IPluginLoupe {
struct ExecutionPluginConfig {
address executionPluginAddress;
HookGroupId[] hookGroupIds;
HookGroup[] hookGroups;
FunctionReference userOpValidator;
FunctionReference runtimeValidator;
}
struct HookGroup {
FunctionReference preUserOpValidation;
FunctionReference preRuntimeValidation;
FunctionReference preExec;
FunctionReference postExec;
}
function getExecutionFunctionConfig(bytes4 executionSelector)
external
view
returns (ExecutionPluginConfig memory);
function getStandardExecutionValidators()
external
view
returns (FunctionReference[] memory userOpValidators, FunctionReference[] memory runtimeValidators);
}
Standard execution interface. Modular Smart Contract Accounts MUST implement this interface to support open-ended execution.
interface IStandardExecutor {
function execute(address target, uint256 value, bytes calldata data, FunctionReference validator)
external
payable;
function executeBatch(Execution[] calldata executions, FunctionReference validator) external payable;
}
The function updatePlugins
takes in arrays of execution updates, hook updates, and hook group updates to perform. It also takes in an optional array of initialization calls. The function MUST perform the update operation sequentially. Then, for each sequential member of the initializationCalls
array, the MSCA MUST perform a call
operation towards the specified target
address with the specified value
and callData
.
⚠️ The ability to update a plugin is very powerful. The security of the updatePlugins determines the security of the account. It is critical for modular account implementers to make sure updatePlugins has the proper security consideration and access control in place.
When the function validateUserOp
is called on modular account by the EntryPoint
, it MUST find the user operation validator defined for the selector in userOp.callData
, which is in the first four bytes. If there is no function defined for the selector, or if userOp.callData.length < 4
, then execution MUST revert.
If the execution selector has associated hook groups with pre user operation validation hooks, then those hooks MUST be run sequentially. If any revert, the outer call MUST revert. If any return an authorizer
value other than 0 or 1, execution MUST revert. If any return an authorizer
value of 1, indicating an invalid signature, the returned validation data of the outer call must also be 1. If any return time-bounded validation by specifying either a validUntil
or validBefore
value, the resulting validation data MUST be the intersection of all time bounds provided.
If the call is to a standard execution function, then the modular account MUST verify that the provided validator
in calldata has previously been associated with either of the standard execution functions. If it was previously added, the specified user operation validator MUST be run.
Then, the MSCA MUST execute the validator function with the user operation and its hash as parameters using the call
opcode. The returned validation data from the user operation validator MUST be updated, if necessary, by the return values of any pre user operation validation hooks, then returned by validateUserOp
.
When a function other than a natively defined function is called on an MSCA, it MUST find the plugin configuration for the corresponding selector added via updatePlugins
. If no corresponding plugin is found, the MSCA MUST revert. Otherwise, the following steps MUST be performed.
Alternatively, when the modular account natively implements functions in IPluginUpdate
and IStandardExecutor
, the same following steps MUST be performed for those functions. Other natively implemented functions MAY perform these steps.
The steps to perform are:
- If the call is not from the
EntryPoint
, then find an associated runtime validator function. If one does not exist, execution MUST revert. The modular account MUST execute all pre runtime validation hooks, then the runtime validator function, with thecall
opcode. All of these functions MUST receive the caller, value, and execution function’s calldata as parameters. If any of these functions revert, execution MUST revert. - If there are pre execution hooks defined in the associated hook groups of the execution function, execute those hooks with the caller, value, and execution function’s calldata as parameters. If any of thee hooks returns data, it MUST be preserved until the call to the post execution hook. The operation MUST be done with the
call
opcode. If any of these functions revert, execution MUST revert. - Run the execution function.
- If any associated post execution hooks are defined, run the functions. If a pre execution hook in the same hook group returned data to the account, that data MUST be passed as a parameter to the post execution hook. The operation MUST be done with the
call
opcode. If any of these functions revert, execution MUST revert.
⚠️ If the execution function does not have a definition for either pre runtime validation hooks, pre execution hooks or post execution hooks, the undefined functions will be skipped. The execution function will be run and it may change account state.
When updatePlugins
is called with PluginAction.ADD
, the following MUST occur:
- Each execution selector MUST be added as a valid execution function, with the contract specified in
pluginAddress
as implementation contract to call to. - Each validator function reference MUST be added to their parent execution function in the role specified by
validatorType
. - Each hook update MUST add the hook type specified by
hookType
to the group specified byhookGroupId
. If that hook type is already defined, the update MUST revert. - Each hook group update MUST add that hook group to the execution selectors specified by
executionSelectors
.
Execution function selectors MUST be unique when added.
When updatePlugins
is called with PluginAction.REPLACE
, each each execution selector or function reference MUST override any previous definition for said function. Existing associated functions not specified in this operation MUST NOT be modified.
When updatePlugins
is called with PluginAction.REMOVE
, execution function definitions and associated function definitions MUST be removed.
When calling updatePlugins
, the account MUST run the validators and hooks as they are specified before the operation is complete. Notably, the post execution hooks defined prior to the update must run after the update.
ERC-4337 compatible accounts must implement the IAccount
interface, which consists of only one method that bundles validation with execution: validateUserOp
. A primary design rationale for this proposal is to extend the possible functions for a smart contract account beyond this single method by unbundling these and other functions, while retaining the benefits of account abstraction.
The function routing pattern of ERC-2535 is the logical starting point for achieving this extension into multi-functional accounts. It also meets our other primary design rationale of generalizing execution calls across multiple implementing contracts. However, a strict diamond pattern is constrained by its inability to customize validation schemes for specific execution functions in the context of validateUserOp
, and its requirement of delegatecall
.
This proposal includes several interfaces that build on ERC-4337 and are inspired by ERC-2535. First, we standardize a set of modular plugins that allow smart contract developers greater flexibility in bundling validation, execution and hook logic. We also propose interfaces like IPluginUpdate
and IPluginLoupe
that take inspiration from the diamond standard, and provide methods for updating and querying execution functions, validation functions, and hooks.
No backward compatibility issues found.
TBD.
Needs discussion.
Copyright and related rights waived via CC0.