fix: dedupe EIP-7702 stateOverrides by sender - #29
Open
SahilVasava wants to merge 1 commit into
Open
Conversation
When a bundle being simulated via filterOps contained multiple userOps
from the same sender (a common back-to-back submission pattern using
different nonceKeys — which the mempool intentionally supports),
getEip7702DelegationOverrides pushed one stateOverride entry per userOp
keyed by userOp.sender. The resulting array carried duplicate `address`
entries, and viem's publicClient.call rejected the simulation with:
State for account "0x..." is set multiple times.
This aborted the entire bundle (filterops_failed in executor_manager),
both userOps were evicted from the mempool ("user operation rejected"),
and meta-aa's billing retried 10x before marking them UOP_NF in
pending_transactions. Observed ~1-2 incidents/day on Arbitrum dropping
2 userOps each.
The delegate code is identical for every userOp from a given sender
(the EIP-7702 authorization on a smart account points at one
implementation), so collapsing to a single entry per sender is safe.
Switch to a Map<address, code> so duplicate senders just overwrite —
viem's stateOverride can't represent different code per address anyway,
so any divergent delegates in a single bundle were already unsimulatable.
getFilterOpsStateOverride was already returning one entry keyed by the
EntryPoint, so the only collision surface was here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When a bundle being simulated via
filterOpscontained multiple userOps from the same sender,getEip7702DelegationOverridespushed onestateOverrideentry per userOp keyed byuserOp.sender. The resulting array carried duplicateaddressentries, and viem'spublicClient.callrejected the simulation with:The whole bundle aborted (
filterops_failedinexecutor_manager), both userOps were evicted from the mempool, and meta-aa's billing retried 10× before marking themUOP_NFinpending_transactions.Why this happens to UR specifically
The mempool intentionally permits multiple userOps per sender when they use different nonceKeys (the high 192 bits of EP v0.7's nonce, i.e. parallel nonce channels). That's a feature — it lets a single account fire multiple in-flight userOps that get batched into one bundle. Observed pair on Arbitrum:
The mempool batched them correctly. The override builder didn't know about that pattern.
Fix
Switch the local accumulator from
StateOverride[]toMap<Address, Hex>. Same sender just overwrites — and since the EIP-7702 delegate is identical for every userOp from a given smart account (one Kernel impl per account), the collapsed entry is correct.getFilterOpsStateOverridealready emits one entry keyed by the EntryPoint, so this was the only collision surface.Why last-write-wins is safe
viem's
stateOverrideis a single snapshot for the entireeth_call— eachaddressmust be unique. Per-userOp staging of different delegate codes was already unsimulatable; the old code would have been rejected by viem the same way. In practice all userOps from one sender in a bundle share a delegate, so the dedup is a no-op semantically.Impact (Arbitrum, last 7 days)
UOP_NFOther chains likely affected the same way wherever back-to-back same-sender submissions happen.