Skip to content

hardening(tier-0): correctness & API-freeze fixes before v0.1.0 - #27

Merged
KaiCode2 merged 13 commits into
mainfrom
hardening/tier-0-correctness
Jul 7, 2026
Merged

hardening(tier-0): correctness & API-freeze fixes before v0.1.0#27
KaiCode2 merged 13 commits into
mainfrom
hardening/tier-0-correctness

Conversation

@KaiCode2

@KaiCode2 KaiCode2 commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Tier 0 — correctness & API-freeze (pre-v0.1.0 hardening)

First of three stacked hardening PRs from the pre-release review, addressing
the P0/correctness + API-freeze items before the v0.1.0 tag. Base: main.
(Tier 1 = docs-truth + robustness; Tier 2 = ergonomics/polish, to follow.)

Changes

Item Fix
PancakeSwap V3 Swap routing Pancake's Swap appends two uint128 fields → distinct topic0. event_sources now subscribes the Pancake topic for PancakeV3 pools and decode_event/decode_swap validate against the matching ABI (shared body decode of words 2/3/4). Previously a Pancake pool's swaps never routed.
Slipstream fee tickSpacing-keyed discovery has no on-chain fee mapping, so discovered registrations leave V3Metadata.fee unset (was a bogus Some(0)); simulate_swap now surfaces MissingMetadata("V3 fee") instead of quoting at fee 0.
V3-family one-shot sync spec cold_start_many uses the canonical V3SyncSpec::uniswap only for genuine Uniswap V3; PancakeSwap V3 / Slipstream use V3SyncSpec::core (slot0 + liquidity + ticks), so hydration never injects Uniswap's unverified fee-growth/observation slots into forks.
cold_start_many finalization The fast path warms the cache and marks Ready without running the planner's finish(); it is now gated on fast_metadata_complete() so metadata-incomplete registrations (e.g. a bare V2 pool missing token0/token1) fall back to the multi-round cold_start that decodes + merges them.
Panic fix A V3 layout with non-positive tick_spacing no longer reaches full_word_range/v3_word_position (which assert > 0) via the fast path — v3_sync_spec returns None and the pool falls back.
Batch robustness A malformed log for a watched topic no longer aborts the batch: AmmReactiveHandler::handle emits a NoStateEffect + amm.decode_error hook (was Err(HandlerError), which aborts ingest_batch); AdapterDriver::apply_logs isolates DriverError::Decode and continues.
API freeze #[non_exhaustive] on V3StorageLayout and SolidlyStorageLayout.

Tests

+6 new (V3 sync-spec selection ×3, driver batch-isolation, reactive batch-isolation, cold_start_many fallback-merge), plus the Pancake reactive test rewritten to the real topic + 9-field body and a Slipstream fee == None assertion.

Verification

Full CI-parity gate green locally: cargo test (all-features / default / no-default), clippy -D warnings (all-features + 7-way per-protocol isolation + no-default), fmt --check, doc -D warnings, experimental-protocols ×2, heavy-dep guard, and MSRV 1.88.

