Hardening #1: V3 cold-start multi-word adaptive tick scan - #14
Merged
KaiCode2 merged 7 commits intoJun 24, 2026
Merged
Conversation
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #13 (adapters pipeline). Base is the e2e-pipeline branch, so the diff is item #1 only. First of the post-merge hardening items.
What
UniswapV3ColdStartPlannerwarmed only the current-tick bitmap word, so a swap crossing into adjacent words fell back toEvmCache's lazy backend fetch. It now warms a bounded window[W0−2, W0+2](V3_TICK_WORD_RADIUS = 2,2R+1words) of bitmap words and their initialized ticks:window: Vec<(i16, U256)>+resolve_window— clamps to the valid V3 word range (±887272ticks) with overflow-safe i32 arithmetic.{0,3}Tick.Infoslots.HotSlotsOnly= slot0+liquidity only;Lazydefers the whole window; slot0-cold repair + config-metadata preservation untouched.Effect: moderate tick-crossing swaps (±2 words ≈ ±512 tick-spacings) are offline-pre-warmed instead of lazily fetched. A true outward-adaptive scan stays a future refinement (documented on the constant).
Tests
Manager-authored in
tests/cold_start_adoption.rs:v3_cold_start_warms_neighbouring_tick_words(neighbour bitmap + tick-info slots warmed — was red) andv3_cold_start_hot_slots_only_skips_tick_words(policy boundary).cold_start_adoption12 → 14.Test plan
Full matrix green:
fmt;clippydefault +--no-default-features(-D warnings);cargo testdefault +--no-default-features;cargo doc -D warnings. Spec:docs/hardening-1-v3-multiword-tick-scan-spec.md.🤖 Generated with Claude Code