Skip to content

feat(mempool): revm post-state replay fallback for V3 tick-crossing swaps - #158

Merged
0xfandom merged 4 commits into
developfrom
feat/revm-post-state-fallback
May 26, 2026
Merged

feat(mempool): revm post-state replay fallback for V3 tick-crossing swaps#158
0xfandom merged 4 commits into
developfrom
feat/revm-post-state-fallback

Conversation

@0xfandom

Copy link
Copy Markdown
Collaborator

Summary

  • Adds aether_simulator::post_state_replay with replay_v3_post_state_rpc — forks pre-victim state, commits the victim, then reads slot0() + liquidity() to recover the V3 pool's analytical post-state
  • Adds aether_pools::predict_post_state_with_replay — replay-aware sibling of predict_post_state_with_fallback; when the analytical predictor's confidence flag is low, dispatches into a caller-provided closure for the EVM-fork escalation
  • Wires the closure in the mempool pipeline behind MEMPOOL_POST_STATE_REPLAY=1; V3 tick-crossing swaps that today bump aether_sim_evm_fallback_total{reason="v3_tick_crossed"} and skip now produce a real UnifiedPostState::UniswapV3 from the forked EVM and continue through the candidate path
  • Adds aether_mempool_post_state_replay_total{outcome} + aether_mempool_post_state_replay_latency_ms histogram with pre-touched labels so dashboards have stable series from cold start
  • Reuses the existing sim_semaphore from BackrunValidatorConfig so replay concurrency is bounded by the same ceiling as the backrun validator

Why

Three cases currently land in predict_post_state_with_fallback's "low-confidence" branch and silently lose the candidate: V3 swaps that cross at least one tick, Curve StableSwap iterations that don't converge, and Balancer pools with unequal weights. The doc on the function explicitly flagged this as scaffolding ("Real EVM fork-replay implementation … lands once the simulator gains a generic 'apply this transaction and read post-state' entry point"). Without it, every multi-tick V3 swap on mainnet — the bulk of V3 traffic — sits in the metric and never produces a graph-edge update.

This PR closes that gap for V3. The replay reads slot0() directly from the forked EVM after the victim is committed, so the answer is exact rather than the bucket-clamped estimate the analytical predictor returns when single_tick = false.

Files Changed

File Purpose
crates/simulator/src/post_state_replay.rs New module — V3 replayer + Curve/Balancer stubs + ReplayError taxonomy + 5 unit tests
crates/simulator/src/lib.rs pub mod post_state_replay;
crates/pools/src/lib.rs predict_post_state_with_replay + ReplayProtocol enum + 3 unit tests
crates/grpc-server/src/metrics.rs mempool_post_state_replay_total{outcome} counter + mempool_post_state_replay_latency_ms histogram, pre-touched outcome labels
crates/grpc-server/src/mempool_pipeline.rs Replace predict_post_state_with_fallback call with replay-aware sibling. New try_post_state_replay helper handles the V3 fork → commit → read flow. 3 new pipeline-level tests. SimContext::with_post_state_replay builder + field
crates/grpc-server/src/main.rs MEMPOOL_POST_STATE_REPLAY=1 env flag wires the bootstrap toggle

Implementation notes

  • Replay path is sync, not async. replay_v3_post_state_rpc is a synchronous function over RpcForkedState (the same shape validate_backrun_rpc already uses). The pipeline calls it from a spawn_blocking worker — AlloyDB's WrapDatabaseAsync does the right thing via block_in_place from within blocking workers.
  • Reuses provider plumbing. No new RPC connection — pulls the DynProvider<Ethereum> already attached to BackrunValidatorConfig. When that config is absent or has no provider the replay short-circuits and counts no outcome (no spurious success bumps).
  • Concurrency bounded by the existing semaphore. A try_acquire_owned on cfg.sim_semaphore — saturation bumps sim_error and skips, same backpressure semantics the backrun validator already uses.
  • single_tick = true on revm-derived V3PostState because the post-state is read directly from post-execution storage; the multi-tick precision concern that motivates the flag does not apply to revm-derived values.
  • amount_out = U256::ZERO on the returned V3PostState. The downstream unified_to_post_reserves reads new_sqrt_price_x96 only for V3, so this field is dead for graph-edge updates. Set to zero with a comment so it doesn't lie to a future caller that may read it.

Scope cut

Curve and Balancer reader hooks are stubbed and surface ReplayError::UnimplementedProtocol. Follow-up PR adds:

  • Curve: balances(uint256 i) view interface + decode for both coin indices; new CurvePostState mapping
  • Balancer: getPoolTokens(bytes32 poolId) view call against the BalancerV2 Vault; new BalancerPostState mapping
  • Both: respective unified_to_post_reserves arms and PredictedPostState writer variants

Splitting keeps each review surface ~300-400 LOC; the EVM-commit machinery is identical and validated by the V3 path here.

Acceptance criteria

  • predict_post_state_with_replay invokes the replay closure on V3 tick-cross and uses its result when Some
  • predict_post_state_with_replay skips the closure when the analytical predictor succeeds
  • replay_v3_post_state_rpc returns DecodeFailed("slot0") against a non-V3 pool address
  • try_post_state_replay short-circuits with no metric bump when post_state_replay_enabled = false
  • try_post_state_replay bumps unimplemented_protocol for Curve and Balancer when enabled
  • MEMPOOL_POST_STATE_REPLAY env flag wires the bootstrap toggle without touching the develop default
  • Live mainnet smoke — aether_mempool_post_state_replay_total{outcome="success"} increments under V3 tick-crossing traffic (follow-up smoke after merge)

Test plan

  • cargo build --release clean
  • cargo clippy --workspace --all-targets --release -- -D warnings clean
  • cargo test --workspace --release -- --test-threads=1 all green (5 new replayer unit tests + 3 new predictor tests + 3 new pipeline tests)
  • go build ./... + go test ./... -count=1 green (no Go changes, regression check)
  • forge build + forge test green (no Solidity changes, regression check)
  • Live mainnet observation with MEMPOOL_POST_STATE_REPLAY=1 — confirm aether_mempool_post_state_replay_total{outcome="success"} increments and the latency_ms histogram populates

@vercel

vercel Bot commented May 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
aether Ready Ready Preview, Comment May 25, 2026 7:14pm
aether-63xv Ready Ready Preview, Comment May 25, 2026 7:14pm

0xfandom added 4 commits May 26, 2026 00:32
Adds aether_simulator::post_state_replay with replay_v3_post_state_rpc:
forks pre-victim state via RpcForkedState, transact_commit the victim,
then transact slot0() + liquidity() against the post-victim state and
return a V3PostState the caller can feed into the graph-edge update.

Curve and Balancer reader hooks intentionally stubbed and surface
ReplayError::UnimplementedProtocol so the metric label space is
already pinned for the follow-up PR.
Adds predict_post_state_with_replay sibling to predict_post_state_with_fallback.
On low-confidence flag the new function invokes a caller-provided replay
closure with the ReplayProtocol family; on Some the unified post-state
proceeds, on None the candidate skips. Backwards-compatible — every
existing call site of predict_post_state_with_fallback keeps its current
shape.
Adds aether_mempool_post_state_replay_total{outcome} with pre-touched
labels (success / victim_reverted / victim_halted / read_call_failed /
decode_failed / sim_error / unimplemented_protocol / timeout) and
aether_mempool_post_state_replay_latency_ms histogram. Label space
matches ReplayError::as_str() so dashboards stay enumerable.
Replaces predict_post_state_with_fallback with the replay-aware sibling.
On V3 tick-cross the pipeline now dispatches to replay_v3_post_state_rpc
when MEMPOOL_POST_STATE_REPLAY=1 and a BackrunValidatorConfig provider
is wired; otherwise the dormant behaviour from develop is preserved.

Curve and Balancer escalations short-circuit to unimplemented_protocol
because their reader hooks land in a follow-up PR. Concurrency is
bounded by the existing sim_semaphore so a replay storm cannot push
past the configured ceiling.
@0xfandom
0xfandom force-pushed the feat/revm-post-state-fallback branch from 5b720ac to bb10975 Compare May 25, 2026 19:13
@0xfandom
0xfandom merged commit d7392e3 into develop May 26, 2026
3 checks passed
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