Skip to content

feat(uniswap-v3): event-source Mint/Burn liquidity (no RPC where warm) - #30

Merged
KaiCode2 merged 2 commits into
hardening/tier-2-polishfrom
feat/v3-liquidity
Jul 7, 2026
Merged

feat(uniswap-v3): event-source Mint/Burn liquidity (no RPC where warm)#30
KaiCode2 merged 2 commits into
hardening/tier-2-polishfrom
feat/v3-liquidity

Conversation

@KaiCode2

@KaiCode2 KaiCode2 commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Event-source Uniswap V3 Mint/Burn liquidity (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 Sync path — 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 to main after the tier stack lands.

Mechanism

The event carries the exact liquidity delta (amount) and the boundary ticks; the current tick comes from cached slot0. For each warm boundary tick the adapter read-modify-writes:

  • Tick.Info word 0liquidityGross (low 128) and liquidityNet (high 128, two's complement, opposite signs for the lower vs. upper tick).
  • tickBitmap bit — toggled on an init/clear (Uniswap flipTick is an XOR); both ticks in a shared word are merged into one combined write.
  • global liquidity±amount when the position straddles the current tick.

Those are exactly the slots a QuoterV2 swap reads. feeGrowthOutside and the positions mapping are accounting-only (they don't affect amountOut) 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 VerifySlots resync — the hybrid write-where-warm / resync-cold policy. No-layout pools still degrade to a conservative whole-storage invalidation.

Correctness basis

  • The reactive runtime applies each input's updates before the next input's decode (verified in evm-fork-cache's ingest_batch_direct), so read-modify-write sees earlier events in the same batch.
  • Bitmap flips for two ticks in a shared word are accumulated into one write — two full-slot writes from the same pre-event view would not compose (a bug the shared-word burn test caught during development).

Tests

  • 9 inline unit tests: packing round-trip (incl. negative liquidityNet), all four mint/burn × lower/upper sign combos, init/clear detection, burn-more-than-gross rejection, v3_bit_position vs Uniswap position (incl. negative ticks).
  • Reactive integration: warm direct writes (liquidity + both ticks, no resync, quality 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 via trace_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.Info word 0 (liquidityGross/liquidityNet, including a negative net in two's complement) and the in-range global liquidity.

MINT  tickLower.word0: matches on-chain 0x7c5dc4e0…7c5e19058bda42e0
MINT  tickUpper.word0: matches on-chain 0xffffffff…7c63a1c56379ba71
MINT  global liquidity: matches on-chain 0xca4544475ef4b80a
BURN  tickLower.word0: matches on-chain 0xffffffff…032d88bda42e0
BURN  tickUpper.word0: matches on-chain 0xffffffff…5bb986379ba71
BURN  global liquidity: matches on-chain 0x4de75e1a5ef4b80a

Built in an isolated git worktree off the tier-2 tip so it did not touch the concurrent Curve-bytecode-seeding work in the main working tree. Commit is unsigned (1Password SSH agent hung this session; pushed over HTTPS).

🤖 Generated with Claude Code

KaiCode2 and others added 2 commits July 7, 2026 14:26
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>
@KaiCode2
KaiCode2 merged commit 76e70e6 into hardening/tier-2-polish Jul 7, 2026
9 checks passed
@KaiCode2
KaiCode2 deleted the feat/v3-liquidity 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