ADR-054: The strategy plumbing surface — ReplayOutcome, journal extractors, the planning workspace, and anchored discovery are the promoted seams¶
Status: accepted (2026-09-17; epic HO76KB, tasks J4SSTF → Z3YEUO: the frame-replay strategy was proven by the live soak report
logs/backrun/replay_soak.md, which this ADR closes over).
Context¶
The pre-epic backrun pipeline decoded calldata: every strategy frame had to
understand the wire (routers, aggregators, batch settle orders, eth-flow
helpers…) — an unbounded-decode problem. The first extractor prototypes
lived in degenbot_decoders::target_classifier, the analytic post-target
estimator lived in degenbot_bot::bot_core::post_target, and the discover
stage fanned 2-hop drift cycles from a QuoteFan/connector-star surface.
The limit was structural: the wire carries MEV shapes the bot has to know
about without knowing the specifics — what pool a frame touches, with
what resulting state, and what cycle through those pools would still pay
after the frame.
The HO76KB epic rebuilt the hot path on four facts every lane consumes equally.
This ADR records the plumbing surface as promoted so the NEXT strategy
doesn’t re-derive guarantees the merge already proves.
Decision¶
Seam 1 — ReplayOutcome carries the whole frame’s failure/success¶
degenbot_simulation::sim::evm::frame_replay::ReplayOutcome. One type for
every possible result of having replayed a pending-tx frame: typed
post-states of touched pools, wall/rpc_reads evidence, and explicit failure
modes (ReplayStatus). A lane that observes an Err/Incomplete-variant
ReplayOutcome knows whether the frame died honestly (RPC failure,
nonce-gap) or simply was never actionable — the infamous “we closed our
eyes” pseudo-success doesn’t exist on this surface.
Owner crate: degenbot-simulation. Producer: degenbot-submission’s frame
pipeline and any other lane feeding the workspace.
Seam 2 — extract_pool_post_states = the journal is the only witness¶
degenbot_simulation::sim::evm::journal_pools::extract_pool_post_states.
One function from (ReplayOutcome, descriptors) to typed
PoolPostState, keyed off the replay’s journaled EV slots and nothing
else: reserve words, slot0/liquidity words, and V4’s per-poolid state rows
flow from the EVM journal, and the extract stage reports a skip reason per
pool (extract_skip in the JSONL trace) instead of hallucinating a state.
The slot_layout module is the single source of truth for where pool
state lives on-chain — a lane that needed the count of pool-reserve-word
decoders from N=3 to N=1 in the first week of the epic.
Seam 3 — The PlanningWorkspace is the only solver state a frame touches¶
degenbot_submission::frame_pipeline (SidecarSolver + the workspace
admission APIs in degenbot_bot::sidecar_engine). The contract is
register with explicit state: nothing in solve/discovery reads the
shared pool graph; a frame carries a private per-frame EVM whose chain
view feeds this frame’s workspace admission and NOTHING else. The
consequence: concurrent frames can never cross-contaminate, and the
fleet’s live dispatch keeps the true queue discipline the ADR-052-era
database had bought per-map.
Owner crates: degenbot-bot (engine) × degenbot-submission (frame
pipeline).
Seam 4 — Anchored touched-set discovery replaces the star fan¶
degenbot_submission::anchored_dfs + degenbot_submission::path_selection::solve_witnesses.
On the frame’s touched pools, the lane walks the connector-index multigraph
for any depth-N cycle that closes in WETH (2-hop parity, 3-hop and beyond by curve). The old star fan (two_hop_cycles, QuoteFan,
normalization, per-frame quote ranking) is deleted outright — not kept
behind a flag — because the anchored lane is strictly the star’s superset
by construction: every star cycle is a depth-2 anchored cycle, every quote
fan that could close through WETH also closes a WETH lexicographic
traversal if the walker is allowed to visit it.
Discovery evidence is committed per frame: sideways steps are the node's residual trees, not side-band quotes; the witness records which
cycle’s price at the frame state justified the envelope’s compose.
Hook-difficulty decisions made here¶
Scratch EVM isolation. A frame’s replay runs against a fresh EVM instance whose backend is the frame’s own chain view; replaying a tx NEVER dirties the solver’s shared
ScrapeDb. The cost is one EVM scaffold per frame (~µs); the win is that relaying the SAME tx twice cannot compound state, and a buggy frame cannot poison a sibling frame.Register with explicit state. The solver workspace offers only
admit({family, pools} + explicit state)— there is SECOND pool-state source a lane could ask for (the graph, the graph’s caches). This makes the seededDB -> EVMside directly auditable atframe_pipeline.rs’s single admission surface.Slot-layout is one module. No per-protocol “count the words” decoder scattered across strategies. The V2 reserves word, the V3 slot0+liquidity pair, and the V4 poolid row each have exactly one home — both extractors and the workspace read through it.
Bid from profit, envelope against the fork view. Compose≠decide: the lane computes
bid = floor(profit * share)from the WETH-closed chain’s profit and validates the compose via the on-chaineth_callManyenvelope, not by replaying the bid inside the workspace. The two checks read different evidence so one bug can’t flip both signs.
How a third strategy consumes these seams — worked example: a liquidation dry-run¶
A lane “observe undercollateralized AAVE positions” consumes exactly each seam in order, with no new machinery:
The frame pump hands pending-liquidation
ReplayOutcomes (seam 1) — no liquidation-specific decode of calldata; a health-factor-touching frame is the wire selectorliquidationCallthat “touched a market” { the journal (seam 2) extracts the typed post-state of that market’s debt-collateral record. The wire is understood as “pools whose state changed,” the classic slot-layout query.admit_extractedputs the affected market into a fresh frame’sPlanningWorkspace(seam 3) — the workspace reads nothing from the arbitrator’s long-lived state, so the lane needs no locks.Anchored discovery (seam 4) enumerates cycles from the debt token through the collateral token; a solver-side ceiling prices each — the whole enumerator is “which markets are depth-2 WETH-closeable from this debt token,” and nothing in the lane’s code knows AAVE-specific words.
The liquidate-side contract call the lane emits goes through the same
eth_callManyenvelope verify each response epoch of composition uses.
The cost touched: none of the four seams. The lane’s own specialised code is “what sounds like a liquidation,” (detect) and “why a profitable relation between collateral and debt persists” (economics).
Cross-references¶
ADR-019 — the settlement/engine split this strategy’s stage layering inherits.
ADR-045 — the registry borrow contract; the strategy NEVER borrows from the registry because the discovery plane holds its own per-frame admission surface.
ADR-051 — the rust-owned console: the lane’s CLI face is a
cargobinary, not a Python driver.AGENTS.md — “Rust is the engine; Python is a driver shell” is the interpretation under which these seams are promoted: every other strategy lives purely in
rust/crates/engine/degenbot-submissionanddegenbot-bot/degenbot-simulation, driving the strategy exclusively from this surface.logs/backrun/replay_soak.md— the live evidence that these are the observed contracts, not the hoped-for ones: 59 live frames, zero fake conversions, latency budget with >99% slot headroom at p95.