Demystifying ERC-6900

Seungmin Jeon
Decipher Media |디사이퍼 미디어
18 min readDec 8, 2023

Disclaimer: The Decipher Open Source Warriors team from the Seoul National University Blockchain Academy has written an article on ERC-6900. This article conducts an analysis based on the code, covering everything from the proposal’s background to its implementation method and significance. None of the content in this report should be considered as investment advice, nor should it be interpreted as such.

Author

Seungmin Jeon of Decipher Open Source Warriors Team

Seoul Nat’l Univ. Blockchain Academy Decipher(@decipher-media)

Reviewed By Decipher Media Team, Sangyeup Kim, Sangwon Moon

Table of Contents

  • Intro: The Limitations of ERC-4337
  • What is ERC-6900?
  • Implementation of ERC-6900
  • Challenges and Prospects of ERC-6900

Intro: The Limitations of ERC-4337

ERC-4337 is a standard that allows for the smooth use of contract wallets (accounts) without changes to Ethereum clients, by conducting additional verification through an object called User Operation, which replaces traditional transactions. Contract accounts that follow the ERC-4337 standard can contain various features such as Paymaster who pays for gas fees, batch transactions, BLS signature aggregation, social recovery, and session keys.

This enables a higher level of UX than traditional EOAs(Externally Owned Accounts). For example, using ERC-4337’s paymaster, users can pay gas fees with ERC-20 tokens instead of ETH, or have the protocol pay for them. Furthermore, instead of having to click a button for every transaction, if users could temporarily delegate their account to the protocol through a session key, they would be able to interact with the protocol with just one click. For a better understanding of ERC-4337, please refer to the following article.

However, ERC-4337 was proposed with the goal of achieving account abstraction without protocol changes, and thus does not define what form smart contract accounts should take. As a result, various forms of contract accounts have been proposed, leading to the following two issues:

  1. Compatibility issues due to various account forms

Currently, various companies (e.g., ZeroDev, Biconomy) offer contract accounts in SDK forms, but the forms of accounts they provide are different for each company, causing compatibility issues. This makes it difficult for users of apps supporting contract wallets to use these wallets outside the app, and also makes it hard for the apps to accommodate users of various contract accounts. In other words, although UX has improved through ERC-4337, it has the side effect of locking users within the app.

2. Extensibility issues of accounts

Most of the features that can be provided through ERC-4337 operate within the account, but since the account is in the form of a smart contract, there are limitations to changes and thus to extensibility. While it is possible to apply a Proxy structure to the account for upgrades, this process is also inconvenient. For example, if a user wants to add a new feature to their existing contract account, they must transfer the entire code to a new contract with that feature. This upgrade process can lead to issues such as additional audit costs for the new contract. Additionally, since a unified form of account is provided for all users, there is a disadvantage in that users cannot choose the features within their account.

To solve these problems, a new standard called ERC-6900 was proposed in April 2023.

What is ERC-6900?

ERC-6900 is an EIP titled “Modular Smart Contract Accounts and Plugins,” offering a standard for accounts compatible with ERC-4337. It is based on a modular structure that allows various functions to be freely installed and removed from an account, similar to installing or uninstalling apps on Android. Module contracts containing features to be included in the account are referred to as plugins.

ERC-6900, through its modular structure, enables users to easily add or remove various plugins (features) to their accounts. Especially, since current contract accounts are often limited to specific apps, using ERC-6900 could allow a single contract account to be easily used across multiple apps.

While this is a very interesting idea, the following must be considered when implementing it in actual code, focusing on security and UX:

  1. Installation and Removal of Plugins

The first consideration should be how to implement the installation and removal of plugins. The developers of ERC-6900 have drawn inspiration from the Diamond Proxy. Diamond Proxy is an extended structure of the typical Upgradeable Proxy, dividing the contract into several components (or facets) and allowing only some of them to be upgraded. Unlike simple Upgradeable Proxy structures, which require changing the entire logic contract for an upgrade, Diamond Proxy allows for the selection and upgrade of only desired components.

