Skip to content
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

AA-527 handleOps should process validateUserOp returning wrong data size #533

Merged
merged 15 commits into from
Feb 16, 2025

Conversation

drortirosh
Copy link
Contributor

@drortirosh drortirosh commented Feb 2, 2025

HandleOps should properly revert with FailedOp if validateUserOp returns wrong data size.

the try/catch is unable to catch wrong return value size, which causes entire handleOps to revert with empty reason

Use _callValidateUserOp instead of sender.validateUserOp
Wrap validateUserOp with assembly call.
Now we can catch FailedOp on broken (or missing) sender.

wrap validateUserOp with assembly call.
This way, we don't rely on EntryPointSimulation to create FailedOp on
sender failures with malformed output (e.g. account not deployed)
optimized callValidateUserOp (less parameters), but actually costs more
(16 per useop, but still...)
Comment on lines 527 to 533
assembly ("memory-safe"){
let success := call(gasLimit, sender, 0, add(callData, 0x20), mload(callData), 0, 32)
dataSize := mul(returndatasize(), success)
validationData := mload(0)
mstore(0x40, saveFreePtr)
}
if (dataSize != 32) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assembly ("memory-safe"){
let success := call(gasLimit, sender, 0, add(callData, 0x20), mload(callData), 0, 32)
dataSize := mul(returndatasize(), success)
validationData := mload(0)
mstore(0x40, saveFreePtr)
}
if (dataSize != 32) {
bool success;
assembly ("memory-safe"){
success := call(gasLimit, sender, 0, add(callData, 0x20), mload(callData), 0, 32)
dataSize := returndatasize()
validationData := mload(0)
mstore(0x40, saveFreePtr)
}
if (!successs || dataSize != 32) {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I don't understand the idea behind the mul(returndatasize(), success) but it seems like you could just check for success, which is a lot more reasonable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the logic is "ignore datasize in case of revert". I don't think that double-negative is more readable than multiplying "success" code by datasize. the both have the same effect. I also mload the returned data optimistically, and ignore it if datasize is not 32

@@ -227,7 +227,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT
//address(1) is special marker of "signature error"
require(
address(aggregator) != address(1),
"AA96 invalid aggregator"
FailedOp(totalOps, "AA96 invalid aggregator")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is totalOps the same as opIndex in this case? If it is not, any tool build around FailedOp will blame the wrong UserOp for this revert.

Comment on lines 520 to 522
assembly ("memory-safe") {
saveFreePtr := mload(0x40)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into it, and there is probably no way to have a clean generic function that wraps this 'save-restore free memory pointer' trick.
Maybe for simplicity we could create inline save() and restore() functions and reuse those instead?
These assembly blocks kind of pollute the code.

@drortirosh drortirosh changed the title _callValidateUserOp to wrap sender.validateUserOp AA-527 handleOps should process validateUserOp returning wrong data size Feb 9, 2025
use if success { ... }  instead of mul(success,....)

also, for EntryPointSimulations:
- had to reduce code size (so cleared simulation-unrelated
"supportsInterface")
- fixed maxgap
@drortirosh drortirosh force-pushed the callValidateUserOp-catch-revert branch from 1a09a5f to ad58580 Compare February 9, 2025 10:06
shahafn
shahafn previously approved these changes Feb 9, 2025
if(sender.code.length == 0) {
revert FailedOp(opIndex, "AA20 account not deployed");
} else {
revert FailedOpWithRevert(opIndex, "AA23 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about it a little more.
I don't know if that's important, but technically it is not checked here if the call reverted or not, only that the return data is not of the correct length. However, if the call did revert, you don't even load it's return data so the emitter even will always be empty, right?
Does it matter? I think it may be useful to see the revert message if there is a way to do it, especially if the UserOp got on-chain and reverted. It could be two different events - FailedOpWithRevert and FailedOpWithBadValidationData. What do you think?
Or at least, maybe change the string to AA23 wrong return or something like that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I check only the size==32, but the assembly code above makes any differrent return value size (either revert or return data) to be size=0
so checking "size==32" actually means "didn't revert, AND size is 32"
thus no-account causes a size=0 with no revert, but handled by this exception case.
also, assembly { return (0,0) } (or any other assembly-level return with size!=32) would be treated as revert.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand what you did, I am asking if that's actually the right thing to do here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean? success is defined as "call didn't revert AND datasize is 32"
since I have to use assembly, I prefer to do the AND also in assembly, and expose only the result to solidity.
I could reverse the terms: have "bool success", and clear it if datasize!=32... it has the same impact, only that the solidity code ends up doing if (success) { ... }

Copy link
Collaborator

@forshtat forshtat Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two distinct failure cases:

  1. The call reverted
  2. The call retuned value of wrong size

I know there is no practical difference to us. But it may be beneficial for wallet developers to be able to tell the difference, maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only way to return the wrong data size is by NOT inheriting the IAccount interface, and implement it differently (or, by using assembly return)
I think neither modes are something we want to explicitly support.
(btw: if solidity "try/catch" worked correctly, then all these cases would be considered "revert"...)

@drortirosh drortirosh force-pushed the callValidateUserOp-catch-revert branch from 24ee87d to 683b2c8 Compare February 16, 2025 14:08
@drortirosh drortirosh merged commit f4e2bd2 into develop Feb 16, 2025
8 checks passed
@drortirosh drortirosh deleted the callValidateUserOp-catch-revert branch February 16, 2025 23:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants