ERC-6900: Modular Smart Contract Accounts and Plugins

Hi everyone - thank you everyone who has continued to work with us on this proposal. We wanted to give an update to bring everyone up to speed on some of the design decisions we’re considering as a result of these conversations, including open areas we’re still working through.

We’re still in the process of formalizing the wording around this proposed change, and would like to get feedback and and bring the community into the decision process around these changes. The current draft is viewable here:

We’re still actively seeking collaboration and co-authorship in order to make this as useful as possible for the ecosystem. Please reach out if you want to work together on this proposal!

Proposed updates to the spec

The biggest change we’ve been testing is the idea of updating all function invocations, including execution functions, validation functions, and hooks, to use call instead of delegatecall. This isolates storage and execution code to be per-plugin, preventing accidental or malicious storage overrides across plugins and fundamentally changes the plugin trust model.

  • To preserve the capabilities of modular accounts to make arbitrary external calls, we introduce two standardized execution functions to ERC-6900, grouped together in a new interface IStandardExecutor. The two functions are execute and executeBatch, taking the same names as the functions from ERC-4337’s SimpleAccount but with new parameters.
  • These functions become necessary to provide through the standard when moving from delegatecall to call, because otherwise every single new contract interaction target would need to be added as a new plugin.
  • The IPlugin interface (with the method pluginStorageRoot()) would be removed, due to the changes in storage management no longer necessitating this.

We propose consolidating global hooks and regular hooks into one concept called “hook groups”. Hook groups have each hook type as an optional field, and accounts must maintain an association between execution functions and which hook groups apply for the execution function. This addresses a comment from @dror on how postExecHooks can take in data parsed from a preExecHook to reduce the number of calldata copies.

We propose a new hook type, pre-validation hooks, that run before either a user op validator or a runtime validator. The intended use case for these hooks is permission checking and similar pre-transaction checks that are not related to signature validation itself.

  • With the new storage model, it is possible to limit the blast radius of unverified validation and execution plugins using pre-validation hooks. These hooks can scope which external contracts or what parameters a given function is using.
  • The code implementations and storage of these two contracts are independent, which allows for independent security assessments to be valid. E.g. a formally verified permissions plugin can limit an unverified validation plugin.

Plugin Loupe functions have been scoped down to reduce implementation overhead. We are actively looking for feedback in this area, as it affects the implementation process for off-chain entities.

Tradeoffs we’re considering

The current plugin update function (updatePlugins in IPluginUpdate) allows for “slim” updates that only specify the fields that actually need to be set. This is done by using only the minimum number of array fields needed to express the desired plugin configuration, specifying function types using the defined enums and omitting any function types that aren’t used.

  • This allows, for example, a function that only has a user op validator and an execution function to not need to specify anything for other fields, like hook groups or a runtime validator. This reduces calldata size when the other fields are not needed.

  • However, this requires all function references to be casted as bytes24. This is what Solidity internally uses as the ABI-encoded type, but requires casting back to a function (or unpacking into address + selector) to perform the call. It might be possible to change the interface to take in function types in the struct (i.e. declare a struct as follows), but doing so would require defining new structs for each function type:

    struct UserOpValidatorUpdate {
        PluginAction action;
        function(UserOperation calldata, bytes32) external validatorFunction;
    }
    

With hooks passing data from preExec to postExec, it now requires a pairwise association with across each, to be able to track which preExecHook returned data should be sent to which postExecHook. However, with the introduction of preValidate hooks, it is unclear exactly what the data flows should look like, if any.

  • The group association is still valuable with the new hook types, as some preValidation hooks will want to defer state updates until execution. For example, preValidate hook that enforces an ETH spend limit will not want to perform the state update for tallying up ETH used by a key during the validateUserOp step. That should be avoided because in the case that execution reverts, the validation step will not be reverted, resulting in the spend limit usage increasing without an actual spend. For this reason, a preHook that performs the state update should be used.

Specifying the intended validator in the calldata of the standard execute functions limits the custom validation routing to just those two functions. This reduces the account-internal storage requirements (how the mappings and/or arrays look) for ERC-6900 implementing accounts. As an alternative, @fmc has suggested specifying the validator in the signature. We’re leaning towards not taking this direction to limit the scope of required validator storage in the modular account, but if there’s interest and a compelling reason to move it into the signature field, we can make that change.

PluginStorageLib

We’re still hard at work on testing and securing a reference implementation. In the meantime, we’ve realized there are some common components that many plugin developers will need to create when developing using the new call model that they did not need to do under the previous delegatecall model, specifically around designing storage layouts that abide by the account-associated storage rules of ERC-4337. To aid in developing these plugins, we’re sharing the first version of PluginStorageLib - a solidity library that presents account associated storage as one giant (bytes32 => bytes) mapping, allowing for both nested mappings and dynamic arrays in associated storage. This is intended for singleton plugin contracts to have storage per-account.

This is an unaudited library intended only as a starting point for plugin development. DO NOT USE THIS IN A PRODUCTION ENVIRONMENT. It requires custom serialization and deserialization for data stored, and care when designing key derivation schemes (i.e. protecting from manufactured key collisions).

We hope this can help explain and potentially alleviate some of the tradeoffs of switching from delegatecall to call, and look forward to feedback on its implementation and usage.

Conclusion

These proposed changes to ERC-6900 need to involve community input. Please feel free to leave comments and let us know what you think!

6 Likes