In Diamond Proxy, various functions of a contract are divided into components called facets, and the Proxy calls them using delegatecall. delegatecall is a function that 'borrows' external contract functions and uses them within the context of its own contract, allowing powerful manipulation of its own contract's storage through external functions.

Additionally, Diamond Proxy maintains a mapping based on function selectors to designate access paths to each facet. This allows the Proxy contract to freely access functions within each facet.

(Structure of Diamond Proxy | Source: ERC-2535 Official Documentation)

Furthermore, for security, Diamond Proxy stores all data in the Proxy contract and assigns accessible data to each facet. If there is no permission restriction on the data used by the facets, problems can arise in certain situations. For instance, zkSync has deployed a Diamond Proxy-like contract on Ethereum, creating facets for governance, L1 ↔ L2 bridging, and rollup data publishing. If the governance facet could access parameters used for bridging, it might jeopardize the entire system through improper operations or attacks.

In summary, Diamond Proxy has three main characteristics:

  1. Protocol functions are divided into several facet contracts, accessed through delegatecall.
  2. Proxy contract can call facet functions through the function’s selector.
  3. Data accessible by each facet is restricted.

Diamond Proxy, with these characteristics, is optimized for building multi-functional contract systems. This aligns well with the goal of ERC-6900 to create ‘smart contract accounts with various functions.’ Therefore, ERC-6900 borrows the structure of Diamond Proxy to implement the functionality for installing and removing plugins. However, there are significant differences between ERC-6900 and Diamond Proxy, which will be discussed later.

2. Interaction Between User and Account

Assuming an environment where plugins can be freely installed and removed is established, the second aspect to consider is how users interact with the account. The interaction between the contract account and the user can be divided into two main types: first, interaction through User Operation via ERC-4337’s entry point contract, and second, direct calling of functions within the contract account by the user. ERC-6900 differentiates these two types of interactions as ‘user operation validation’ and ‘runtime validation’, respectively, and presents separate processes for validation and execution for each.

(Interaction between User and Smart Account | Source: Seungmin)

3. Permission

The third aspect to consider is Permission. If anyone can create and install plugins, one must consider the possibility of these plugins being misused. For instance, imagine a session key plugin that lends account ownership for a certain period. If the session key owner interacts with a malicious contract, the account’s funds could be completely lost. Therefore, it is crucial to strictly set and verify permissions for external contract functions that a plugin can access and for interactions between plugins.

4. Guaranteeing Modularity

The last thing to consider is how to ensure modularity. In an environment where various plugins can be freely installed and removed from an account, there exist several cases including associated functions. For example, plugins A and B may have functions with identical selectors, which has the same functionality. Given that the plugin A is already deployed, the deployer of plugin B is recommended to ‘borrow’ that function from plugin A, rather than implementing the identical code in their plugin.

To address this, ERC-6900 introduces a feature called ‘dependency’. This mechanism introduces a clever way to form a modular ecosystem on plugins, which can increase readability and usability of associated functions.

Therefore, the characteristics of ERC-6900 can be summarized in four points:

  1. It follows the structure of Diamond Proxy (but has significant differences, which will be discussed later).
  2. User interactions are divided into User Operation and Runtime, with different validation and execution processes for each.
  3. It strictly limits permissions for operations through plugins.
  4. Dependencies can be set to ensure modularity during plugin installation and removal.

In the following sections, we will take a closer look at these four aspects.

Implementation of ERC-6900

Diamond Proxy

ERC-6900 adopts the structure of Diamond Proxy to form a modular structure for plugins and contract accounts. The account acts as the Proxy contract, and each plugin functions as a facet. Thus, when a User Operation or direct call occurs to the account, it is processed within the plugin through a fallback function (a function that is executed when the called function’s selector is not found in the contract, commonly used in Proxy patterns). In this process, there is a crucial difference from the previously mentioned Diamond Proxy.

