ADR-055: Pending-transaction strategy seams — MarketContext, PendingTxStrategy, SubmissionTarget, and the V4 closure¶
Status: accepted (2026-09-18). Basis: the seam synthesis
(.scratch/strategy-arch-survey/strategy-seam-design.md, reviewed and
approved), ADR-054’s promoted evidence seams, ADR-019’s strategy-vs-engine
doctrine. The engine-family generalization of ADR-018 is pulled here by
decision: a strategy suite over one core is the stated end-state, which
Phases B/C below schedule.
Context¶
Two strategies (settlement arbitrage over sealed blocks; backrun over
observed pending transactions) shared a healthy lower substrate (solvers,
pools, simulation, executor grammar, RPC provider) but nothing above it:
strategy was not a config/console/launcher concept, the pending-transaction
pipeline was one monolithic function (frame_pipeline::process_frame) with
backrun selection and economics inline, StrategyRuntime misnamed shared
caches as strategy identity, V4 pools could neither be extracted from the
replay journal nor admitted into the per-trigger sandbox, and unknown pool
families were silently skipped at use sites.
Decision¶
D1 — Vocabulary (CONTEXT.md is canonical)¶
pending transaction, PendingTxDriver, PendingTxStrategy,
SettledBlockStrategy, MarketContext, ComposedIntent/Decided,
SubmissionTarget, strategy facet. Names carry plain meanings; the retired
“frame”/”lane” vocabulary survives only where a historical module name still
says it.
D2 — The pending-transaction seam (landed, commit 024cad4d4)¶
MarketContext(degenbot-submission/src/market_context.rs) — the frame-surviving caches: connector index + DFS graph, token id/address joins, warm code cache. Substrate, never strategy; the formerStrategyRuntimeis retired.PendingTxStrategy(degenbot-submission/src/pending_tx.rs) — one strategy reacting to observed pending transactions. Stages:admit→discover→evaluate→compose→decide, with strategy-neutral hand-off artifacts (ComposedIntent,Decided) and associated types (Affected/Intents/Evaluated) keeping strategy shapes out of the driver. Consumed via generic dispatch; theasync_fn_in_traitexpectation records the no-dyn contract.BackrunStrategy(degenbot-submission/src/backrun_strategy.rs) — the first implementation: WETH/quote-orientation gating, anchored DFS wiring, envelope-gated evaluation, net-bid economics, and the decision policy moved out of the driver. The driver’s stage code names no WETH / MEVBlocker / wallet-economics vocabulary.Driver side —
frame_pipeline::process_frameis re-expressed as the pending-transaction driver: replay (ADR-054 seam 1) → descriptors + journal extraction (seam 2) → the strategy’s stage chain → bundle simulation gate → submission. The driver keeps replay/extraction/ timings/liveness as strategy-neutral machinery;PipelineConfigand the honest observe vocabulary are driver-owned.SubmissionTarget { Bundle(BundleTarget), Public }atdispatch_and_submit— the typed channel vocabulary replacingOption<&BundleTarget>; the Python FFI wrapper passesPublic.
D3 — V4 is first-class in the pending-transaction substrate¶
Sandbox admission (commit 90668c03e):
ExplicitPoolState::V4delegates to the canonicalregister_v4_pool; a golden V4→V2 route solve (workspace_v4_cycle_reaches_the_solver) pins the behavior.Journal extraction (commit 024cad4d4):
PoolFamily::V4PoolManagercarries aV4PoolSetof known pool identities; extraction decodes slot0, liquidity, and touched tick words per poolId against the pinned PoolManager layout (docs/architecture/v4_poolmanager_storage_layout.md). An unknown poolId NEVER fabricates state — it stays explicitlyUnsupported. The stale “layout nobody pinned” premise is revoked.Remaining lane-level gap (tracked): the connector index (
V2ConnectorIndex) carries V2/V3 edges only, so production descriptors pass an emptyV4PoolSetuntil the index learns V4 identities andSuccessStrategycompose gains V4 funding/capture shapes. Substrate is ready; lane wiring is a strategy task, not a substrate defect.
D4 — Loud-abort posture for pool families at use sites¶
Standing rule (user directive): if code tries to use a pool of a family
the infrastructure cannot serve, that aborts loudly (typed fatal/tripwire),
never a silent skip. Transient RPC/timing/fetch failures keep existing skip
semantics. The audit (.scratch/strategy-arch-survey/loud-abort-audit.md)
found 24 family-at-use silent-skip sites vs 8 legitimate transient skips —
the five worst (silent pool-kind drop in DB pathfinding,
#[non_exhaustive] PoolKind walker abandonment, silent V4-half drop in
mixed frames, fabricated zero V2SwapOutcome for unknown pools, silent
exchange-name drop in the pool updater) are the fix order; each fix is a
typed-abort conversion with a named red test.
D5 — Phases B/C (recorded, not built here)¶
Phase B (ergo PF37R7): core-service consolidation toward the strategy host — one event hub over pending-tx feeds + newHeads/logs with fan-out to subscribed strategies; one pool-state tracker every strategy reads (the sidecar’s leaked empty anchor retires); one route registry built once and shared by reference; both drivers consume services by handle.
Phase C (ergo GOTEEG): the dynamic strategy host — register/enable/ disable strategies at runtime (operator verbs +
.enabledfacets; the currentstrategy.namesingle-selector lets exactly one arm run until then); pulls the ADR-018 engine generalization for the settled-block family (parameterized stage payloads, genericEngineDriver, per-family fleet globals) — scheduled, not speculative.
D6 — Amendments to ADR-054 (corrections of record)¶
Seam 4’s cited
path_selection::solve_witnessesnever landed; the witness role is played bySolveStats/ChainOutcomein the pipeline.Anchored discovery is depth-capped at 3 hops (
find_paths_iter(..., Some(2), ...)); “3-hop and beyond by curve” is an aspiration, not a shipped capability.The decision gate consumed the per-frame liveness pair
(age_ms > stale)historically; the finality-based liveness FSM (Tentative/revive/death;NonceConsumed/MinedAt/SlotTakenAt) replaced it and is deliberately strategy-agnostic channel machinery — its eventual home is the submission channel (Phase B), recorded jointly with the liveness task’s own record.
Consequences¶
Adding a pending-transaction strategy is: one
PendingTxStrategyimpl + typed config facet readers + one launcher/console row. The four surveyed wedge buckets (driver/FFI/CLI/config/env) collapse to one new Rust module and values.V2/V3/V4 all admit, extract, and solve inside the per-trigger sandbox.
The settlement path drives the same submission channel with
SubmissionTarget::Public; its remaining Python-side candidate assembly is tracked (ergo GE5BE7).Loud-abort is the running audit-and-fix posture (ergo TYIXQ7, blocked on the fix pass).