ERC-6900: Modular Smart Contract Accounts and Plugins

Hi all, we’re excited to share some updates to the proposal taking into account feedback provided thus far.

Most notable are changes to the way plugins are configured on the account and the introduction of an interface for plugins. Updates related to the plugin manifest that enable plugin installation and removal are largely a product of suggestions and feedback by @yoavw, who will be joining as a coauthor to this proposal. As a result of these updates, the former IPluginUpdate interface and the updatePlugins function have been removed.

The full changelog, along with areas we’re still thinking through can be found below.

ERC Changelog:

  • Updated shared components diagram to more accurately depict modular functions and their relationship to plugins.
  • Added “native function” definition under Terms.
  • Added the IPlugin interface that describes the functions a plugin must implement.
    • Plugins must implement ERC-165 and support this interface.
    • Associated functions in the interface take a uint8 function identifier to support multiple implementations of the same type of associated function.
  • Removed the FunctionReference custom type, as function identifiers do not represent function selectors.
  • Removed the hook group concept and its associated types.
  • Replaced the IPluginUpdate interface with the IPluginManager interface.
    • The function updatePlugins has been replaced by the functions installPlugin and uninstallPlugin.
  • Updated the IPluginLoupe interface to reflect the above changes.
  • Added the IPluginExecutor interface, which introduces new functions executeFromPlugin and executeFromPluginExternal that allow plugins to interact with execution functions on the modular account as well as external contracts.
  • Standard execution functions and executeFromPluginExternal should not allow calls to contracts that implement the IPlugin interface to maintain the integrity of account and plugin configuration.
  • Added a section describing the plugin manifest, and the expected behavior for installPlugin and uninstallPlugin.
  • Added a section describing the expected behavior for calls made from plugins.
  • Style updates and typo fixes.

Areas we’re still working on

  • We’re actively working on introducing a permissions model similar to what @yoavw has described in an earlier comment. We hope to release an update on this in the next couple of weeks.
  • Using ERC-165 is one way to enforce data integrity on the account and associated data within plugins, but presents additional overhead during standard execution. We’re still exploring ways we can improve this flow.
  • The way dependencies are requested by the plugin manifest and provided during installation prevents having to make immutable dependency references in the manifest, and offers flexibility for the user in providing a plugin of their choice for the dependency. However, there is still some rigidity in that the expected class of plugins for a given dependency must all implement the same interface. We’re giving more thought to this area to see if there is room for improvement.

As stated, some details are still in flux and will be ironed out in subsequent updates. We’d love to continue to refine the proposal with input from the community. Let us know what you think!

PR: Update EIP-6900: Spec update 3 by jaypaik · Pull Request #7516 · ethereum/EIPs · GitHub

6 Likes

Can someone help give me an example of a Validation Hook (either pre-UserOp or pre-Runtime)? I’m not immediately seeing the practical utility of this hook at validation time that differentiates from the same logic at pre-execution runtime?

Good question! There are a couple of design patterns that are a better fit as a pre-validation hook than a pre-exec hook.

Consider a “permission delegated” entity, like a session key or an authorized spender. These may be other contracts, or a specific EOA, that are able to interact through your account but only in a predefined way - such as only interacting with a specific contract, or only to send a specific amount of a given token.

When defining the rules and conditions under which this entity may interact through your account, it is preferable to perform the checks during validation instead of execution for the user operation case, because if the checks fail (like an attempt to interact with a different contract that what’s been assigned), then there is no valid user operation. If this check was instead performed at runtime, then the result of validateUserOp(...) would be a valid operation to perform that just reverts during execution. This would allow the entity to waste gas on behalf of the account.

There is less clear of a distinction with pre runtime validation hooks, but there still are some cases where this makes sense. Having these hooks allows you to define actions that should occur based only on the validator chosen, instead of for all uses of the execution method. This mostly matters for the standard execution functions execute and executeBatch, where more than one validator may be assigned and which one is used depends on how the function is called. For instance, you may want a hook to only run when a specific validator is used to invoke a call, either to enforce permission checks or to perform a related action like emitting a log.

Generally, we hope to be un-opinionated about what plugins should do - we want to support as much creativity and innovation in plugin design as possible. This can come with added workloads to the account though, so managing complexity is also a design goal we need to balance. If you have ideas for different account organization strategies, we’d love to hear it too!

1 Like

Sorry if this doesn’t make sense, but why can’t validateUserOp just be a module/plugin? The module can then use a runtimeValidator which appropriately casts the data to (UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) and does whatever additional checks it needs to do like signature validation.

Good question This is something we chose to “bake in” to the standard to allow the account to be a single source of truth for the modular account config.