Diamond Proxy uses delegatecall to call functions within the facet. Since a facet is a contract that only contains execution logic and doesn't need storage (sometimes even deployed as a library without storage), delegatecall is used. However, in ERC-6900, functions within a plugin are called using call, and the plugin has its own storage. The reason is as follows:

If delegatecall to a plugin is allowed from an account, the plugin's functions could access the account's storage data. This is very risky as a malicious plugin could delete or manipulate the account's storage information. This was not a major issue in the original Diamond Proxy because not just anyone could add facets. However, since ERC-6900 aims for an environment where anyone can freely build plugins, delegatecall could pose a significant risk.

Therefore, in ERC-6900, call is used instead of delegatecall, allowing data to be stored in the plugin’s storage as well.

Call Flow at MSCA

In ERC-6900, the contract account is referred to as Modular Smart Contract Account, or MSCA. The Call Flow within MSCA defined by ERC-6900 can be expressed as follows:

(ERC-6900 Call Flow | Source: ERC-6900 Official Documentation)

Let’s look at User Operation part. In ERC-4337, the verification and execution of User Operations are separated, so the flow is divided into verification and execution as shown in the left part of the above diagram. On the other hand, for Direct calls within MSCA, the flow undergoes verification through the Runtime Validation function as shown on the right. Each Flow goes through several stages including validation functions, hooks, and execution functions.

The functions used here can be broadly divided into three types. First, functions that perform some operation within the account or plugin are called execution functions. Subsequently, for each execution function, there is a validation function that performs verification for its call. Furthermore, hooks can be applied before and after each function call. Let’s look into each of these in more detail.

  1. Validation Function

This function performs authority verification for callers to the account. As mentioned earlier, if anyone can call the functions within the account, the account may be vulnerable to attacks that exhaust funds through gas consumption. Therefore, functions that consume gas or access storage must be controlled with validation functions.

There are two types of validation functions: User Operation validation function for calls coming in from the entry point, and Runtime validation function that are executed when an EOA directly calls a function within the account.

These validation functions do not exist in the account itself; they are all found within plugins. Hence, all calls to the account pass through the Validation function in a plugin. Depending on the use case, there can be various types of validation functions, such as:

  • Functions that verify signatures and allow only the owner’s address to pass.
  • Functions that verify signatures and allow only designated addresses to pass.
  • Functions that allow any address to pass.

2. Execution Function

These are functions where actual fund transfers and interactions with external contracts occur. There are two main types:

1) Standard execute function

Refers to the execute and executeBatch functions compatible with the IAccount interface of ERC-4337 reference implementation. These functions can perform all types of interactions based on the account’s gas fees and thus require strict validation functions (e.g., allowing only the owner to call).

2) Execution function

These are functions that exist within each plugin and common functions that can be executed from the account. For example, consider a function called recoverOwner in a social recovery plugin. Since only the guardian assigned for recovery should call this function, it must have an appropriate validation function. In ERC-6900, this is applied to the execution function as follows:

ManifestAssociatedFunction({
executionSelector: this.recoverOwner.selector,
associatedFunction: onlyGuardiansValidationFunction
});

By storing this information in the account’s storage, when the recoverOwner function is called, the caller’s authority is verified through the onlyGuardiansValidationFunction.

The selector of the Execution function is stored in the account storage, mapped to the plugin address during plugin installation.

3. Hook

There is also a function called a hook that runs before and after other functions. Similar to Uniswap V4’s Hook, it defines operations that need to be performed before and after specific tasks. For example, there could be a DailyGasSpendingLimit Hook that limits daily gas consumption. This hook can check the gas consumption using the gasleft() function before and after the execution function and prevent the execution of the function if a certain amount of gas is consumed within a day.

