-
Notifications
You must be signed in to change notification settings - Fork 38
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
fix: [spearbit-98][quantstamp-8] State is not cached properly before validation/execution steps, Non-Idempotent Pre-Execution Hook #58
Conversation
ffabbae
to
ac8c8c0
Compare
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 this lgtm, have to step out for a bit but will take another pass soon!
bytes4 selector = _selectorFromCallData(userOp.callData); | ||
SelectorData storage selectorData = _getAccountStorage().selectorData[selector]; | ||
|
||
FunctionReference userOpValidationFunction = selectorData.userOpValidation; | ||
hasPreValidationHooks = selectorData.hasPreUserOpValidationHooks; | ||
bool hasPreValidationHooks = selectorData.hasPreUserOpValidationHooks; |
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.
Nice
postHooksToRun = new FunctionReference[][](postHooksToRunLength); | ||
postHookArgs = new bytes[](postHooksToRunLength); | ||
|
||
uint256 currentIndex = 0; |
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.
uint256 currentIndex = 0; | |
uint256 currentIndex; |
postHooksToRun = new FunctionReference[][](postHooksToRunLength); | ||
postHookArgs = new bytes[](postHooksToRunLength); | ||
|
||
uint256 currentIndex = 0; |
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.
uint256 currentIndex = 0; | |
uint256 currentIndex; |
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 want to keep these explicit for the reader, rather than having them thinking about the default values.
// We use `currentIndex + prePermittedCallHooks.length` for the starting index but do not update it, | ||
// because its current value is useful for executing the hooks. | ||
_cacheAssociatedPostHooks( | ||
preExecHooks, selectorData.executionHooks, postHooksToRun, currentIndex + prePermittedCallHooks.length |
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: maybe store currentIndex + prePermittedCallHooks.length
in a variable to reuse in line 694?
// in the args for their empty `bytes` argument. | ||
uint256 postHooksToRunLength = preExecHooks.length + (hasPostOnlyExecHooks ? 1 : 0); | ||
postHooksToRun = new FunctionReference[][](postHooksToRunLength); | ||
postHookArgs = new bytes[](postHooksToRunLength); |
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.
Why not keep same indexing for postHooksToRun
, postHookArgs
, and preHooks
, add postOnlyHooks
in the end of postHooksToRun
?
Save you the need of passing currentIndex
.
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 guess the indexing wont match when the permitted call hooks are present. With such a change to hooks run rn, I am now more open to remove permitted call hooks.
// in the args for their empty `bytes` argument. | ||
uint256 postHooksToRunLength = preExecHooks.length + (hasPostOnlyExecHooks ? 1 : 0); | ||
postHooksToRun = new FunctionReference[][](postHooksToRunLength); | ||
postHookArgs = new bytes[](postHooksToRunLength); |
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 guess the indexing wont match when the permitted call hooks are present. With such a change to hooks run rn, I am now more open to remove permitted call hooks.
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 for the thorough tests! Liking the consolidation of post hooks and post only hooks, along with the design of using a 2d array to address duplicate pre hooks that are associated with different post hooks.
Left some comments, mostly nits.
// Source: pre-Exec | ||
// Target: pre-UserOp-Validation | ||
// n/a - runs before | ||
|
||
// Source: pre-Exec | ||
// Target: UserOp-Validation | ||
// n/a - runs before | ||
|
||
// Source: pre-Exec | ||
// Target: pre-Runtime-Validation | ||
// n/a - runs before | ||
|
||
// Source: pre-Exec | ||
// Target: Runtime-Validation | ||
// n/a - runs before | ||
|
||
// Source: pre-Exec | ||
// Target: pre-Exec (same phase) | ||
// Addition (first element): *impossible* |
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: wasn't really clear why these were repeated (here and below) until I realized you're using these as placeholders for tests you are purposely skipping.
if (hasPostOnlyPermittedCallHooks) { | ||
// If we have post-only hooks, we allocate an single FunctionReference[] for them, and one element in | ||
// the args for their empty `bytes` argument. We put this into the first element of the post hooks in | ||
// order to have it run last. | ||
postHooksToRun[currentIndex] = | ||
CastLib.toFunctionReferenceArray(permittedCallData.permittedCallHooks.postOnlyHooks.getAll()); | ||
unchecked { | ||
maxPostHooksToRunLength += preHookSet.getCount(CastLib.toSetValue(preExecHooks[i])); | ||
++i; | ||
++currentIndex; | ||
} | ||
} | ||
|
||
if (hasPostOnlyExecHooks) { | ||
// If we have post-only hooks, we allocate an single FunctionReference[] for them, and one element in | ||
// the args for their empty `bytes` argument. We put this into the first element of the post hooks in | ||
// order to have it run last. | ||
postHooksToRun[currentIndex] = | ||
CastLib.toFunctionReferenceArray(selectorData.executionHooks.postOnlyHooks.getAll()); | ||
unchecked { | ||
++currentIndex; | ||
} | ||
} |
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 logic will run the different hooks in this order:
- Associated post hooks for
selectorData.executionHooks
- Associated post hooks for
permittedCallData.permittedCallHooks
- Post only hooks for
selectorData.executionHooks
- Post only hooks for
permittedCallData.permittedCallHooks
Technically, I think the order before was 1, 3, 2, 4. For example see this code from before:
// Run any post exec hooks for the `executeFromPluginExternal` selector
_doCachedPostHooks(postExecHooks, postExecHookArgs);
// Run any post only exec hooks for the `executeFromPluginExternal` selector
_doCachedPostHooks(
CastLib.toFunctionReferenceArray(
storage_.selectorData[IPluginExecutor.executeFromPluginExternal.selector]
.executionHooks
.postOnlyHooks
.getAll()
),
new bytes[](0)
);
// Run any post permitted call hooks specific to this caller and the `executeFromPluginExternal` selector
_doCachedPostHooks(postPermittedCallHooks, postPermittedCallHookArgs);
// Run any post only permitted call hooks specific to this caller and the `executeFromPluginExternal`
// selector
_doCachedPostHooks(
CastLib.toFunctionReferenceArray(
storage_.permittedCalls[permittedCallKey].permittedCallHooks.postOnlyHooks.getAll()
),
new bytes[](0)
);
I think either way is okay, but is this an intentional change?
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, this is probably worth fixing. We'll need to keep track of the two indices that point to the beginning of each type of associated hook but seems like an easy fix.
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 fixed this to run in the post hooks in the following order:
- associated postExec
- post-only postExec
- associated postPermittedCall
- post-only postPermittedCall
In this commit: bae9391
Could you please double check? I walked through with a tracer but wanted to confirm.
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.
It seems that permitted call hooks are being removed (basing this off of PR 65). Will this PR need to be updated as well (maybe after merging PR 65)?
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.
PR 65 will be rebased on top of this and merged later. So I guess we don't strictly need to review this, since the invocation of _doPrePermittedCallAndPreExecHooks
will be replaced with _doPreExecHooks
.
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/execution steps, Non-Idempotent Pre-Execution Hook (#58)
…validation/execution steps, Non-Idempotent Pre-Execution Hook (#58)
Motivation
https://github.com/spearbit-audits/alchemy-nov-review/issues/98
We are inconsistent in what account state we cache into memory prior to execution, and what we load from storage when needed.
Solution
Define distinct validation and execution phases, for which storage changes to account state should not result in different execution.
Update the implementation of
UpgradeableModularAccount
to cache according to these phases.Test the behavior of account state changes to ensure phases are preserved. This is a very tricky to orchestrate test since it involves re-entering the account from different plugin functions, and making assertions about the overall call flow. It also has a very large number of cases to test (89), so please read the comments in the
AccountStatePhasesTest
carefully.Implementation strategy for hook caching
(Note the the extra slots for post-only hooks are only allocated if needed, otherwise they are not added to the length).
Regular (non-
IPluginExecutor
) call flow:bytes[] preExecReturnData
ofpreExecHooksLength + 1
for holding the return data. (last element goes unused, to indicate no parameter)FunctionReference[][] postExecHooks
ofpreExecHooksLength + 1
for holding the post-exec hooks.postExecHooks
with the post-only hooks.Permitted call flow:
bytes[] preHookReturnData
ofpreExecHooksLength + prePermittedCallHooksLength + 2
for holding the return data. (Last 2 elements go unused)FunctionReference[][] postHooks
ofpreExecHooksLength + prePermittedCallHooksLength + 2
for holding the post-* hooks.Code size
Code size is now 24464 bytes, leaving 112 bytes to spare.