Very happy to hear about the pivot from delegatecall to call.
Can’t wait get my hands dirty with the Plugin Storage lib!
Meanwhile, msged you on Telegram regarding contributing to the proposal.
Thanks @adamegyed for worth proposition!
This standardization of modular architecture is a very important proposition, given the variety of wallet providers that will enter the wallet market in the future.
BACKGROUND
We’re building a borrow/lend application that requests to prohibit execution transfer()
and related modules, setApprovalForAll()
, approve()
and burn()
methods to borrower’s wallet.
Those prohibit logic was implement into Safe’s guard contract so that borrower doesn’t be required to deploy each app-specific contract wallets but they are only required to apply guard to their wallet.
- At ETHGlobal Istanbul, Pinky team also built an application which implement prohibited logic into guard contract.
- If the each application provide own “app-specific” wallet, Users has to deploy each wallet that they want to use.
Following above, I could say that, from application developer side of view, this topic is much important for usability and account based identity contexts.
I draw a shape of composability like below (from this tweet).
STATUS QUO
The safe’s guard can be bypassed by module logic and this issue was flagged couple years ago and Safe V2 will implement “global guard” (details are unknown).
IMO, the reason why they didn’t support it at V1 that the Safe designed their architecture for organizations who is motivated to manage treasury by Multi-Sig so module would be setup by member of HQ, not setup by outside application.
OPINION
So, standing to above problem, I think the global preHook
also need to implement logic below:
- With Opt-In, global
preHook
enables applications to prohibit transactions to wallet user. - User can’t disable global
preHook
without application permission.
*In case of our application, if borrower will return NFT from their wallet, wallet user could disable globalpreHook
.
When updatePlugins
is called with PluginAction.REPLACE
or PluginAction.REMOVE
, the calls MUST be validated by preHook and postHook.
function updatePlugins(PluginUpdate[] memory pluginUpdates, address init, bytes calldata callData) external;
function updateGlobalPlugins(GlobalPluginUpdate[] memory globalPluginUpdates, address init, bytes calldata callData)
external;
I’d like to discuss two issues and suggest a model that might solve them:
1. The use of DELEGATECALL
.
While the latest version of the ERC doesn’t require DELEGATECALL
, at least one operation still does:
The function
updatePlugins
takes in arrays of execution updates, hook updates, and hook group updates to perform. It also takes in an optional initialization function. The function MUST perform the update operation sequentially, then, if the address provided ininit
is notaddress(0)
, MUST executeinit
with the calldatacallData
through adelegatecall
.
Using DELEGATECALL
for init is a risky default. We may need DELEGATECALL
for some use cases but CALL
should also be supported and used by default. Therefore an ExecutionMode
should be specified for init.
2. The need to enforce permissions in the pre-validation hooks, based on calldata alone.
It is hard to enforce permissions in pre-validation functions if the subsequent function runs in the context of the account itself. Real enforcement must happen inline. An alternative is to run the functions themselves outside the account context and provide callbacks that enforce permissions.
Permissions model
Plugins have unlimited power, especially if they use DELEGATECALL. Permissions can’t really be enforced in pre-validation if the plugin operates in the context of the account itself.
This unstructured access This makes auditing and formal verification of plugin challenging. This might limit the growth of the plugins ecosystem.
I’d like to propose a permissions system loosely modeled after Android’s:
The plugin declares its requested permissions, as well as the components it has:
- Initialization function and how it should be called (
CALL
/DELEGATECALL
) - Hooks it needs to use and how they should be called (
CALL
/DELEGATECALL
) - Execution functions it adds to the account.
It may also declare custom permissions defined by the plugin itself, which other plugins may request in order to interact with this plugin in certain ways.
This definition is similar to AndroidManifest.xml
which every Android app includes. It should be returned by a pure
function by the plugin, e.g. getManifest()
, and the wallet would communicate it to the user before accepting it. The account will also use it for setting things up when adding/updating the plugin.
Here’s what the permissions might look like:
The default
By default (if the plugin doesn’t ask for any permissions), no DELEGATECALL
, no special permissions in any account callbacks, the plugin is not trusted by the account beyond just calling the requested functions (CALL
only). Some plugins may be able to work without any permissions.
For example, a spending-limit guard could be implemented using just a pre-execution hook which checks the current balance and returns it as a context
, and a post-execution hook that reverts if the balance change is over the limit. Users will be able to add it with little risk because it won’t ask for any scary permissions.
DELEGATECALL permission
Requiring ongoing DELEGATECALL
is the riskiest permission and should be avoided unless there is no other way to achieve the same functionality. This is equivalent to Android’s SYSTEM
permission and would be used only by the most trusted plugins.
It might make sense for account devs to prevent users from adding such a plugin unless they explicitly whitelisted it, to prevent users from getting scammed. This is how Android handles apps that require SYSTEM
- they must be signed by the manufacturer or by Google.
DELEGATECALL permission only during init
DELEGATECALL
only during init is a bit safer since it’s easier to verify what it does, so it is considered a separate permission. But should also be avoided when possible. It might make sense to also treat it as a SYSTEM
permission that requires whitelisting.
Specific storage permission
Specific-slot permissions can be given through setter/getter functions in the account, allowing plugins to access only certain slots. Similarly, the account may provide permissioned setters for certain mappings, arrays and structs. Specific-storage permission could allow a plugin to manage certain aspects of the account without giving up full control.
For example, a plugin that adds social recovery capability would need access to manage guardians and to set the account’s control key, but it doesn’t need access to manage modules or to transact on behalf of the account. It would ask for a PERMISSION.SET_KEY
permission that allows it to call a callback that sets the key, and that’s it.
Per-plugin storage
A per-plugin getter/setter can be used for the plugin storage itself, providing unrestricted access to a mapping indexed by msg.sender. Each plugin has implicit permission to its own storage.
If the plugin is the only one that uses this mapping, it may be redundant since the plugin could use account-associated-storage in its own storage to store its data, but it will be useful for cross-plugin storage since it will be stored in the account itself and managed by the permissions system. Plugin1 may maintain a mapping in the account and define a PERMISSION.Plugin1.storage_read
. Plugin2 will request that permission and be able to access that mapping as well. It enables splitting plugins to multiple modules with different permissions.
In the social recovery plugin example, there may be a complex module that manages the guardians and offers permissioned read access to the guardians. And then a very simple module that verifies guardian signatures according to the stored configuration, and sets the key. The complex module has no permissions (but declares a custom permission), and the simple module requests two permissions: PERMISSION.SET_KEY
and PERMISSION.Plugin1.storage_read
.
Execution from plugin
PERMISSION.execute_from_plugin
, enabling calls to an executeFromPlugin
function, similar to Safe’s executeFromModule
. It allows the plugin to perform specific operations on behalf of the account without modifying its storage. E.g. send eth or transfer an ERC20 token.
Inside the account’s executeFromPlugin
it is possible to enforce granular permissions for each plugin, e.g. the plugin can only transact with a specified contract. A plugin that create pre-execution hooks may declare custom permission such as PERMISSION.transact_with_contract_X
and get called from executeFromPlugin
before execution, where it’ll be able to check if the calling plugin has the permission to transact with contract X (or with all contracts). And a plugin that adds an execution function would only need to ask for the permission to transact with the contract it needs.
For example, a game plugin might ask for permission to transact with the game contract (and no other permissions). Users will be able to add it without worrying that it might steal assets.
Custom permissions
The account (or any of the plugins) may add custom permissions, similarly to how Android apps can create custom permissions. These permissions are not a part of the ERC but they are defined in the context of the account, the wallet knows how to interpret them to the user, and plugins may request these permissions when available.
Popular custom permissions may be standardized later if they are used by multiple account implementations and plugins.
PluginManifest
The model above requires the plugin to expose a structure, similar to AndroidManifest.xml
to define the plugin’s required permissions, hooks with their execution modes, execution functions, etc. It should also have a protocol version to allow future upgrade, and a list of optional capabilities, e.g. declare that it only supports an account that implements a certain ERC beyond just 6900.
The manifest may also specify dependencies on other plugins. E.g. if PluginA adds a new execution function and PluginB uses it, then PluginB’s manifest would require PluginA. The wallet will detect this and install them together as needed (with the user’s approval).
The plugin’s getManifest()
will return the manifest, RLP-encoded.
The update function could be replaced with an installation function that calls the plugin’s getManifest()
. It’ll then install the plugin (unless it requires capabilities or a version that the account doesn’t have), grant the needed permissions, configure hooks, etc.
The wallet will first show the user the manifest and explain the meaning of the requested permissions. The user will be asked to sign a installPlugin(pluginAddress,manifestHash)
transaction. The manifestHash is included to prevent the plugin from baiting and switching the requested permissions.
Example - social recovery plugin
Manager plugin doesn’t ask for permissions, only declares a permission and a few execution functions. All of them require the validation function ownerOnly
which validates the owner’s key.
<manifest plugin="socialrecovery.manager" version:"1">
<permission name="PERMISSION.socialrecovery.READ">
<execution name="addGuardian(address)" validation="ownerOnly">
<execution name="removeGuardian(address)" validation="ownerOnly">
<execution name="setRecoveryPolicy(PolicyStruct)" validation="ownerOnly">
</manifest>
Recovery plugin asks for the SET_KEY
permission. It also adds a validation function that validates the guardian signatures (so it needs the socialrecovery.READ
permission defined by the manager module). And an execution function that sets the key, validated by the guardians validation function
<manifest plugin="socialrecovery.recovery" version:"1">
<uses-permission name="PERMISSION.SET_KEY">
<uses-permission name="PERMISSION.socialrecovery.READ">
<validation name="validateGuardians">
<execution name="setKeyByGuardians(bytes[])" validation="validateGuardians">
</manifest>
We’re not really going to use the XML format on-chain. It’ll be a more efficient RLP representation of this information. We should also optimize this model to better suit the ERC’s need. In this post I deliberately followed the Android model to show how it could work, but we can deviate from it when it makes sense.
Misc
Aside from the permission model and installation method, I’d like to propose another minor change:
In Calls to execution functions, If no corresponding plugin is found, the MSCA MUST revert.
makes it incompatible with Safe fallback modules. Maybe we could consider supporting a “default plugin” with an associated validation function. Fallback modules built for Safe could then be compatible with this ERC with little or no changes.
Yes, I completely agree with what Yoav said:
-
For modular wallets, plugin is not enough. This is the solution we implemented (a detailed document will be released by David in a few days):
- Plugin is a collection of hooks.
- Because the Pre-UserOpValidate hook, plugins can only access associated storage. To address the issue of dynamic storage for plugins, we introduced
Plugin storage
(plugins can read and write the wallet’s storage, but each plugin has its own separate storage).
2. There is a significant use case: 2FA. We introduced out of signature data in the Pre-UserOpValidate hook to address this. Even if the data are outside the signature, data security can still be ensured easily (【1】 【2】). To avoid conflicts when different plugins parse guardData, the wallet must parse ExtData and pass it to the respective plugin based on its needs.
-
Module is an external contract authorized to execute a specific list of functions.
-
As Yoav mentioned, a permissions model needs to be defined. In our implementation, we require the Permission list to be pure [module] pure [plugin]. However, this cannot solve the problem (as it is actually invoked using staticcall on chain) unless the contract source code is audited to ensure “pure” functions.
-
Yoav suggest can use sstore2, and Dror suggest can use Bundler.UserOpValidate.simulate. All reasonable. However , taking into account security and simplicity of implementation, I suggest using an
Request -> Approval Model
:-
Plugins have a permission request manifest. When users add a plugin, they need to input the corresponding permission manifest code. This ensures that all necessary permissions are granted, considering that if only certain hooks are allowed, the plugin may not function properly.
-
Modules have a permission request manifest. When users add a module, they need to input a list of permissions (
bytes4[]
) that the user allows. This allows customization of permissions based on user requirements.
-
-
-
I suggest removing ExecutionMode.DELEGATECALL because it poses unresolved security risks that cannot be mitigated.
- In our current implementation, only the
upgradeTo(address)
function ( in UpgradeManager.sol) can access delegatecall, but this function is accessible to any authorized module:- Users have the freedom to choose their wallet implementation (with a 48-hour waiting period).
- In case of any big issues, we can add our new version of the implementation to the whitelist, allowing users manual to upgrade immediately.
- [context: by default, users trust our whitelist, and only modules in the whitelist can be added immediately. Other modules need to wait for 48 hours to take effect.]
- In our current implementation, only the
Looking forward to the author’s reply.
Hi @in2xx !
To answer your question directly, updatePlugins
and other functions defined directly on the account (as opposed to defiined via plugin execution functions) still run hooks. It is possible to design, under the current architecture, a custom validation function and/or preExec hook to enforce that type of check. On a standards-compliant account, there should be no way to bypass hooks.
Thank you for your detailed feedback. We think the plugin manifest - some form of a self-descriptive view function that describes what functions a plugin provides and how they can be added to the account - makes a lot of sense. For the execution-related permissions, we’re generally leaning towards removing mandatory delegatecall
usage entirely from the specification, so accounts that implement such features do so at their own risk.
For the “exec from plugin” workflow, we hear you and realize that runtime validators are not a good enough solution on their own. So, we’ll take a look at how we can adapt the spec and the way it represents associations to support this use case. Hopefully, sharing the reference implementation we’ve been working on can provide clarity in this area too.
Let’s wait on the the account storage API for now, and see how plugins can rely on existing tools to perform cross-plugin reads and writes. These include functions defined in the account fallback, and on the upcoming “exec from plugin” function, which can allow specific plugins to view and edit the data of other plugins. If we see the direct shared storage become a critical feature in plugin development, we can choose to add it to this spec. In the meantime, because the account storage API is defined as an interface on the account, it can be represented as a “required capability” of a plugin that needs it in its plugin manifest.
Hi @in2xx,
To answer your question directly, updatePlugins
and other functions defined directly on the account (as opposed to via execution functions) still run hooks. It is possible to design, under the current architecture, custom validation and execution functions that enforce that type of check. On a standards-compliant account, there should be no way to bypass hooks.
There’s also a small batch of ERC changes that we’re getting ready to merge soon. Here’s what’s included and what’s planned for future work:
ERC Changelog:
- Removed the execution mode enum from
IStandardExecutor
. Now, it is only expected to use call.- Non-standard native account functions that use other call modes, such as
delegatecall
, are not prohibited by the spec.
- Non-standard native account functions that use other call modes, such as
- Changed plugin init operation to an array of
call
operations instead of adelegatecall
. - All plugin function signatures have been collected in one spec section for clarity.
- Pre user op validation functions now return a packed time bound, which is intersected with the validator-returned value.
- Specified behavior for natively-defined functions on the account.
- Specified operation order for
updatePlugins
(pending plugin function updates do not apply until after execution completes - i.e. after all postExec hooks return and the call frame is complete. - Updated rationale section, fixed a typo, and updated the two call paths diagram.
Areas we’re still working on
- How to use a plugin manifest to replace the declaration of individual functions within
updatePlugins
.- This would also replace the current usage of hook groups.
- How to expose and control the ordering of hooks by the user.
- How to specify default fallback functions via the plugin update interface
- How to provide
msg.sender
andvalue
as a meta-context to execution functions. - Possible hook consolidation, especially as these type declarations are growing to be similar.
PR: Update ERC-6900: Spec update 2 by adam-alchemy · Pull Request #7247 · ethereum/EIPs · GitHub
Thank you all for your feedback!
I agree, that this is the only way for plugins to access MSCA’s storage that makes sense.
The purpose of Smart Accounts is to “store” and manage assets.
Since SA is a wallet, all operations made by SA should be managing the assets, and for this, there’s no need of using the SA’s storage.
IMO, Modular Smart Account should only use its own storage to store information about modules. And do it in the safest way to avoid situations, when SA can be upgraded to use another logic that can work in an unexpected way with an existing storage.
Thus, if as Adam mentioned we see a strong need for a direct shared storage, mapping indexed by module address could be the most straightforward and long-term oriented way of implementing it.
Overall looks excellent! This is very comprehensive, so much sothat it might be beneficial to consider a phased approach or provide more detailed guidance and support to facilitate its integration amongst the community.
Maybe this EIP should just focus on the hooks, then once those are finalized, more EIPs can build on top to eventually reach the current scope of this design. I think even just the hooks have a lot of nuance, and reminds me of iptables. Ensuring those are well thought through is critical.
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 theIPluginManager
interface.- The function
updatePlugins
has been replaced by the functionsinstallPlugin
anduninstallPlugin
.
- The function
- Updated the
IPluginLoupe
interface to reflect the above changes. - Added the
IPluginExecutor
interface, which introduces new functionsexecuteFromPlugin
andexecuteFromPluginExternal
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 theIPlugin
interface to maintain the integrity of account and plugin configuration. - Added a section describing the plugin manifest, and the expected behavior for
installPlugin
anduninstallPlugin
. - 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
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!
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 forpermittedExternalCalls
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
anduninstallPlugin
- 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:
pluginExecutionHooks
⇒permittedCallHooks
- 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 decodingcalldata
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.
- The spec is now more open-ended in implementation choices for how accounts should maintain consistency of installations, but requires that consistency is enforced.
- 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 toPRE_HOOK_ALWAYS_DENY
to indicate valid assignment target.VALIDATION_ALWAYS_ALLOW
⇒ renamed toRUNTIME_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
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:
permittedCallHooks
⇒InjectedHooks
inIPluginManager
.validator
⇒validation
/validationFunction
in various places.association
⇒associated
in various places.functionSelector
⇒selector
in various places.ManifestFunctionType
⇒ManifestAssociationFunctionType
in plugin manifest.
@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.
@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.