There can also be a pre-validation hook before the validation function, which is mainly used when multiple validations are necessary. For example, if a session key in a session key plugin is a multisig wallet, it would need to pass both multisig and session key validations. In this case, the verification for multisig can be executed as a pre-validation hook, and the verification for the session key as a validation function.

A more detailed call flow can be represented by the following diagram:

(Call Flow Between Account and Plugin | Source: Seungmin)

Each process is summarized as follows:

  1. The execution function of the plugin to be executed is encapsulated in calldata and called. If there is no matching selector in the account for this function, the fallback function is executed (this applies to all cases except for Standard execution functions like execute, executeBatch, or later mentioned executeFromPlugin, executionFromPluginExternal).
  2. After parsing which plugin address contains the execution function in the calldata, the associated pre-validation hooks and validation functions are executed. If the call comes through the ERC-4337 entry point, the verification logic would have already been executed, so it is skipped.
  3. Pre-execution hooks related to the Execution function are executed. The result of this hook execution returns information about which post-execution hooks to run.
  4. The execution function is executed.
  5. Based on the results returned in step 3, post-execution hooks are executed.

An important point here is that the msg.sender for calls to the plugin is always the account. Therefore, when storing or querying account-related information in a plugin, msg.sender is used as follows:

// src/plugins/owner/SingleOwnerPlugin.sol

mapping(address => address) internal _owners;

function isValidSignature(bytes32 digest, bytes memory signature)
public view override returns (bytes4) {
// Parameter used when accessing to _owners mapping is msg.sender.
// Through this, it retrieves stored owner address of the account.
if (SignatureChecker.isValidSignatureNow(_owners[msg.sender], digest, signature)) {
return _1271_MAGIC_VALUE;
}
return 0xffffffff;
}

This means that the functions of the plugin are based on the premise of being called from the account. However, this can lead to the following problem:

If a function of a specific plugin is called from another account or plugin, msg.sender is set as the caller, referring to a different storage than when called from the account, resulting in different outcomes. How can this problem be solved if a specific plugin needs to call a function in another plugin?

Permission For Plugins

To solve this, ERC-6900 defines functions such as executeFromPlugin and executeFromPluginExternal. These functions not only solve the above issue but also prevent various attack scenarios on the account.

(ERC-6900 Plugin Execution Flow | Source: ERC-6900 Official Documentation)

executeFromPlugin and executeFromPluginExternal are functions that define and limit tasks that can be executed through a plugin, each with the following features:

executeFromPlugin is used when one plugin wants to call a function in another plugin. It sets msg.sender as the account, allowing the execution of the execution function in another plugin without referring to incorrect storage or losing the context of the call. Additionally, to prevent attacks from malicious plugins, this function is designed to revert the call if it is not a function of a pre-designated plugin at the time of installation.

On the other hand, executeFromPluginExternal focuses more on security issues. It is used when a plugin wants to call a function in an external entity. A typical use case is the session key plugin. If an address with a session key could call any external function, it would pose a significant security risk. Therefore, it is necessary to pre-specify the external contracts and functions that the session key can access and revert the call if it tries to access unlisted functions. Implementing this according to the ERC-6900 standard looks like this:

manifest.permittedExternalCalls[0] = ManifestExternalCallPermission({
externalAddress: _TARGET_ERC20_CONTRACT,
permitAnySelector: false,
selectors: permittedExecutionSelectors
});

First, the external contracts and functions that can be accessed are specified within the plugin. This is hardcoded into the contract during the plugin deployment, making it immutable later on.

Then, the following execution function is defined within the same plugin:

function transferFromSessionKey(address target, address from, address to, uint256 amount) external 
returns (bytes memory returnData) {
bytes memory data = abi.encodeWithSelector(TRANSFERFROM_SELECTOR, from, to, amount);
returnData = IPluginExecutor(msg.sender).executeFromPluginExternal(target, 0, data);
}