A skeptical review agent audited the diff (verdict SHIP, only Low/Nit findings); two refinements were folded in (a corrected decode_swap comment about alloy's trailing-data tolerance, and requiring tokens in the Solidly fast-path gate for metadata consistency with the fallback).

Note: the commit is unsigned — the 1Password SSH agent was hung this session (commit-signing + SSH push both blocked on it), so this was committed with signing off and pushed over HTTPS. Re-sign on merge if branch protection requires it.

🤖 Generated with Claude Code

KaiCode2 and others added 2 commits July 7, 2026 12:15
Pre-release Tier 0 of the pre-launch checklist (correctness + API freeze):

- PancakeSwap V3 Swap routing: Pancake's Swap appends two uint128 fields, so
  its topic0 differs. `event_sources` subscribes the Pancake topic for
  PancakeV3 pools and `decode_event` validates against the matching ABI
  (shared body decode of words 2/3/4). Previously a Pancake pool's swaps never
  routed. The pancake reactive test now uses the real topic + 9-field body.

- Slipstream fee: tickSpacing-keyed discovery has no on-chain fee mapping, so
  discovered registrations leave `V3Metadata.fee` UNSET (was a bogus Some(0));
  `simulate_swap` surfaces `MissingMetadata("V3 fee")` instead of quoting at
  fee 0 (Slipstream is discovery-only for quoting).

- V3-family one-shot full-sync spec: use canonical `V3SyncSpec::uniswap` only
  for genuine Uniswap V3, and `V3SyncSpec::core` (slot0 + liquidity + ticks)
  for Pancake/Slipstream, whose fee-growth/observation slot positions are
  unverified — no longer injects Uniswap's aux slots into forks.

- cold_start_many finalization: the fast path warms the cache and marks Ready
  WITHOUT running the planner's finish(); it is now gated on
  `fast_metadata_complete()` so metadata-incomplete registrations (e.g. a bare
  V2 pool missing token0/token1) fall back to the multi-round cold_start that
  decodes + merges them.

- Panic fix: a V3 layout with non-positive tick_spacing no longer reaches
  full_word_range/v3_word_position (which assert > 0) via the fast path —
  `v3_sync_spec` returns None and the pool falls back.

- Batch robustness: a malformed log for a watched topic no longer aborts the
  batch. `AmmReactiveHandler::handle` emits a NoStateEffect + `amm.decode_error`
  hook; `AdapterDriver::apply_logs` isolates `DriverError::Decode` and continues.

- API freeze: `#[non_exhaustive]` on V3StorageLayout and SolidlyStorageLayout.

Tests +6 (cold_start spec-selection x3, driver isolation, reactive batch
isolation, cold_start_many fallback-merge) plus the Pancake rewrite and a
Slipstream fee=None assertion. Full gate green: all-features / no-default /
7-way isolation clippy -D warnings, fmt, doc -D warnings, experimental x2,
MSRV 1.88.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second of the pre-release hardening PRs (P1 correctness + doc accuracy),
stacked on tier-0.

- SimError typed source: `SimError::Execution` now carries a boxed
  `dyn Error + Send + Sync` (was `String`) with a `source()` impl, matching the
  crate's other boxed-source facades (CacheError/DriverError). `quote_via_call`
  boxes the CacheError so a consumer can downcast the underlying cause. SimError
  drops Clone/PartialEq/Eq (boxed payload); affected test assertions moved to
  `matches!`, and facade_typing gains a downcast-source assertion.

- V3 offline completeness: the windowed cold-start planner now warms ALL FOUR
  `Tick.Info` words of each initialized tick (was {0, 3}); a tick-crossing quote
  reads feeGrowthOutside{0,1}X128 (words 1/2) too, so {0,3} forced a mid-quote
  lazy fetch. The reactive Mint/Burn resync (repair.rs) refreshes all four to
  match (a tick flip changes feeGrowthOutside). Golden/oracle tests updated
  (7→11, 6→10 slots) and the cold-start test now asserts all four are warmed.

- README doc-truth:
  * headline "purely from logs / no RPC / fully offline" narrowed to the real
    exact-write-vs-resync split;
  * "pool's own quote entrypoint" → "protocol's canonical quote entrypoint"
    (V2/V3 quote via Router/Quoter, not the pool);
  * cut the "Balancer/Curve seeding … in scope … before complete" roadmap leak
    (now: seeding covers V2/V3; others fetch code lazily — a latency-only diff);
  * added a Solidly offline caveat (getAmountOut also reads stable/decimals +
    an external factory.getFee()).

- Factory-preset comment reconciliation: the gated RPC-test headers/messages
  claimed "placeholder … TODO(slice)" while the shipped presets carry the
  on-chain-confirmed constants (Pancake getPool slot 2 / feeAmountTickSpacing
  slot 1 + verify_derivations on + create2 pinned; Aerodrome getPool slot 5 +
  SOLIDLY_AERODROME_LAYOUT). Comments now describe the tests as confirming the
  shipped constants. Velodrome doc reworded (reuses Aerodrome's Base-verified
  constants; unverified for Optimism). Slipstream doc: discovery-only quoting
  (fee left unset → MissingMetadata).

Full gate green: tests (all/default/no-default), clippy -D warnings
(all-features + 7-way isolation + no-default), fmt, doc -D warnings,
experimental x2, heavy-dep guard, MSRV 1.88.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
KaiCode2 and others added 2 commits July 7, 2026 12:47
Part 1 of the Tier 2 polish PR (missing_docs sweep follows as its own commit).

- Configurable quote sender: `SimConfig.from` (default `Address::ZERO`) + a new
  public `quote_via_call_from(cache, from, target, calldata)`; `quote_via_call`
  now delegates to it with ZERO (unchanged behavior). Every adapter's
  `simulate_swap` threads `config.from` so a caller can quote against a target
  that gates on `msg.sender`. facade_typing gains a sender-threading test.

- New `docs/protocol-support-matrix.md`: a per-protocol v0.1 capability matrix
  (cold-start, offline-after-cold-start, exact-write vs resync, factory
  discovery, known limitations), linked from the README's protocol table.

- README: note that the published crate excludes `tests/`, so `cargo test` on a
  crates.io download runs only inline unit tests (clone for the full suite).

- docs/benchmarks.md: a point-in-time / reproduce note on the results (medians
  are one host/run; `cargo bench` emits full Criterion stats + HTML reports).

Full gate green: tests (all/default/no-default), clippy -D warnings, fmt, doc.

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

Part 2 of the Tier 2 polish PR.

Enable `#![warn(missing_docs)]` at the crate root (CI's `-D warnings` promotes it
to an error), and document every previously-undocumented public item so the
surface stays fully documented as it grows — ~300 items across types, factory,
bytecode, registry, cache, traits, storage, reactive, driver, the adapter
structs, and the module declarations. Docs only; no behavior change.

Verified: missing_docs = 0 under all-features / no-default / experimental;
clippy -D warnings (all-features + no-default + isolation), doc -D warnings, fmt,
and tests all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
KaiCode2 and others added 9 commits July 7, 2026 14:09
…onal code seed

CurveColdStartPlanner now skips discovery when CurveMetadata.discovered_slots is already known (a prior discovery, a trace, or a registry): no pool-account/bytecode fetch and no cold-cache get_dy slot-faulting, just a single verify round over the known slots. This makes a known-read-set single-pool cold_start as cheap as the bundled cold_start_many storage-program path Uniswap V2/V3 use. The discover->verify path is unchanged when discovered_slots is empty.

Adds optional CurveMetadata.code_seed (+ with_code_seed builder + CurveAdapter::code_seeds): a caller-supplied Vyper runtime, verified once against on-chain EXTCODEHASH (mismatch -> purged -> lazy fetch), removing the one lazy code fetch a Curve pool otherwise pays on its first simulate_swap. Additive, non-breaking.

Tests: verify-only skips-discovery + unfetchable-slot repair (cold_start_adoption), code_seeds unit test (curve), positive Curve one-shot classification (bootstrap_many). Adds examples/curve_cold_start_phases.rs and updates docs (curve-adapter, README, benchmarks). 216 tests pass; clippy/doc/all-features clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Uniswap V3 `Mint`/`Burn` previously always emitted a tick-range resync (an RPC
round trip). They now event-source the affected state directly wherever it is
already warm, matching the exact-write posture of the V2/Solidly `Sync` path, and
resync ONLY for ticks outside the warmed window.

The event carries the exact liquidity delta and the boundary ticks; the current
tick comes from cached `slot0`. For each warm boundary tick the adapter
read-modify-writes the packed `Tick.Info` word 0 — `liquidityGross` (low 128) and
`liquidityNet` (high 128, opposite signs for the lower vs upper tick) — and
toggles the `tickBitmap` bit on an init/clear (Uniswap `flipTick` is an XOR); the
in-range global `liquidity` slot is adjusted by `±amount`. Those are exactly the
slots a `QuoterV2` swap reads; `feeGrowthOutside`/`positions` are accounting-only
(they do not affect `amountOut`) and are intentionally not maintained.

A cold boundary tick (word 0 not cached) cannot be read-modify-written, so its
info + bitmap slots fall back to a `VerifySlots` resync — the hybrid
write-where-warm / resync-cold policy. No-layout pools still degrade to a
conservative whole-storage invalidation.

Correctness rests on the reactive runtime applying each input's updates before the
next input's decode (verified in `ingest_batch_direct`), so read-modify-write sees
prior events in the batch. Bitmap flips for both ticks in a shared word are
accumulated into one combined write (two full-slot writes from the same pre-event
view would not compose).

Tests: 9 inline unit tests (packing round-trip incl. negative net, all four
mint/burn sign combos, init/clear detection, overflow rejection, bit-position vs
Uniswap `position`); reactive integration tests for warm direct writes,
out-of-range liquidity, burn-to-zero bitmap clear (shared word), and cold-tick
resync fallback; Pancake/Slipstream layout coverage retained. `RepairAction::
V3TickRange` is retained as a reserved variant. Full gate green (tests
all/default/no-default, clippy -D warnings ×2, doc, fmt, missing_docs=0).

Follow-up: an env-gated RPC parity test (apply a real Mint/Burn, compare
event-sourced slots to eth_getStorageAt ground truth at the post-event block) —
deferred pending an archive endpoint to author it against.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ports the Curve verify-only fast path to BalancerV2ColdStartPlanner: when BalancerV2Metadata.balance_slots is already known (a prior discovery or a trace), skip the getPoolTokens discovery -- no vault-account fetch, no cold-cache faulting -- and warm exactly those slots in a single verify round, matching the bundled cold_start_many storage-program path. Config-supplied tokens are preserved (no getPoolTokens decode repopulates them). The discover->verify path is unchanged when balance_slots is empty.

Adds balancer_cold_start_verify_only_skips_discovery. 217 tests pass; clippy/all-features/doc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds tests/v3_liquidity_rpc.rs (env-gated, #[ignore]): for a real add- and
remove-liquidity transaction, fetch the exact per-tx storage diff via
trace_replayTransaction(stateDiff), warm the pre-tx state, apply the event
through the adapter, and assert the event-sourced writes reproduce the on-chain
post-tx values for every slot the adapter maintains — each boundary tick's packed
Tick.Info word 0 (liquidityGross/liquidityNet, incl. a negative net in two's
complement) and the in-range global liquidity.

Verified live against a mainnet archive+trace endpoint: a JIT add + remove of the
same liquidity on the USDC/WETH 0.05% pool — BOTH the Mint and the Burn reproduce
the on-chain per-tx storage exactly (tickLower/tickUpper word0 + global
liquidity). This is the RPC parity check deferred in the feature commit; it now
exists and passes.

serde_json added as a dev-dependency (parses the trace stateDiff JSON). Full gate
green: clippy -D warnings (all-features + no-default), offline tests, fmt, doc,
missing_docs=0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(uniswap-v3): event-source Mint/Burn liquidity (no RPC where warm)
perf(cold-start): verify-only fast path for Curve + Balancer (+ optional Curve code seed)
hardening(tier-2): polish, ergonomics, full-surface docs, V3 liquidity, and cold-start fast paths
hardening(tier-1+): doc-truth, tier-2 polish, V3 liquidity, and cold-start fast paths
@KaiCode2
KaiCode2 merged commit 7139971 into main Jul 7, 2026
9 checks passed
@KaiCode2
KaiCode2 deleted the hardening/tier-0-correctness branch July 8, 2026 09:37
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