Skip to content
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

Add ERC: Minimal Modular Smart Accounts #163

Merged
merged 14 commits into from
Jan 30, 2024
345 changes: 345 additions & 0 deletions ERCS/erc-7579.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
---
eip: 7579
title: Minimal Modular Smart Accounts
description: Modular smart account interfaces and behavior for interoperability with minimal restrictions for accounts and modules
author: zeroknots (@zeroknots), Konrad Kopp (@kopy-kat), Taek Lee (@leekt), Fil Makarov (@filmakarov), Elim Poon (@yaonam), Lyu Min (@rockmin216)
discussions-to: https://ethereum-magicians.org/t/erc-7579-minimal-modular-smart-accounts/17336
status: Draft
type: Standards Track
category: ERC
created: 2023-12-14
requires: 165, 1271, 2771, 4337
---

## 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](./eip-165.md) and [ERC-1271](./eip-1271.md). 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](./eip-4337.md) 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.

```solidity
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;
zeroknots marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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.

```solidity
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

```solidity
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](./eip-2771.md) 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:

```solidity
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`.

```solidity
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`.

```solidity
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:

1. Take learnings from existing smart accounts that have been used in production and from building interoperability layers between them
2. 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:

1. The function called by Entrypoint on the account.
2. The target address if it is an execution function.
3. Whether it is a `call` or `delegatecall`.
4. Whether it is a single or batched transaction.
5. 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`](../assets/eip-7579/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](../LICENSE.md).
Loading
Loading