This function is used when the session key sends tokens by calling the executeFromPluginExternal function within the account. The executeFromPluginExternal within the account performs the following steps:

  1. Retrieves the predefined permitted call information from storage.
  2. Compares this information with the input call information, and reverts if the call is not allowed.
  3. Executes the call. If there is an execution hook, it is executed as well.

Thus, executeFromPlugin and executeFromPluginExternal limit the functions that can be executed by the plugin. This not only prevents the context of the call (msg.sender) from changing when directly calling another plugin from within a plugin but also restricts access to external contracts.

However, it is not possible to completely prohibit calls from a plugin to an external contract through executeFromPluginExternal. It can’t prevent execution functions within a plugin from hardcoding an address and calling externally. Therefore, only plugins that have completed an audit should be installed on the account to prevent issues like hacking.

Dependency

Finally, a plugin can have dependencies (dependency) on other plugins. This is mainly used when borrowing functions (Validation function, Execution function) from other plugins. A typical example is the SingleOwnerPlugin in the Reference Implementation. It contains a function that allows only the owner's calls, as follows:

function userOpValidationFunction(uint8 functionId, UserOperation calldata userOp, bytes32 userOpHash)
external
view
override
returns (uint256)
{
if (functionId == uint8(FunctionId.USER_OP_VALIDATION_OWNER)) {
// Validate the user op signature against the owner.
(address signer,) =
(userOpHash.toEthSignedMessageHash()).tryRecover(userOp.signature);
if (signer == address(0) || signer != _owners[msg.sender]) {
return _SIG_VALIDATION_FAILED;
}
return _SIG_VALIDATION_PASSED;
}
revert NotImplemented();
}

Since this is a very generic validation function, other plugins will likely borrow and use it. In such cases, if it is entered as a dependency at the time of plugin installation, the validation function can be used in the plugin without having to implement the function separately.

exampleDependency[0] = address(singleOwnerPlugin).pack(
uint8(ISingleOwnerPlugin.FunctionId.USER_OP_VALIDATION_OWNER)
);

bytes32 exampleManifestHash = keccak256(
abi.encode(baseSessionKeyPlugin.pluginManifest())
);
account.installPlugin({
plugin: address(examplePlugin),
manifestHash: exampleManifestHash,
pluginInitData: "",
dependencies: exampleDependency,
injectedHooks: new IPluginManager.InjectedHook[](0)
});

This feature greatly helps in reducing the size of the code and improving readability when validation functions for specific execution functions overlap. Plugins with functions that only the owner can access can set the above-mentioned SingleOwnerPlugin as a dependency and use its validation function for their functions.

(Modular Architecture of Plugins | Source: Seungmin)

Thus, it is possible to receive all Hooks, Validation functions, and Execution functions from other plugins, as shown above. In other words, a modular architecture of plugins can be implemented based on dependencies.

(Proxy-like Structure of Plugins | Source: Seungmin)

An example of this can be seen in a Proxy-like structure as mentioned above. The basic validation functions and necessary data are housed in a ‘parent plugin’, with several ‘child plugins’ containing only logic and having dependencies on the validation functions in the parent plugin. This allows for safe addition, deletion, or replacement of logic while preserving the data and context within the parent plugin contract.

The Decipher Open Source Warriors team has implemented a session key plugin in line with the ERC-6900 standard, utilizing the modular architecture of plugins and the executeFromPluginExternal feature mentioned above, and has requested a pull request to the official ERC-6900 implementation GitHub. Currently, we are revising it based on feedback received after the PR and plan to contribute to a separate place for the community-based plugin.

The session key plugin consists of a parent plugin called BaseSessionKeyPlugin and a child plugin called TokenSessionKeyPlugin. This plugin has the following two advantages compared to the existing session key implementations:

  1. By using dependencies, it can manage all session key information in a single parent plugin.
  2. By using executeFromPluginExternal, it strictly limits the external contracts that child plugins can access, thereby preventing the outflow of funds within the account even in the event of malicious or hacked session keys.

Manifest

