-
Notifications
You must be signed in to change notification settings - Fork 29
feat: [v0.8-develop] account self call restrictions #78
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
Conversation
2f3cb82 to
2ba7da9
Compare
| (address target,,) = abi.decode(callData[4:], (address, uint256, bytes)); | ||
|
|
||
| if (target == address(this)) { | ||
| // There is no point to call `execute` to recurse exactly once - this is equivalent to just having |
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 effectively prevents execute make self-call, right?
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 think so, because there isn't really a scenario where execute() would do a single self call except for attempted shenanigans, because the call could just be executed regularly too
|
The solution is simple and makes sense to me. I wonder if there are scenarios we missed where a 2 levels down nested self-calls necessary. |
d48a9a4 to
9bc6789
Compare
2ba7da9 to
e0ea804
Compare
9bc6789 to
17906b1
Compare
e0ea804 to
95f32e8
Compare
17906b1 to
e3ab9a2
Compare
95f32e8 to
68995c3
Compare
e3ab9a2 to
ae5c79a
Compare
53ddd40 to
4bcb785
Compare
|
Hmm, don't think the runtime path is protected. Someone with Check I was thinking of: |
Ahh shoot, good catch on this. Yes we're deprecating that path, but this PR still has it. I'll make a note to hold off on merging this until we get that change in from #67, and that fix is patched. As for the inner check - I think within the scope of this PR's change, we're OK without that check, but if we ever re-introduce something like |
howydev
left a comment
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.
Makes sense, agree that considering the shape of runtime validation is out of scope of this PR. Additions here LGTM
ae5c79a to
5785dba
Compare
5d0e75b to
7b18487
Compare
5785dba to
2a965ac
Compare
7b18487 to
1e415b1
Compare
huaweigu
left a comment
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
| function _checkIfValidationAppliesCallData( | ||
| bytes calldata callData, | ||
| FunctionReference validationFunction, | ||
| bool isDefault |
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.
isGlobal? I guess it depends on if renaming PR gets in first
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, the rename is on another branch, will merge as soon as possible
| _checkIfValidationAppliesSelector(nestedSelector, validationFunction, isDefault); | ||
| } | ||
| } | ||
| } |
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.
Should we explicitly check executeWithAuthorization and executeUserOp and only allow these to pass?
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.
executeUserOpdoesn't yet exist on this branch, but on the branch where it does, it requiresmsg.senderto be the entrypoint, so we don't need extra handling here.executeWithAuthorizationwill re-run a new validation function, so it should be OK to allow - it doesn't really make sense to call it in a nested way viaexecute/executeBatchdue it just adding gas, but it won't break any security guarantees.
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.
Thanks! I meant reverting on line 609 if random function selector is accidentally calling but not in (executeWithAuthorization, executeUserOp). Maybe it's a bit too defensive.
2a965ac to
4e0fdc3
Compare
1e415b1 to
7e76786
Compare
4e0fdc3 to
18abe0f
Compare
7e76786 to
2569ebd
Compare
18abe0f to
17ed4e0
Compare
2569ebd to
1d34450
Compare
17ed4e0 to
78ae1eb
Compare
1d34450 to
5fda9a2
Compare
huaweigu
left a comment
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
| _checkIfValidationAppliesSelector(nestedSelector, validationFunction, isDefault); | ||
| } | ||
| } | ||
| } |
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.
Thanks! I meant reverting on line 609 if random function selector is accidentally calling but not in (executeWithAuthorization, executeUserOp). Maybe it's a bit too defensive.
78ae1eb to
5c10b0d
Compare
5fda9a2 to
ce90f55
Compare
| // If the selector is executeUserOp, pull the actual selector from the following data, | ||
| // and trim the calldata to ensure the self-call decoding is still accurate. | ||
| callData = callData[4:]; | ||
| outerSelector = bytes4(callData[: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.
@adamegyed Is this line redundant (same as line 626)? Or do you want to pull the actual selector from bytes4(userOp.callData[4:8])?
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 do want to pull the actual selector from bytes4(userOp.callData[4:8]), because this is the calling convention of 4337's IAccountExecute interface. This is in this format during validation, but it will look a bit different during execution. Specifically, when using executeUserOp:
- During validation,
userOp.callDatawill contain:- bytes [0:4]: executeUserOp selector
- bytes[4:end]: actual callData
- During execution, the EntryPoint will send a call to
executeUserOpwith the fullPackedUserOpstruct, and the user op hash.userOp.callDatawill have the same format as above, which is why we need to skip the first 4 bytes before doing the self-call in the account's implementation ofexecuteUserOp.
It's a bit non-standard, but you can see how it gets encoded here: https://github.com/eth-infinitism/account-abstraction/blob/v0.7.0/contracts/core/EntryPoint.sol#L101-L107
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.
Thanks, I think I overlooked the line callData = callData[4:]; (I was probably just following the order in comment here :) ) when I made the previous comment. The current impl is essentially equivalent to
outerSelector = bytes4(callData[4:8]);
callData = callData[4:];
Motivation
In ERC-6900 v0.8, we’re planning on explicitly defining account self-calls to bypass validation. Previously in 6900 v0.7, this behavior was left up to the runtime validation function, but we depended on this behavior for things that the core account needed to do - like batching internal calls by setting the target to self.
Self-calls are also necessary for 6900 v0.8 to efficiently support two new workflows:
executeUserOp, the function introduced in EntryPoint v0.7 to allow accounts to access user op contents in the execution phase, andexecuteWithAuthorization, a workflow to allow selecting between different runtime validation functions & to provide data to the runtime validation function and its hooks.In both of those functions, a self-call is used after the “validation” checks are complete. Then, the account is able to execute the function(s) as regular native functions or functions routed through the fallback. Doing so lets us avoid having internal routing for every external function. Internal routing is difficult, un-idiomatic solidity because it involves reimplementing the automatically generated function dispatcher. This would also increase implementation complexity of 6900 v0.8.
However, there is a notable issue with self-call authorization: If a validation function is added to either standard execute functions (
executeorexecuteBatch), then that validation function effectively gets “root” access. Any execution function, including execution functions that the validation function is not allowed to call directly, may instead be called by performing a self-call and encoding the execution function invocation into calldata.We want to prevent the privilege escalation, while still allowing self-calling for the purposes of batching actions, and handling
executeWithAuthorizationandexecuteUserOp.Solution
When running user op and runtime validation, add an extra check:
execute, disallow a target address of the account. This is unnecessary wrapping, and the inner call may instead be pulled up to the entire calldata itself.executeBatch, then:Callwhere the target is the account, inspect the selector in calldata:executeorexecuteBatchfunctions.This preserves the ability to use self-calls for batched actions,
executeUserOp, andexecuteWIthAuthorization, while preventing privilege escalation.Also add a test that shows these different use cases with
ComprehensivePlugin.Design Questions
In earlier attempts to solve this problem, I suggested allowing arbitrarily-deep recursion via
execute/executeBatch. This would pose a problem to pre-validation hooks looking to inspect calldata, wherein we must either:However, this PR proposes an approach of entirely disallowing
executeto self, and only allowing a max recursion depth of 1 forexecuteBatch. This simplifies things by capping the depth of any required calldata-inspecting pre validation hooks to max 1, and only for theexecuteBatchselector. Given this constraint, I think it is reasonable to expect pre-validation hooks that fall into this category of "inspecting calldata selectors + params" to either:executeBatch, and not have to worry about this.executeBatch, and unpack + handle the logic for each call in the batch within the hook itself.Does that seem like a reasonable expectation?