One of the core parts of the standard is allowing for multiple types of transaction validity to coexist at the same time in an account. To do this, the logic of validateUserOp switches and is routed differently based on what function is being called, as specified by the selector in the first 4 bytes of userOp.callData sent to validateUserOp. If the switching behavior of validateUserOp is implemented as a plugin, then the relationship between the function selector and which validation function to run would have to be implemented within the plugin, and the “modular account config” would end up being in 2 locations.

If we only wanted one logical component to run during validateUserOp, then it would simple to make a plugin that just defines the function with its single implementation. But, since we want to allow different types of user ops to be validated differently, and for this validation logic to live in separate contracts that can be added or removed, then the plugin defining validateUserOp must now manage the execution → validation function mapping and the overall state of installed / uninstalled plugins. The reason validateUserOp is given special treatment and required to be a native function for 6900 is to consolidate the installation management and modular features, so plugin developers don’t have to re-invent modularity.

Would love to hear more of your thoughts too – is this something you’re interested in for supporting other validity checking functions?

Yeah, I was trying to think how wallets could install a plugin to support ERC-7521, which requires a similar validateUserIntents function. I’m still confused why whatever you would have installed as a Pre User Operation Validation Hook couldn’t just be installed as a Pre Runtime Validation Hook for the function validateUserOp installed in some base 4337 plugin. This would allow you to stack any sort of additional rules that apply to specific user operations. It would be kind of ugly to add another exception to ERC-6900 for ERC-7521 user intent validity checks. It would also help reduce ERC-6900 complexity if there were no special exceptions at all.

Hi all, we are excited to share the latest update to the ERC spec. This completes the update we began with the changes we posted on August 23, and focuses on expanding and refining the permissions system for plugins. This change also allowed us to streamline parts of the spec by removing the special-case validation for standard execute functions. Finally, we have updated visual and text portions of the spec to make it easier for builders and stakeholders to engage with.

As a next step, we’ll be releasing a Reference Implementation incorporating all of these changes in the next few weeks. Stay tuned!

The full changelog, along with areas we’re still thinking through, can be found below.

ERC Changelog

  • Expanded permission system
    • Plugins that define execution functions can optionally define sets of permission strings for each function to indicate their level of sensitivity. These can be discovered with view calls by wallets and displayed to users when other plugins request permission to use them.
    • Plugins that wish to perform calls through the account to external contracts can request specific permissions to do so. These requests can be scoped to specific address, or to selectors on those addresses, via executeFromPluginExternal. This was introduced in the previous update, and now the plugin manifest struct for permittedExternalCalls is updated to move the field for “any external contract allowed” up one level to the manifest itself.
  • Updates to standard executions
    • Introduce return data to standard execute functions.
    • Removed the “multiple validation functions” special casing for the standard execute functions. This was originally added specifically to provide expanded capabilities for plugins, but using validation functions to do so ended up being rather cumbersome. It also introduced an additional difficulty for wallets preparing calldata. Since plugins no longer execute through the standard execute functions, it is therefore unnecessary to support multiple validation functions for the standard execute functions.
      • This also removes the validator based hook assignment in the plugin manifest used to special-case standard execution hooks.
      • This also removes previous account storage fields.
      • Together, these changes greatly simplify the account storage requirements to be ERC-6900 compliant, without compromising on feature capabilities.
  • Updates to installPlugin and uninstallPlugin
    • The spec is now more open-ended in implementation choices for how accounts should maintain consistency of installations, but requires that consistency is enforced.
      • For instance, one wallet implementation may choose to put installation data into storage, while another may choose only calldata with hash checking.
      • A custom configuration field is added to uninstallPlugin to support implementation-specific data needs. This is expected to be handled via the wallet software itself.
    • Dependency injection in plugin installation is updated to also inject the function ids of the provided methods, as opposed to just the plugin address, to allow for more user flexibility in configuration.
    • Naming change: pluginExecutionHookspermittedCallHooks
    • Additional permission-enforcing mechanisms can be done by hooks, with an added workflow for applying permittedCallHooks during install time to protect the account. These hooks may perform custom checks, like decoding calldata to interpret a parameter or checking storage for valid state.
    • Installation of protected function selectors is now prohibited by the spec. Protected selectors include:
      • Function selectors for calls the ERC-4337 EntryPoint contract makes to non-account contracts, such as paymasters and aggregators. Without blocking these, accounts installing unknown plugins with clashing selectors may exhibit unexpected behavior and pay gas for other entities.
      • Function selectors for methods natively defined on the account (non-plugin functions). To help accounts maintain data consistency, these should be avoided.
  • Miscellaneous changes
    • Updated drawings, Terms and refactored Interfaces sections to improve clarity and readability.
    • Simplified manifest structs for validators and hooks, due to the same type of assignment.
    • There are more naming changes on concepts and variable. For example,
      • for clarity, replace usage of the phrasing “execution selector” with just “function selector”. “Execution function” remains as the name for fallback-handling functions on the account.
      • VALIDATION_ALWAYS_DENY ⇒ renamed to PRE_HOOK_ALWAYS_DENY to indicate valid assignment target.
      • VALIDATION_ALWAYS_ALLOW ⇒ renamed to RUNTIME_VALIDATION_ALWAYS_ALLOW to indicate valid assignment target.

