Skip to content

Latest commit

 

History

History
290 lines (200 loc) · 19.1 KB

eip-6900.md

File metadata and controls

290 lines (200 loc) · 19.1 KB
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

Abstract

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.

Motivation

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.

diagram showing relationship between accounts and plugins of each type

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.

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.

Terms

  • 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.
  • 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.

Plugin Function Signatures

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 for authorizer, validUntil, and validAfter. The packing order MUST be validAfter in the first 6 bytes, validUntil in the next 6 bytes, and authorizer in the last 20 bytes. Pre User Operation Validation Hooks MUST NOT return an authorizer 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.

Overview

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.

diagram showing execution flow within an account

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.

Interfaces

Modular Smart Contract Accounts MUST implement the IAccount interface from ERC-4337.

Common types

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.

IPluginUpdate.sol

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;
}

IPluginLoupe.sol

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);
}

IStandardExecutor.sol

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;
}

Expected behavior

Calls to updatePlugins

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.

Calls to validateUserOp

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.

Calls to execution functions

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 the call 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.

Plugin update operations

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 by hookGroupId. 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.

Rationale

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.

Backwards Compatibility

No backward compatibility issues found.

Reference Implementation

TBD.

Security Considerations

Needs discussion.

Copyright

Copyright and related rights waived via CC0.