DEV Community

Cover image for Inside an ERC-4337 Bundler: What Actually Happens Between simulateValidation and handleOps
chain box
chain box

Posted on

Inside an ERC-4337 Bundler: What Actually Happens Between simulateValidation and handleOps

Most explainers on Account Abstraction stop at "bundlers package UserOperations into a transaction." That's true, but it skips the part that actually makes bundler infrastructure hard to build and easy to break. The interesting engineering is in the gap between simulation and inclusion and closing that gap is the entire reason ERC-7562 exists.

The core problem bundlers are solving

A UserOperation isn't a transaction. It's a signed intent sitting in an alt-mempool, waiting for a bundler to bet gas on it. When a bundler calls handleOps on the EntryPoint, it's fronting real ETH for every operation in that bundle. If one of those operations reverts during actual execution, the bundler eats the gas cost with no refund.

So before anything goes near a real block, the bundler runs simulateValidation against the EntryPoint, which triggers the account's validateUserOp and, if a paymaster is attached, validatePaymasterUserOp. The catch: Ethereum state between simulation and inclusion isn't frozen. Balances shift, other transactions land, block parameters change. A validation function that reads block.timestamp or checks a mutable external balance can pass simulation cleanly and still revert on-chain a few seconds later.

Why ERC-7562 had to exist

The original ERC-4337 specification left this as an informal convention the reference bundler just banned a set of opcodes and hoped implementations converged. ERC-7562 formalizes it. During the validation phase, a handful of opcodes are off-limits entirely: TIMESTAMP, NUMBER, COINBASE, GASPRICE, SELFBALANCE, BALANCE, ORIGIN, CREATE, and a few others whose return values can legitimately differ between the moment of simulation and the moment of inclusion.

Storage access gets the same treatment. A validation function can only touch storage slots that belong to its own contract, or slots explicitly associated with the sender, factory, or paymaster involved in that specific operation. Reach outside that boundary and the bundler is supposed to reject the operation outright, because there's no way to guarantee that storage hasn't mutated by the time the bundle lands.

There's also a hard gas ceiling on the whole phase roughly 150,000 gas each for account and paymaster validation which exists purely to stop a validation function from becoming a computational sinkhole that ties up bundler resources for pennies of fee.

None of this is enforced on-chain. The EntryPoint doesn't care about opcode bans. It's the bundler's tracer usually a custom debug_traceCall implementation, like the one in the eth-infinitism reference bundler that walks the execution trace and flags violations before the operation ever gets gossiped to the P2P mempool.

The part most people skip: reputation

Opcode and storage rules solve determinism, but they don't solve spam. Because the alt-mempool is permissionless, nothing stops an address from flooding bundlers with operations that pass every simulation check and still burn resources at scale. ERC-4337 handles this with a staking and reputation model applied to the three "global" entities factory, paymaster, and aggregator since these get reused across many UserOperations and can invalidate a large batch at once if something goes wrong. Bundlers track opsSeen versus opsIncluded per entity and throttle or ban ones with a bad ratio. Staked entities get looser storage rules in exchange for skin in the game.

If you're shipping a smart account or paymaster and want to know beforehand whether your validation logic will actually clear these rules in production rather than fail silently on mainnet, that's a standard part of pre-launch review for account abstraction infrastructure checking storage access patterns and gas bounds against ERC-7562 before deployment, not after.

Where implementations diverge

The EIP leaves tracer implementation unspecified, which means Pimlico's Alto, Alchemy's Rundler, and the eth-infinitism reference bundler don't all enforce identical edge cases. An operation that clears one bundler's simulation can occasionally get rejected by another's stricter tracer. If you're building wallet infrastructure that depends on multi-bundler redundancy, this is worth testing against directly rather than assuming uniform behavior.

Top comments (0)