Skip to content

Deploy adapters pipeline + hardening to main (PRs #10, #13, #14, #15, #16) - #17

Merged
KaiCode2 merged 13 commits into
mainfrom
codex/e2e-adapter-pipeline
Jun 24, 2026
Merged

Deploy adapters pipeline + hardening to main (PRs #10, #13, #14, #15, #16)#17
KaiCode2 merged 13 commits into
mainfrom
codex/e2e-adapter-pipeline

Conversation

@KaiCode2

Copy link
Copy Markdown
Owner

Brings the full adapters-pipeline body of work to main. These were merged as a stack of PRs, but their bases were each other (not main), so the merges collapsed into codex/e2e-adapter-pipeline without ever reaching main (which was still at the #9 merge). This PR lands the lot.

What's included (10 commits)

Validation

Each constituent PR was reviewed and landed green (fmt, clippy ×N, per-protocol isolation builds, offline tests, RPC parity, live WS E2E). No code changes here — purely the merge to main.

KaiCode2 and others added 13 commits June 23, 2026 23:51
Give Balancer V2 a real cold_start (it previously used the Unsupported default)
via the slice-1 ColdStartPlanner machinery. Balancer pool state is not at
predictable slots, so the planner discovers the vault balance slots by access
list rather than naming them.

BalancerV2ColdStartPlanner (src/adapters/balancer_v2.rs):
- Factory cold_start_planner: resolves the vault (metadata.vault, falling back to
  the first state address) and the poolId (bytes32 key). No vault ->
  Err(MissingMetadata("Balancer vault")); non-bytes32 key -> Err(Custom).
- Round 1 (discover): accounts=[vault] + a getPoolTokens(poolId) ColdStartCall on
  the vault with restrict_to=[vault]. A local sol! IBalancerVault::getPoolTokens
  ABI is added here (not the simulation-gated cache_sync copy).
- on_results decodes the token list from the discover call's return data
  (ExecutionResult::output() -> getPoolTokensCall::abi_decode_returns) and Continues
  into a verify round over exactly the captured (vault, slot) pairs; round 2 warms
  them authoritatively. Robust to failure: no discover result / no output / decode
  error -> DiscoverFailed; decoded-but-empty capture -> NoSlotsDiscovered.
- finish: success -> BalancerV2Metadata { vault, pool_address (poolId[..20]),
  tokens }, status Ready. DiscoverFailed -> NeedsRepair(ColdStart);
  NoSlotsDiscovered -> NeedsRepair(PurgeStorage) — distinct repairs.

Tests (manager-authored, tests/cold_start_adoption.rs): a compiled MockBalancerVault
stub fixture (tests/fixtures/, getPoolTokens SLOADs fixed slots 0..=4 and returns
the (address[2], uint256[2], uint256) tuple) installed offline. New acceptance
tests: discover->verify reaches Ready with decoded tokens + verify-refreshed
balance slots and zero RPC; a vault-less pool is Unsupported. Mirrors the upstream
two_round_discover_then_verify_offline pattern (install Address::ZERO beneficiary +
the stub, discover -> verify, assert read_q().is_empty()). Spec at
docs/phase-a4-slice2-spec.md.

Full CI matrix green: fmt; clippy default + adapters-only + no-default (-D warnings);
tests default (cold_start_adoption 8/8) + adapters-only + no-default; cargo doc
-D warnings. The cold-start path compiles in the adapters-only build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A 23-agent adversarial audit of the slice-2 planner (15 confirmed findings, no
high-severity bugs; happy path verified sound) drove these fixes:

- Verify round now honors per-slot fetch outcomes (the one behavioral fix). The
  BalancerPhase::Verify arm returned Done unconditionally, so an archive miss /
  FetchFailed on a discovered balance slot still yielded Ready with unwarmed
  balances. It now inspects results.fetched like the V2/V3 planners and, on an
  unfetchable/never-attempted discovered slot, sets a new
  BalancerRepair::BalancesUnfetched -> NeedsRepair(VerifySlots(discovered)) /
  Degraded; a genuine Zero stays acceptable. This is the per-slot-outcome
  surfacing A4 exists for.
- Empty-capture (NoSlotsDiscovered) now repairs via ColdStart (re-discover)
  instead of PurgeStorage(vault) — the Balancer vault is a shared singleton, so
  a wholesale purge would have wiped every co-tenant pool's warmed state.
- Discover arm branches on call.result.is_success() before decoding and uses
  abi_decode_returns_validate, instead of relying on the decoder to reject a
  revert/halt payload.

Tests (tests/cold_start_adoption.rs, manager-authored): new acceptance tests for
revert->repair, empty-capture->ColdStart, verify-slot-fetch-failure->repair, and
N=3 tokens, with new MockBalancerVault3 / MockBalancerVaultNoSlot / revert
fixtures. pool_address = poolId[..20] is now asserted via a distinct
leading-20/trailing-12 poolId; the missing-vault test pins
MissingMetadata("Balancer vault"). Doc comments corrected from address[2]/
uint256[2] to the real dynamic ABI. cold_start_adoption: 8 -> 12 tests.

Full matrix green: fmt; clippy default + adapters-only + no-default (-D warnings);
tests default + adapters-only + no-default; cargo doc -D warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…idation

Completes the adapters pipeline (track -> react -> simulate) for Uniswap V2,
Uniswap V3, and Balancer V2, validated offline, against RPC parity, and against
a live WebSocket feed. Stacks on the Phase A4 slice-2 Balancer cold-start.

Swap simulation (src/adapters/sim.rs + AmmAdapter::simulate_swap): executes the
protocol's canonical quote in revm against the cold-start snapshot via
AdapterCache::call_raw and decodes amount_out — the deployed bytecode does the
math, no amm-math/LocalAMM/hand-rolled formulas. V2 = Router02.getAmountsOut,
V3 family = QuoterV2.quoteExactInputSingle (fee from metadata), Balancer =
Vault.queryBatchSwap (amount_out = -assetDeltas[1]). SwapQuote/SimError/SimConfig
(mainnet quoter/router defaults + overrides) + local sol! quote ABIs; a single
run_quote maps revert/halt -> SimError::Reverted. Quote-target bytecode is lazily
fetched live or installed as a fixture offline.

Balancer V2 reactive (was routing-only): on a Swap, decode_event now emits
RepairAction::VerifySlots over the cold-start-discovered vault balance slots
(persisted on BalancerV2Metadata.balance_slots by finish), which the reactive
runtime lowers into a resync — keeping cached balances fresh for a subsequent
simulate_swap without reverse-engineering the vault's balance-mapping layout.

Tests:
- tests/adapter_swap_sim.rs (8, offline): per-protocol quote vs a mock quoter
  (no RPC), revert -> Reverted, V3 missing-fee -> MissingMetadata, and a Balancer
  reactive integration test (cold-start -> Swap refresh -> re-simulate reflects
  the new balance). New mock fixtures.
- tests/pipeline_e2e.rs (1): chained cold-start -> reactive Sync on a shared
  cache+registry updates the warmed reserves slot (the integration gap).
- tests/adapter_swap_sim_rpc.rs (3, #[ignore], RPC-gated): sim == eth_call at a
  pinned block for V2/V3/Balancer. Confirmed passing against a live archive node.
- tests/reactive_ws_e2e.rs (#[ignore], RPC-gated): LIVE WebSocket E2E — cold-start
  pinned at B0, then ONLY Sync events over a wss subscription move reserves, and
  simulate_swap matches on-chain getAmountsOut at the event block while NOT
  matching the pinned-B0 quote (proves event-sourced state, not a refetch).
  Confirmed passing live (6 events / 6 min; sim == eth_call to the wei). Subscribes
  topic-only (the provider does not push address-filtered subscriptions) and
  routes by the handler. Plus a fast subscription-health probe.

Full matrix green: fmt; clippy default + adapters-only + no-default (-D warnings);
tests default + adapters-only + no-default; cargo doc -D warnings. Live/RPC tests
are #[ignore] and gated on E2E_RPC_URL (run by the manager, not CI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add offline register -> cold-start -> reactive-event -> simulate_swap regression
tests for Uniswap V2 and V3 in tests/pipeline_e2e.rs, exercising all three
adapter-pipeline legs in one flow (mock quote contracts return a seeded value, so
these pin the chain WIRES end-to-end; state-vs-quote correctness is covered by the
RPC-parity and live-WebSocket tests). Balancer's full chain already lives in
tests/adapter_swap_sim.rs. pipeline_e2e: 1 -> 3 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The adapters pipeline (cold-start -> reactive -> simulate) is self-contained and
validated, so remove the legacy stack entirely to make the crate adapters-only
and agent-traversable. Net: 34 files / ~14.5k lines deleted.

Deleted (all behind the now-removed `simulation`/`search` features):
- legacy modules: amm_wrapper (LocalAMM), cache_sync/, configured_amms, data,
  discovery, events/, progress, routing/ (search/arbitrage, built on LocalAMM),
  and the pool-math modules balancer_pool, balancer_v3_pool, cryptoswap_math,
  curve_pool, slipstream_pool, solidly_v2_pool, stableswap_math, uniswap_v4_pool,
  plus the inline balancer_math + profit modules in lib.rs.
- legacy examples (event_subscription, triangular_arbitrage, programmatic_loading,
  toml_loading, amms.toml) and the legacy `simulation` bench.

Cargo.toml: removed the `simulation`, `search`, `toml`, `full-protocols`,
`common-protocols` features and the no-adapter protocol sub-features; removed the
now-orphaned deps `amms`, `amm-math`, `rayon`, `toml`, `criterion`,
`tracing-subscriber` (all gone from Cargo.lock). `default` is now
`["adapters", "uniswap-v2", "uniswap-v3", "balancer-v2"]`.

Added examples/adapter_pipeline.rs: a runnable adapters-path demo (register ->
cold-start -> WS event subscribe -> reactive apply -> simulate_swap), modeled on
tests/reactive_ws_e2e.rs; env-gated, no-ops (never panics) if no RPC/WS URL.

Arbitrage search (routing/) is removed with the legacy path; it can be rebuilt on
the adapters `simulate_swap` later. No adapter logic changed. Full matrix green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
UniswapV3ColdStartPlanner warmed only the current-tick bitmap word + its
initialized ticks, so a swap crossing into adjacent bitmap words fell back to
EvmCache's lazy backend fetch. Warm a bounded WINDOW instead.

- Add V3_TICK_WORD_RADIUS = 2 (window [W0-R, W0+R] = 2R+1 words) + V3_MIN/MAX_TICK.
- Planner field `window: Vec<(i16, U256)>` (was single word/bitmap_key);
  `resolve_window` computes the window clamped to the valid V3 word range with
  overflow-safe i32 arithmetic.
- Round 2 (Strict/Eager) verifies all window bitmap words in one round; Round 3
  scans each word for initialized ticks (skipping ticks outside ±887272) and
  verifies their {0,3} Tick.Info slots in one round.
- Policy unchanged in spirit: HotSlotsOnly = slot0+liquidity only; Lazy defers
  the whole window; slot0-cold repair + config-metadata preservation untouched.

Effect: moderate tick-crossing swaps (±2 words) are offline-pre-warmed. A true
outward-adaptive scan stays a future refinement (documented on the constant).

Manager tests (tests/cold_start_adoption.rs): v3_cold_start_warms_neighbouring_tick_words
(neighbour bitmap + tick-info slots warmed; was red) and
v3_cold_start_hot_slots_only_skips_tick_words (policy boundary). cold_start_adoption
12 -> 14. Full matrix green: fmt, clippy default + no-default (-D warnings), tests
default + no-default, cargo doc -D warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cold_start returns ReadyWithDeferred(report, Vec<DeferredWork>) for the Lazy
policy (V2 defers token slots; V3 defers the bitmap-word window) but nothing
executed the deferred work, so a Lazy cold-start could never be completed.

Add `AdapterRegistry::run_deferred(&self, &[DeferredWork], &mut dyn AdapterCache)
-> Result<DeferredOutcome>`:
- DeferredWork::VerifySlots(slots) and Repair(RepairAction::VerifySlots(slots))
  -> cache.verify_slots(slots); SlotChanges accumulate into DeferredOutcome.verified.
- ColdStart / Custom / other Repair variants are not executed here (they need
  repair execution / re-cold-start-by-key — item #3 / future); pushed verbatim
  into DeferredOutcome.unhandled rather than dropped or panicked on.

New `DeferredOutcome { verified, unhandled }` (+ is_fully_handled()) in types.rs,
re-exported from mod.rs. cold_start behavior unchanged (Lazy still defers). The
only DeferredWork variant produced today is VerifySlots, so this completes every
current Lazy cold-start; `unhandled` future-proofs the rest.

Manager test (tests/cold_start_adoption.rs): v2_run_deferred_warms_lazy_deferred_slots
(Lazy cold-start -> run_deferred warms the deferred token slots). cold_start_adoption
14 -> 15. Full matrix green: fmt, clippy default + no-default (-D warnings), tests
default + no-default, cargo doc -D warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a real adapter for ProtocolId::SolidlyV2 (previously scaffold-only):
cold-start + reactive + swap-sim, mirroring the Uniswap V2 adapter but for
Solidly's unpacked reserves and stable/volatile invariants.

New types: SolidlyStorageLayout { reserve0_slot, reserve1_slot, token0_slot,
token1_slot } (config-supplied; slot indices are fork-specific so there is no
derivable default), SolidlyV2Metadata { token0, token1, stable, storage_layout },
ProtocolMetadata::SolidlyV2 variant + Debug arm, and a `solidly-v2` feature
(in default).

src/adapters/solidly_v2.rs (SolidlyV2Adapter): event_sources (Sync); cold-start
planner verifying reserve0/reserve1 (both mandatory, classified from SlotFetch so
genuine-zero -> PurgeSlots and archive-miss -> VerifySlots stay distinct) + token
slots, with HotSlotsOnly/Lazy policies (Lazy defers tokens); decode_event Sync
(uint256,uint256) -> two exact full-slot writes (no fetch); after_apply skipped ->
VerifySlots; simulate_swap via the pool's own getAmountOut(amountIn, tokenIn)
through call_raw (stable/volatile math runs in-EVM, none reimplemented).

Manager tests (cold_start_adoption.rs, adapter_reactive.rs):
solidly_cold_start_ready_warms_reserves_and_tokens,
solidly_cold_start_zero_vs_failed_reserves_are_distinct_repairs,
solidly_sync_writes_both_reserve_slots_through_runtime. cold_start_adoption 15->17,
adapter_reactive 28->29. Full matrix green: fmt; clippy default + adapters+solidly
+ no-default (-D warnings); tests default + no-default; cargo doc -D warnings.

Follow-ups (documented in the spec): an offline simulate_swap test with a mock
Solidly pool fixture, the #[ignore] RPC-parity test, and a verified velodrome_v2()
layout default (slot indices need on-chain confirmation — config-supplied until
then). Implemented inline by the manager because subagent dispatch was returning
529 Overloaded; verified against the matrix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A 17-agent adversarial audit of the inline-implemented Solidly adapter (11
confirmed findings) drove these fixes:

- HIGH: run_quote + its imports in sim.rs were gated on
  any(uniswap-v2,uniswap-v3,balancer-v2) but NOT solidly-v2, so
  `cargo build --no-default-features --features solidly-v2` failed to compile
  (masked by the all-features default build). Added solidly-v2 to the four
  cfg(any(...)) gates; all four protocol features now build in isolation.
- Robustness: a missing storage layout in decode_event returned MalformedLog,
  which made ReactiveRuntime::ingest_batch fail the ENTIRE batch (one
  un-cold-started Solidly pool would break reactive processing for every pool).
  Now returns `ignored()` — a config-missing event is skipped, not a
  batch-breaking error (there are no slots to target without a layout anyway).
- Validation: cold_start_planner now rejects a SolidlyStorageLayout whose slots
  collide (UnsupportedReason) instead of silently corrupting the verdict/token
  decode.
- Removed the dead after_apply override (Solidly's unpacked full-slot writes are
  never cold-skipped, unlike V2's masked write, so VerifySlots was unreachable;
  the trait default is correct) + documented why.
- Fixed overclaiming docs: getAmountOut also reads factory/stable/decimals and
  STATICCALLs the factory, so the quote is not reproducible from warmed reserves
  alone (live backend / fixture must reach those).

Tests (Solidly 3 -> 8): colliding-layout -> Unsupported; Lazy defers token slots
+ run_deferred warms them + HotSlotsOnly no-defer; offline simulate_swap (mock
pool getAmountOut) + revert -> Reverted; layout-less Sync doesn't mutate the
cache. Full matrix green incl. per-protocol isolation builds.

Follow-up (needs a Base/Optimism RPC + verified slots): a real-fork RPC-parity
test for an Aerodrome/Velodrome V2 pool, which is the only thing that validates
the real storage layout + getAmountOut/Sync ABIs and exercises the factory/stable
path (the offline mock is a trivial sload(0) stub).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the one remaining gap from the Solidly thorough-testing pass: the
offline mock (sload(0) stub) couldn't exercise the real getAmountOut/Sync
ABIs or the storage layout. This adds an env-gated #[ignore] parity test that
forks Base at a pinned block and validates against a live Aerodrome WETH/USDC
volatile pool:

  1. cold-start decodes the real token0/token1 from the configured token slots
     (proves slots 13/14),
  2. the configured reserve slots hold the pool's authoritative
     reserve0()/reserve1() (proves slots 20/21),
  3. simulate_swap (getAmountOut) == the same call via eth_call at the fork
     block (on-chain ground truth).

The storage layout was verified empirically (eth_getStorageAt scan matched
against the pool view fns) before being baked into the test as constants.
Confirmed Aerodrome keeps token0/token1 in storage (not immutable code), so
the 4-slot SolidlyStorageLayout holds.

Base RPC is taken from E2E_BASE_RPC_URL, or derived from E2E_RPC_URL by
swapping the Alchemy eth-mainnet host for base-mainnet. fork_cache/eth_call
helpers now take a block param so mainnet and Base forks share one harness;
the test entry gains the solidly-v2 required-feature.

Verified: all 4 RPC parity tests pass live (V2/V3/Balancer mainnet + Solidly
Base); fmt + clippy --all-targets --all-features -D warnings clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pter

Hardening #4: Solidly V2 (Aerodrome/Velodrome) adapter
…driver

Hardening #2: DeferredWork driver (run_deferred)
…ick-scan

Hardening #1: V3 cold-start multi-word adaptive tick scan
@KaiCode2
KaiCode2 merged commit 9fad488 into main Jun 24, 2026
1 check passed
@KaiCode2
KaiCode2 deleted the codex/e2e-adapter-pipeline branch June 25, 2026 09:55
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