A proposal for modular smart contract accounts and account plugins, which allow for composable logic. This proposal is compliant with ERC-4337, and builds on the existing work from ERC-2535 when defining interfaces for updating and querying modular function implementations.
Please note, this standard is still in the draft state, and will likely be significantly amended with the input of the community.
A software standard is only as useful as its ability to coordinate the development of different parties. We hope this standard can coordinate plugin developers with account developers to unlock the full potential of account abstraction.
Thanks @adamegyed for initiating this EIP!
Having a standard for AA-enabled wallets is crucial, and it is impossible without having standard for the wallets themselves.
Iâve been working on the architecture of what you call Validation Plugins for the last several days.
I have a couple of questions:
When the function validateUserOp is called on an MSCA by the EntryPoint, it MUST find the user operation validator defined for the selector in userOp.callData
Where is it expected for MSCA to look for the operation validator address? do you suggest having some âselector=>validatorâ registry?
If weâre using userOp.callData, to look for the validator address, we wonât be able to forward signature validation flow for the EIP1271 flow, as isValidSignature only operates with hash and signature.
What do you think of adding the validator address to the signature?
let signatureWithValidatorAddress = ethers.utils.defaultAbiCoder.encode(
["bytes", "address"],
[signature, validatorAddress]
);
and then decode it at validateUserOp or _validateSignature.
That will allow MSCAs to verify signed messages for dapps with validation plugins.
This example above is for passing the signature validation flow to the plug-in (module) , however it is possible to validate not only the signature but the whole userOp (for the recurring payments and similar use cases) in the similar way
Is the idea that all non updatePlugins or validateUserOp functions are handled in a fallback?
Or would it be useful to have an execute function which handles routing to the appropriate plugin? Since we build on Safe there is already a fallback handler which could be edited, but it may be an unnecessary call.
Where is it expected for MSCA to look for the operation validator address? do you suggest having some âselector=>validatorâ registry?
MSCA must be able to find all functions (validation, execution, hooks) and their relationships in some form. For any incoming execution request, MSCA will need to look up the associated functions. Depends on the account implementation, a registry can be useful. Thatâs what I did in the sample implementation (coming soon).
If weâre using userOp.callData, to look for the validator address, we wonât be able to forward signature validation flow for the EIP1271 flow, as isValidSignature only operates with hash and signature.
Are you treating validateUserOp as an execution function and forwarding data that way? It is doable, but can be very messy.
For validateUserOp, the recommendation is that the account implements it directly, can calls to validation plugins to do the actual validation. Accounts passes userOp, userOpHash, and userOpValidator Plugin implementations can safely expect them as params.
Whenever you implement userOpValidator, make sure either implement an runtimeValidator that handles calls not directly from EntryPoint or disable that call path altogether.
Is the idea that all non updatePlugins or validateUserOp functions are handled in a fallback?
updatePlugins can certainly be handled in a fallback.
As for validateUserOp, as mentioned in the above comment, it is recommended to be handled on the account directly, though it is doable in the fallback (can be very messy).
Or would it be useful to have an execute function which handles routing to the appropriate plugin? Since we build on Safe there is already a fallback handler which could be edited, but it may be an unnecessary call.
The routing to appropriate plugins is expected to happen in the account level before the 4 steps starts.
What you may need to think with build on Safe are:
How can you add global hooks for execution functions?
How can you add custom validation and hooks for execution functions?
Are you treating validateUserOp as an execution function and forwarding data that way? It is doable, but can be very messy.
Nope, validateUserOp is not an execution function.
What Iâm trying to say, is that if we have a module that validates signatures via some alternative algorithm (say passkey module), we may want to sign messages to verify them via isValidSignature via same alternative algorithm.
In this case, we will need isValidSignature on the MSCA to understand where to forward this signature validation flow to, and it only can take it from signature.
I realize, that we can have different flows, like
For the AA flow we get the operation validator plug-in address from the callData
For the EIP-1271 flow we take get the operation validator plug-in address from the signature
Happy to see that discussions around this topic start. In my opinion it would have been nicer if the ERC would not enforce a specific proxy (e.g. currently the Diamond Proxy). The choice of a proxy depends on your use case and therefore a standard a standard that is agnostic to the proxy would be more widely applicable.
I would be interested in more details how hooks are designed. For example the Safe currently has a similar concept called Guards and it would be interesting to see the differences.
What do you think of adding the validator address to the signature?
Imo it should not be up to the signer, but the account what validator should be used. But this depends on the setup of the account (i.e. is there 1 address that fully controls the account, than there is no issue with this approach).
Totally agree with not enforcing a specific proxy pattern!
Imo it should not be up to the signer, but the account what validator should be used.
Yep, ideally it is the account that should decide, what validator to use.
However, if we consider, some SDK/dApp as a part of the account, it can decide what module address to append to the signature based on how this signature (or any other validation information) has been obtained from the signer.
It can be cheaper and easier than performing this search on-chain.
There is been some work around this also in conjunction with the Safe and the approach there was to enforce EIP-712 and then route the signing validation based on the domain separator.
When the function validateUserOp is called on an MSCA by the EntryPoint , it MUST find the user operation validator defined for the selector in userOp.callData
Also, does this mean that there is only one operation validator at a time defined for a given method of the Smart Account?
Letâs say, there is a method execute(address dest, uint256 value, bytes calldata func) that just calls an external smart contract on behalf of MSCA
Does it mean, that for all the calls with execute.selector in userOp.calldata there will be the same operation validator?
From how I see it, you could have different validators for the same executionSelector.
The ExecutionUpdate struct allows for an array of AssociatedFuntions, so based on some conditions in validateUserOp you could select which implAddress/implSelector you want.
I guess it depends on how the plugin information is stored⌠but like you asked above, I think Iâll go for some mappings in the MSCA. Could be worth standardising that storage into some IPluginManager interface so dapps could easily check if particular plugins are enabled
While the rhinestone team has been initiating open discourse about modularising ERC-4337 with multiple teams working in the space and we have been looking to push the space forward in a positive sum way, we feel that this EIP does not aim to start a conversation about this topic but is rather a way for the authors to further their own reputations.
The main reason we have for thinking this is that the EIP is clearly very heavily inspired by the work we did during the ETHDenver hackathon, yet fails to attribute or even mention our project (GitHub - kopy-kat/ethdenver-aa: Account Abstraction Project for ETHDenver). During the hackathon we built the (to our knowledge) first implementation of ERC-4337 using the Diamond Proxy Standard.
It is obvious that this EIP takes inspiration from our code for several reasons, most notably because it exactly follows our peculiar naming convention exactly and repeats many of the errors and security vulnerabilities present in our proof of concept.
Unlike Safe, who call their extensions modules, or ERC-2535, who call them facets, this EIP has chosen to call them plugins, naming that we hadnât seen in solidity before we chose it. Further, while the extensions are called plugins, the functions that call them are called hooks, a name inspired by frontend libraries and that is completely absent in other solidity contracts today. Further, our names for the specific hooks, such as preExecution and postExecution have equally been retained exactly by the authors.
Unlike ERC-2535, which stores function selectors in a mapping, this EIP has chosen to use arrays, something that we had done during the hackathon but is, on second thought, a very suboptimal implementation. Perhaps most interestingly, during the hackathon we were unable to finish testing our implementations of the hooks, so they are left blank on the public GitHub repo. It seems that rather than trying to figure out our intentions and planned implementation of these hooks, the authors just left them out of the EIP, perhaps hoping for others to tell them how to implement these.
Since we have been working on improving the code we had written during ETHDenver over the last month, there are many design choices that we have now revisited in order to make a modular implementation of ERC-4337 using diamond Proxies viable to be used in production. As stated above, we are happy to share our progress in public in order to propel the space forward and have already done so with select teams. However, because we are not yet confident in the security guarantees of our code, we have for now kept most of it internal and are planning on releasing it, together with detailed explanations of our design choices and extensive testing, at a later date.
Due to the numerous âcoincidentalâ similarities to our code, examples of which were given above, but the clear lack of attribution or even mention of the rhinestone project, we feel like this EIP does not aim to create and improve public discourse around modular ERC-4337 in a positive-sun way and hence we will abstain from commenting and pointing out the numerous flaws that we have since discovered in our original code, most of which have been repeated in this EIP.
We want to quickly address the above comment, which weâve attempted to mediate directly, in a public comment and then move towards focusing discussions on technical criteria and improvements.
Our motivation for creating this EIP is not to âfurther our reputationsâ and indeed an EIP is not a vehicle to do that. We see contract accounts as an absolute requirement to get 1B people on crypto rails, and to that extent weâre pushing things forward where we can, including standards, open source software, and developer products. Standards specifically streamline the ecosystem, maximize developer leverage, minimize fragmentation and developer lock-in, and allow more cohesion in moving the space forward.
We were actually entirely unaware of Rhinestone when publishing. We reached out to engage the Rhinestone team as soon as a community member flagged the similarities. Itâs actually cool to see convergence in design here. That generally means as a collective group weâre trending in the right direction. That said, similarities are a large step from copies - we demonstrated live to the Rhinestone team the history of how we landed at this conclusion after months of iteration. Nomenclature like âpluginsâ and âpreExecutionâ are not unique to either this standard or Rhinestoneâs implementation and are canonical and intuitive terms to capture the intended meaning.
The reason we moved our internal discussions into a public forum is to engage in the discourse and discussions, such as the ones from @rmeissner, @fmc, and @jamesmccomish, that forces us to either justify our design decisions or incorporate feedback and adapt the design. To that point, all decisions in the standard now have been prototyped and battle tested internally, and we have not seen counter arguments to standard specifications - including when we presented these âerror ladenâ decisions to the Rhinestone team.
Weâd been hoping to reach a resolution directly with the Rhinestone team, but unfortunately the asks they were making were not appropriate for this EIP or any other. Specifically, the two asks are:
a) Calling out Rhinestone in the EIP abstract. EIPs are not a vehicle for products or teams to gain relative standing or distribution. This is a conscious decision to maintain neutrality in standards for an open decentralized platform, and to that extent Alchemy is not mentioned a single time in the EIP, and we reference examples we researched in design where appropriate in the context of the specification.
b) Adding a Rhinestone author to the EIP authorset. We plan on collaboratively defining a set of criteria for expanding the author set that we can apply equally to everyone. Weâll share those in the TG facilitating these discussions next week, but canât create special cases. Thatâs not to say Rhinestone doesnât or wonât eventually qualify based on these criteria but we need a system that is uniform and applies equally to everyone in the community based on contributions and body of work. Working on parallel implementations that result in similarities to the standard likely donât suffice and arenât sufficient constraints on growing the authorset responsibly.
Taking a step back here: the goal is to provide an open standard that streamlines development and maximizes outcomes for the ecosystem. The goal here is not to assume individual ownership of this standard, use it to promote teams or products, or otherwise bias it towards a particular entity. To make sure that weâre able to do this, we need to enforce an equal process for each stakeholder and a conscious mitigation of any brand and product associations.The TL;DR of this situation:
We unequivocally did not copy work, and demonstrated our methodology to the Rhinestone team.
To that point this work is now in the public domain and should be owned by the ecosystem.
This is not a zero sum game, and the most outsized outcome will be a result of everyone working together, agreeing on scope, charter, and process, and pushing this forward in a timely manner.
This is not a vehicle for products or teams to gain standing or distribution.
Moving forward, letâs keep the discussions on Eth Magicians and similar public forums focused on technical discussions. Thereâs an open Telegram group as well to facilitate higher bandwidth conversation, per suggestion from the Rhinestone team. Weâre always happy and available to chat with any teams directly on other matters.
I fully agree with your point that this discussion should be reserved for comments on this EIP, so I think it makes sense to leave it at this. However, we also donât want any misrepresentations to hang around so I just wanted to briefly clarify a few points:
Primarily, the asks that you mention were in fact not our asks, but your suggestion on the call with myself, you (Noam), Adam and Fangting (all from the Alchemy team) on Friday April 21. After discussing your proposal to add me as co-author and referencing our code from ETHDenver in exchange for removing my comment above, my team agreed to this and informed you over the weekend. After this you changed your mind, said this was no longer an option and followed up with this comment.
My understanding was that our discussions were centred around collaboration and clearing the air between us, not promoting rhinestone, so this comment is disappointing.
We are committed to building modular AA in the open and ensuring it is valuable to all in the ecosystem (as seen in the telegram conversations Telegram: Join Group Chat). However, as I pointed out on the call, we do not think that it is beneficial for the ecosystem to try to enshrine a specific implementation of modular AA as a standard, but we should rather aim to standardise interfaces for modules and how they interact with smart accounts, allowing for a diverse set of implementations to exist. We have initiated discussion about this in the group chat and will move some of the arguments into this forum should that be useful in the future.
I looked at the standard and in general I like it and I think it is a great idea. In order to leverage the EIP-2535 Diamonds community and its tooling, interoperability and documentation I think it would be great, if possible, to make the smart contract accounts compliant with EIP-2535 Diamonds.
Making it compliant with EIP-2535 Diamonds may be easier than first considered.
The upgrade function diamondCut specified in EIP-2535 Diamonds is optional so smart contract accounts would not have to implement that and the standard allows other custom or standardized upgrade functions to be used like updatePlugins.
The EIP-2535 Diamonds standard only requires that the DiamondCut event be emitted and the four IDiamondLoupe read-only functions be implemented. The standard requires these for interoperability and interfacing with tooling and other software, for example louper.dev. A great article on EIP-2535 Diamond compliance is here: Compliance with EIP-2535 Diamonds Standard
It seems possible to me that a smart contract account could implement the IPluginLoupe interface and IPluginUpdate interface other interfaces defined by the smart contract account EIP as well as implement the IDiamondLoupe interface and emit the DiamondCut event when adding/replacing/removing functions.
I would love for EIP-2535 Diamonds and this smart contract accounts EIP to work together.
âEIP: Modular Smart Contract Accounts and Pluginsâ is generally feasible!
But I think there are several points that may need attention:
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
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).
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.
Hi everyone, Iâm happy to check in on this thread and provide an update on this proposalâs progress. There is now an updated and merged draft of the proposal on the main EIP site:
The current draft is a response to the helpful and engaged dialogue happening here and on the related telegram channel. The feedback so far has pointed to some concerns about the the multi-facet proxy approach and interfaces, particularly in the context of where storage can feasibly reside. Weâve revised the EIP to be more explicitly agnostic in its approach to proxies and interfaces â this version allows for (but no longer requires) ERC-2535 style âdelegatecallâ operations as the basis for execution.
This change has had a number of implications for security, interoperability, plugin management, and other issues that weâre currently exploring through research and development. In particular, weâre currently working on a library to address some of the storage limitations for plugins when using call. (In more detail, this library would aid in storing bulk data within an accountâs associated storage, allowing for ânestedâ mappings and dynamic arrays). We are also working on benchmarking some of the implementation options allowed by the new spec, and on developing a reference implementation. We look forward to sharing the results of that work in the coming weeks.
In the meantime, weâd be very grateful for your perspectives on some key unanswered questions that weâre also working on. First, interfaces: the changes weâve made to IPluginUpdate.sol and IPluginLoupe.sol have made them more flexible, but with some tradeoffs in complexity. Are there implications for this approach that we havenât yet discussed? We would appreciate your feedback in either implementing an account or writing plugins for modular accounts.
Second, changing the interaction flow from accounts to plugins to use call instead of delegatecall requires both standardizing execution and explicitly allowing for multiple validator functions on the same execution function. These are additional changes to the spec that weâre considering, while also weighing the costs of added complexity. Thank you to @fmc for the suggestion on self-identifying signatures, that strategy (or a similar one in calldata) can provide the flexibility needed to adopt these changes. We would appreciate any additional insight into what baseline features execute function(s) should have.
Also, at this time Iâd like to introduce @JasonW to the ERC-6900 author team. He will be helping with research and organization, making sure this standard is the best it can be and enabling community members to become contributors.
Excited to hear about the library that addresses storage limitations for plugins when using call! Let me know if you need any assistance with r&d of it
must of discussion at the link above. In terms of adding and removing plugins, we think itâs slightly different. We think itâs more important for the user to have the wallet address âforeverâ, but plugins pose a security risk, for example, if a hacker steals your private key, can the hacker modify the storage by adding a plugin ( This would make the social recovery function unavailable), and we are trying to make the social recovery always available if the user is concerned
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:
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!