feat(uniswap-v3): event-source Mint/Burn liquidity (no RPC where warm) - #30
Merged
Conversation
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>
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>
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.
Event-source Uniswap V3
Mint/Burnliquidity (no RPC where warm)Closes the largest reactive gap: V3 liquidity events 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
Syncpath — and resync only for ticks outside the warmed window.Stacked on the tier-2 tip (
hardening/tier-2-polish) so it inherits the pre-release hardening; retarget tomainafter the tier stack lands.Mechanism
The event carries the exact liquidity delta (
amount) and the boundary ticks; the current tick comes from cachedslot0. For each warm boundary tick the adapter read-modify-writes:Tick.Infoword 0 —liquidityGross(low 128) andliquidityNet(high 128, two's complement, opposite signs for the lower vs. upper tick).tickBitmapbit — toggled on an init/clear (UniswapflipTickis an XOR); both ticks in a shared word are merged into one combined write.liquidity—±amountwhen the position straddles the current tick.Those are exactly the slots a
QuoterV2swap reads.feeGrowthOutsideand thepositionsmapping are accounting-only (they don't affectamountOut) and are intentionally not maintained.A cold boundary tick (word 0 not cached) can't be read-modify-written, so its info + bitmap slots fall back to a
VerifySlotsresync — the hybrid write-where-warm / resync-cold policy. No-layout pools still degrade to a conservative whole-storage invalidation.Correctness basis
evm-fork-cache'singest_batch_direct), so read-modify-write sees earlier events in the same batch.viewwould not compose (a bug the shared-word burn test caught during development).Tests
liquidityNet), all four mint/burn × lower/upper sign combos, init/clear detection, burn-more-than-gross rejection,v3_bit_positionvs Uniswapposition(incl. negative ticks).Exact), out-of-range leaves global liquidity, burn-to-zero clears the shared bitmap word, and cold-tick resync fallback. PancakeSwap/Slipstream layout coverage retained (cold-resync targets the right layout).Full offline gate green: tests (all-features / default / no-default),
clippy -D warnings(all-features + no-default),doc -D warnings,fmt,missing_docs = 0.Live RPC parity — verified ✅
tests/v3_liquidity_rpc.rs(env-gated,#[ignore]) validates against real chain state: for a real add- and remove-liquidity transaction it fetches the exact per-tx storage diff viatrace_replayTransaction(stateDiff), warms the pre-tx state, applies the event through the adapter, and asserts our event-sourced writes reproduce the on-chain post-tx values.Run live against a mainnet archive+trace endpoint on a JIT add+remove of the same liquidity on USDC/WETH 0.05% — both the Mint and the Burn reproduce the on-chain per-tx storage exactly: each boundary tick's packed
Tick.Infoword 0 (liquidityGross/liquidityNet, including a negative net in two's complement) and the in-range globalliquidity.🤖 Generated with Claude Code