Areas we’re still working on

  • Working on the final pieces of a reference implementation to be released in the coming weeks.
  • How the plugins should request access for and use native tokens.

PR: Pull Request #7796

3 Likes

are there implementations of std execute that consider the toggle for delegatecall if value is set to max uint, or otherwise, examples of providing both call and delegatecall in an account that conforms to 6900 (whether in the combined function mentioned above or separately)?

related somewhat, I think ERC6900 might recommend, but not require, the convenience of executeBatch in order to simplify its adoption by accounts. Accounts currently can implement a multicall method from libraries like Solady and OZ to batch execute as well as other ops. There are also multisend batching contracts (though this would encourage delegatecall which might be noted). The goal I think should be to maintain simpliciity, like ERC4337 (which only requires that accounts implement one function, validateUserOp). I am a fan of the execute interface and see this an overdue standardization :~).

Hi all, we are excited to share the reference implementation of ERC-6900 with you. The reference implementation reflects the most recent revisions to the spec around the plugin manifest and permissions. To maximize alignment between the spec and this implementation, we’ve also made some smaller changes to the spec itself. We describe these in the changelog below.

Publishing these changes is an opportunity to build on the conversations we’ve already been having with the community. Please take a look at the repo and leave comments and PRs or leave comments here!

We’re looking forward to building together!

Ref Implementation

The reference implementation is a draft implementation of ERC-6900. The reference implementation is an example to show how to build an ERC-6900 compliant modular account. It is NOT a production-ready implementation.

The implementation includes an upgradable modular account with two plugins (SingleOwnerPlugin and TokenReceiverPlugin). It is compliant with ERC-6900 with the latest updates.

Important Callouts

  • Not audited and should NOT be used in production.
  • Not optimized in both deployments and execution. We’ve explicitly removed some optimizations for reader comprehension.

ERC Changelog

Most of the changes to the ERC are for clarify and consistency, including

  • More sections are added like expected behavior for different Executors.
  • Struct and interface updates to be more efficient and consistent with reference implementation.
  • Naming changes for clarity and consistency, notably:
    • permittedCallHooksInjectedHooks in IPluginManager .
    • validatorvalidation / validationFunction in various places.
    • associationassociated in various places.
    • functionSelectorselector in various places.
    • ManifestFunctionTypeManifestAssociationFunctionType in plugin manifest.
3 Likes

@PixelCircuits Implementing ERC-7521 via a plugin, and making use of these hooks, is a good way to go about it. The specific handling of validateUserOp is needed for transaction validity switching, and we are looking into either making this modular switching step either apply to other functions too, or de-enshrining it’s handling, to better support other transaction flows in the future. Will definitely take this into consideration for that design research as well.

2 Likes

@z0r0z Thanks for the suggestions! A while back we made the call to not require accounts to support arbitrary external delegatecall s in the spec, and leave that choice up to implementation, to support a higher maximum security bar for ERC-6900 compliant accounts. Packaging it within the existing execute function using the maximum amount for the value param would technically break this spec, but it seems like a reasonable way to save some gas and code overhead, so it should be fine as an implementation choice. Wallet software for those accounts would just need to be aware of that behavior and display the appropriate warnings and safeguards.

Re: multicall, also something worth considering. Right now there’s one ERC-6900 specific reason to support executeBatch, and that is to avoid duplicating work with validation functions and hooks. Since those outer functions happen before the inner native function, naively doing batch executions only with a multicall and the execute function would result in duplicate ownership / validity checks for each call. With executeBatch, these calls happen once. For hooks that want to do per-execution checks as well, they’re given as parameters all of the requested calls to perform.

There might be a way to write a more custom multicall that allows for aggregation of these checks, but it would take some extra design effort to make sure none of the security invariants are broken. Will explore this idea a bit. Previously we’ve also considered dropping support for the non-batch execute as a way to simplify things, but people generally like the calldata savings.

1 Like

What do you mean by “transaction validity switching”? Is this a security concern the ERC-7521 validation function would run into too?

I’m referring to what I tried to explain previously here: ERC-6900: Modular Smart Contract Accounts and Plugins - #33 by adamegyed

An ERC-6900 modular account maintains a mapping from function selector → plugin address for execution functions. For validation functions, it also maintains this mapping, even though they all go through the single external function validateUserOp. So, to “switch” which validation function to actually run during the call to validateUserOp, it decodes the function selector from the first 4 bytes of userOp.callData, then does a mapping lookup, then runs the validation function.

