Skip to content

fix: dedupe EIP-7702 stateOverrides by sender - #29

Open
SahilVasava wants to merge 1 commit into
mainfrom
fix/dedupe-7702-stateoverride-by-sender
Open

fix: dedupe EIP-7702 stateOverrides by sender#29
SahilVasava wants to merge 1 commit into
mainfrom
fix/dedupe-7702-stateoverride-by-sender

Conversation

@SahilVasava

Copy link
Copy Markdown
Collaborator

Summary

When a bundle being simulated via filterOps contained multiple userOps from the same sender, 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.

The whole bundle aborted (filterops_failed in executor_manager), both userOps were evicted from the mempool, and meta-aa's billing retried 10× before marking them UOP_NF in pending_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:

sender 0x3916484216f6d0Cef056a6C4Ca5C840FB4C9260A
uop1: nonceKey=0x53c1c9ca366fb03256d26859ffea84d51231689e6bdd, seq=0
uop2: nonceKey=0x326f082f29f9b2d3394cdbda4d69a571472757d6eb5e, seq=0  ← different channel

The mempool batched them correctly. The override builder didn't know about that pattern.

Fix

Switch the local accumulator from StateOverride[] to Map<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.

getFilterOpsStateOverride already emits one entry keyed by the EntryPoint, so this was the only collision surface.

Why last-write-wins is safe

viem's stateOverride is a single snapshot for the entire eth_call — each address must 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)

  • ~13 userOps dropped, ~1-2 incidents/day
  • Bursty: May 9 had 14 issue lines, May 10 had 0, May 11 had 7
  • Pattern: same sender, different nonceKeys, both userOps in a bundle → UOP_NF

Other chains likely affected the same way wherever back-to-back same-sender submissions happen.

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.
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.

1 participant