-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: [v0.8-develop, experimental] multi validation in user op signature [5/N] #62
Conversation
c83ac72
to
e871784
Compare
41b30e7
to
f347833
Compare
77ce9ff
to
f48ccd7
Compare
aefbc5f
to
6d48a68
Compare
taking a look! thoughts on adding something to the loupe to expose |
Overall this looks good! Some thoughts/q's:
a. security related - calls from plugins are less dangerous, e.g. account doing token.transfer vs plugin doing token.transfer, and installing a validation/prevalidation function stack on a new execution function is also less dangerous Curious if there are more reasons here |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lgtm.
This got me thinking: are we comfortable with the idea of providing a single validation function for an executeBatch
? I know we had toyed around with the idea of being able to specify a different one for each in the past.
if (_selectorData.validation.notEmpty()) { | ||
// Fail on duplicate definitions - otherwise dependencies could shadow non-depdency | ||
// validation functions, leading to partial uninstalls. | ||
if (!_selectorData.validations.add(toSetValue(validationFunction))) { | ||
revert ValidationFunctionAlreadySet(selector, validationFunction); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should come back this naming problem (validation vs. validation function vs. ?) later. Same with isPublic
.
if (_selectorData.validation.notEmpty()) { | ||
// Fail on duplicate definitions - otherwise dependencies could shadow non-depdency | ||
// validation functions, leading to partial uninstalls. | ||
if (!_selectorData.validations.add(toSetValue(validationFunction))) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I imagine we'd want to revisit this if we end up allowing multiple installations of the same plugin with different permissions? Still maybe makes sense to check for duplicates but I guess the key might include the permissions.
cc @howydev
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validation routing is done off-chain so definitely worth exploring moving execution selector routing off-chain too!
Re: session keys, i think the decision is between plugin + 1-key-per-plugin (or multiple keys that are managed by a dapp) v.s. 1 plugin + n keys per plugin?
- I do prefer the security model of 1 key per plugin since we can use the account's permissions framework directly. That's preferable for 2 reasons - we don't need to trust the plugin to manage individual key permissions, and we dedupe permissions checking (in the other model, account allocates a global limit to the plugin, then the plugin allocates per-key limits). This plugin could be as lightweight as a signature validator if the native permissions framework is sufficiently granular/flexible
- But in the 1-key-per-plugin model, we'll potentially have to deploy a new contract to add a new key (probably a proxy from a ProxyFactory, which is the safe/zodiac modules model). Unless the deployed plugin is ultra lightweight it should be more costly than the other approach
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah we definitely need some changes to support that workflow, but I wanted to keep each change scoped down to just what is needed per PR.
Also, I think having 1 key per plugin is too inefficient - we can treat the function id as a per-account, per-key item to perform that switching, like in the validator experiments.
"Key" is also the wrong term here, it's leaking the underlying validation logic into a higher layer of abstraction. They're really independent "validation functions" or "validators".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I think having 1 key per plugin is too inefficient - we can treat the function id as a per-account, per-key item to perform that switching, like in the validator experiments.
Hmm I might be missing something, but here's an example I was thinking about - in the case of adding a new session key to the existing MA session key plugin with n
permissions, the minimal state change required will be:
n
x nonzero to nonzeroSSTORE
s via increment the ERC20 + native token spend limits from the account to the pluginn
x zero to nonzeroSSTORE
s via incrementing the ERC20 + native token spend limits of the plugin to the session key
Whereas deploying a new validator would be just n
x zero to nonzero SSTORE
s via incrementing the ERC20 + native token spend limits of the account to the new validator
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know in the old v0.7 model, we basically needed two layers of permissions - account <> plugin, and in the case of a key-based validation plugin, then permissions for each key.
If we switch to composable validation (aka, allow validation plugins to be installed multiple times), then we go back to just needing 1 set of permissions per "validation", which is a combination of both the address of the validator plugin and the ID of the key within it.
I think the model of doing proxies on plugins per account is wrong - the cost of any contract creation is already too. high (32k), not to mention the added forwarding cost of being a proxy.
if (_storage.selectorData[msg.sig].denyExecutionCount > 0) { | ||
revert AlwaysDenyRule(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kinda want to revisit naming of AlwaysDenyRule and denyExecutionCount too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or we can get rid of the magic values in #63, if that is finalized
@@ -262,6 +262,41 @@ contract UpgradeableModularAccount is | |||
return returnData; | |||
} | |||
|
|||
/// @inheritdoc IPluginExecutor | |||
function executeWithAuthorization(bytes calldata data, bytes calldata authorization) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again re: naming (we can punt) - this is still (runtime) validation, so a bit surprising to introduce new vocabulary authorization
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah happy to revisit. For some background on why I started with this, I saw this field as analogous to the "signature" data in user ops, but we've overloaded that term in too many places already (including 1271), and it's just the authorization for one particular action.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great! Would this also unlock the support for runtime multisig?
{ | ||
bytes4 execSelector = bytes4(data[0:4]); | ||
|
||
FunctionReference runtimeValidationFunction = FunctionReference.wrap(bytes21(authorization[0:21])); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: do we allow invalid runtimeValidationFunction
? If not, should we consider validating here before reading into storage?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please explain your question a bit more? The workflow here is currently:
- The requested validation function is read from the data in
authorization
. - The account checks to make sure that this validation function is actually installed.
- The account checks that the validation function applies to the selector this call is trying to run.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah I just meant function references like bytes21(0)
might not be valid, so we can probably fail fast here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see, since that step will fail anyways on the storage check later I don't know if it's worth failing early - it reduces gas in the failure case but increases gas in the success case, and generally I think optimizing for the success case is better.
payable | ||
returns (bytes memory) | ||
{ | ||
bytes4 execSelector = bytes4(data[0:4]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto on validity check
/// @param data The calldata to send to the account. | ||
/// @param authorization The authorization data to use for the call. The first 21 bytes specifies which runtime | ||
/// validation to use, and the rest is sent as a parameter to runtime validation. | ||
function executeWithAuthorization(bytes calldata data, bytes calldata authorization) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we also need to update https://github.com/erc6900/reference-implementation/blob/6d48a68021bff6f3a50a508873595af9fb1939a6/standard/ERCs/erc-6900.md in this PR or later on
(update: okay, I see it's moved to different interface in #65)
@huaweigu sorry I missed the earlier comment - yes this does make it possible to have a runtime path multisig. The signatures (or any other data related to authorization) can be put in the |
@howydev just saw this
Yeah that definitely makes sense! we should revisit loupe functions overall at the end of the major spec changes, this and probably more might need to change. |
Motivation
As described in erc6900/resources#4, supporting multiple validation functions has been a long-standing goal of ERC-6900.
Solution
This PR implements switching between different validation functions using a field in the user op signature for user op validation, and a calldata field for runtime validation.
It also lays the groundwork for, but does not implement, associating pre-validation hooks with validators, and per-hook/per-validation auth data (erc6900/resources#32).
To avoid needing a "default" or "primary" validation per selector, like in #52, this PR also changes the default auth flow through accounts. Previously, there were three validation flows through the account:
validateUserOp
called by EntryPointexecuteFromPlugin
This PR keeps the user op validation flow, and reorganizes the auth flows of the latter two:
executeWithAuthrization
Note that the
isPublic
property of an execution function, introduced earlier in the stack at #61, is still applied when calling functions through the fallback and native function paths.By making this organizational change, this solution does not require a "default" or "primary" validation per selector. Whenever a validation function needs to run, it is specified by the caller and checked by the account to be an installed validator for the given function.
Future work
The validation encoded in the user op signature (currently 21 bytes) would easily fit within the key portion of the user op nonce (24 bytes). However, due to potential future changes to how we identify validation functions likely coming with composable validation (erc6900/resources#36) and user-supplied install configs (erc6900/resources#9), I've avoided making that optimization for the time being. We should revisit the possibility of optimizing this once those are addressed.
Additionally, this PR does not make changes to pre-validation hook associations, which I believe multi-validation makes necessary. See #52 for more info on this.