If validateUserOp were itself implemented as a plugin’s execution function, it could work. However, it would then run into a problem, having to pick 1 of these 2 options:

  1. The plugin implements validateUserOp with a single “logical” validation function, like checking an owner’s ECDSA signature.
    • This limits the flexibility of modular accounts.
  2. The plugin itself has to re-implement modularity and do the same type of switching as what the account currently does.
    • This makes it harder to do account management and to enforce consistency guarantees.

So, to avoid either of these options, we chose to make validateUserOp a native account function with switching. But like you said, this also limits flexibility for other validity functions. So, I’m currently trying to find a way to possibly genericize the switching process to be able to apply it to other functions as well.

Hope this can be helpful, and let me know if there’s something else I can clarify.

2 Likes

So it’s thought that wallets will want different validation logic based on the calldata of the userOp? I’m confused because the reference implementation for 4337 has an example wallet that always uses the same validation logic. I’m also confused because the first 4 bytes of a userOp will almost always be the same (again, in the 4337 reference implementation , userOps are almost always calling the wallets “execute” of “executeBatch” function).

1 Like

the reference implementation for 4337 has an example wallet that always uses the same validation logic.

The example wallet from reference 4337 implementation is not meant to be Modular. It is just a SimpleAccount(.sol).

the first 4 bytes of a userOp will almost always be the same

Yes, it can be like this. However, in 6900 it is expected that there will be plenty of execution methods called on MSCA and then forwarded by the fallback function to the plug-in which contains the implementation of this method. So, the first 4 bytes of userOp.callData can be different from execute or executeBatch selector.

Has the team heard of a fancy use case where signature verification depends on the calldata?

Also, I can’t really see use cases for wanting to hit a function other than execute or executeBatch. Granted, those functions wouldn’t be able to do script like things that need memory, but that could be solved with executeDelegate and executeDelegateBatch calls. I would say that’s actually preferable to the alternative of requiring a user to install some script function to their wallet. The user can just elect to use scripts (via a delegate call) at sign time instead.

1 Like

fancy use case where signature verification depends on the calldata

For example, if there is a method performRecurringPayment(...) in the plugin,
This method can be called by anyone and we do not even need MSCA’s owner sig to validate such an userOp.
So if the userOp.callData[0:4] is performRecurringPayment.selector we want not just some other signature verification algorithm, but no signature verification at all. In this case validation function will verify has the period passed or not and maybe something else.

btw, I’m not a part of the 6900 team. They could have better use case examples.

1 Like

I think IPluginManager.installPlugin and IPluginManager.uninstallPlugin are not enough for the ERC-6900 spec.

Consider this situation, if an account has only one plugin. At the same time, this plugin is responsible for selector IPluginManager.installPlugin.

If I want to switch to a new plugin. Maybe I should uninstall the old plugin (IPluginManager. uninstallPlugin) and install the new plugin (IPluginManager.installPlugin).

Uninstalling the old plugin is fine. However, I cannot install new plugin through IPluginManager.installPlugin for there is no plugin responsible for the selector IPluginManager.installPlugin after uninstalling the old plugin.

I think maybe we can unify the interfaces like this.

function updatePlugin(
    UninstallPluginParams pluginToUninstall
    InstallPluginParams pluginToInstall,
)

If pluginToUninstall is empty, just install pluginToInstall.

If pluginToUninstall is not empty, uninstall pluginToUninstall first and then install pluginToInstall.

[details=“Summary”]
This text will be hidden
[/details[quote=“cejay, post:15, topic:13885, full:true”]
“EIP: Modular Smart Contract Accounts and Plugins” is generally feasible!

But I think there are several points that may need attention:

  1. add/remove Hook plugins may need to be divided into two parts:
    For example: 1. add plugin → 2. wait for a security time delay (e.g. 48 hours) → 3. user confirms that the plugin has been added.At least the implementation in soulwallet is like this, this is mainly for security reasons, for full context see the previous discussion with the author of ‘EIP-2535 Diamonds’ mudgen at here . So for us only PluginAdd function is not enough, at least a PluginPreAdd similar process is needed, and this process for monitoring purposes, will also emit event

  2. validateUserOp because it will be called at high frequency, so we need to consider the ‘gas efficiency’, in our consideration for the time being will be validateUserOp logic fixed in the contract (rather than through the plugin way to achieve), later if user need to upgrade the logic of validateUserOp, the user can update the ‘proxy contract’->‘logic contract’ address pointer(lower gas).

  3. I think MSCA implicitly aims to create a set of standards that any standard-compliant SCA can use any standard-compatible frontEnd (users don’t need to rely on ‘ONE’ frontEnd only), while the signature assembly and verification process often differs from one SCA to another (gas efficiency first|stability first|code readability first… etc.), I’m thinking that this area could be a major challenge for MSCA implementation.
    [/quote]

]