Lastly, to ensure safe installation without conflicting with other installed plugins, a plugin has a data structure called manifest. It includes information about the plugin's execution functions, validation functions, hooks, as well as dependencies, permitted calls, etc. During installation, this information is verified and all selectors of the functions within the plugin are stored in the account's storage.

Challenges and Prospects of ERC-6900

Currently, the reference implementation of ERC-6900 is completed as follows:

However, ERC-6900 still has many issues to be addressed, for the following reasons:

  1. Contract Size

The contract size of the smart account UpgradeableModularAccount.sol in the reference implementation is about 33KB. Ethereum limits the maximum size of contracts that can be deployed on the mainnet to 24KB, a restriction set in the 2016 Spurious Dragon hard fork. Hence, this contract cannot yet be deployed on the mainnet or testnets.

One of the reasons is because ERC-6900 uses call instead of delegatecall. By implementing some functions in external libraries or contracts and calling them via delegatecall, it is possible to modify the account storage while keeping the contract size down. However, as ERC-6900 restricts the use of delegatecall for security reasons, functions accessing storage must be implemented within the contract, increasing its size.

Therefore, efforts are needed to reduce the size of the contract, such as by refactoring redundant parts.

2. Gas Consumption

As seen in the call flow diagram, ERC-6900 includes several calls between the account and plugins, which increases the gas cost. Depending on the implementation, an external contract call consumes about 2,000 GAS each time. While this cost might be slightly reduced if only one plugin is involved (warm/cold address), it becomes expensive with several plugins involved, increasing the user’s burden. However, it is crucial to reduce user costs through gas optimization, such as using assembly blocks within the functions of the account and plugin contracts. The Alchemy team, who implemented ERC-6900, is considering modifying the architecture based on various options, like applying the transient storage from EIP-1153 that comes with the Dencun update or bundling multiple verification steps into a multicall.

Meanwhile, the current reference implementation has partially sacrificed optimization for readability. The Alchemy team plans to update it to be deployable on the mainnet by early next year, and at that time, the issues mentioned in points 1 and 2 are expected to be mostly resolved.

3. Complexity in Plugin Implementation

From a developer’s perspective, implementing a plugin requires considering numerous factors. While there are many complexities, some examples include:

  • Deciding whether to directly define validation functions associated with each execution function or to receive them through dependency settings from other plugins.
  • When multiple validations for a caller are required, determining how to divide and set up pre Validation hooks and validation functions.
  • Ensuring compatibility with existing plugins. For instance, if a function to be implemented in a plugin already exists in an existing plugin, it needs to be called via executeFromPlugin or set as a dependency.

These complexities can be addressed from various perspectives. Firstly, the architecture itself could add some form of interface or method to abstract the complexity. Documentation for plugin developers is needed, and a dashboard that organizes the functions and methods of existing plugins could be developed to facilitate the management of dependencies and executeFromPlugin.

The Alchemy team is working on updates to reduce the complexities pointed out, in response to community feedback received through Telegram and bi-weekly community calls.

ERC-6900 allows for the installation and removal of plugins from an account, similar to installing and uninstalling Android apps. This enables users to personalize their wallets and add or remove features according to their needs and preferences, creating a customized user environment. It will solve compatibility issues among ERC-4337 compatible wallets, allowing for free movement across different wallet platforms.

Furthermore, ERC-6900 presents a universal standard that can be widely accepted for contract accounts, while incorporating various elements to facilitate implementation and enhance account security.

  1. A modified Diamond Proxy structure for safe installation and removal of plugins.
  2. Functions (validation function, execution function, hook) and a Call Flow structure to manage interactions between accounts and plugins efficiently and securely.
  3. Permission settings to ensure the flexibility and security of plugins.
  4. Dependencies to aid in code optimization and enhance code readability.

Through this standard, it is expected that the wallet ecosystem of Ethereum and EVM-compatible chains will evolve, and user experience will be significantly improved.

Reference

--

--