feat(ekubo): decode pool state from the anonymous swap log - #9970
feat(ekubo): decode pool state from the anonymous swap log#9970moodysalem wants to merge 2 commits into
Conversation
PR SummaryLow Risk Overview Non-swap branches (
Reviewed by Cursor Bugbot for commit f74d077. Configure here. |
Ekubo Core emits swaps as an anonymous log (topic0 is null), so there is
no decoded `core_evt_swapped` table on Dune and spellbook parses the raw
log bytes itself. The parser stopped after the two amount deltas, leaving
the last 32 bytes of the 116-byte payload unused.
Those bytes carry the pool state after the swap:
off len field
0 20 locker
20 32 poolId
52 16 amount0 (int128, signed) already decoded
68 16 amount1 (int128, signed) already decoded
84 12 sqrtRatio (uint96 float) added
96 4 tick (int32, signed) added
100 16 liquidity (uint128) added
`sqrt_ratio_after` is exposed as the raw uint96 float and, via the new
`ekubo_sqrt_ratio_to_fixed` macro, as `sqrt_ratio_after_fixed` in Q128
form (sqrt(token1/token0) * 2^128), which is what the contracts and the
interface expose. The float packs a 2-bit exponent above a 94-bit
mantissa: fixed = mantissa << (32 * exponent + 2).
Verified against independent RPC reads of CoreDataFetcher.poolState:
tick, sqrtRatio and liquidity decoded from tx 0x24b3dff9 match the pool
state read at the following block exactly, and the Q128 conversion
reproduces 2^128 * 1.000001^(tick/2) to within one tick across exponents
0-2 and ticks from -19.8M to +27M.
Also pins the swap filter to `length(data) = 116`. Every anonymous log
emitted by both the v1 and v3 Core contracts is 116 bytes today
(5.27M logs checked), so this is a no-op on current data and guards the
offsets if another anonymous event is ever added.
Non-swap events (modify_liquidity, fees_collected, fees_accumulated)
carry null for the three new columns.
Claude-Session: https://claude.ai/code/session_016Pzo1k2MwRueV2unUJq8Jh
f74d077 to
cbb631b
Compare
|
@jeff-dude @tomfutago — gentle bump on this one, it's been open since Aug 26 without a reviewer assigned. Just rebased onto the latest Happy to make any adjustments you'd like. Thanks! |
|
Following up — still no reviewer on this one. It's been open since Aug 26, and CI is fully green (build, CLA, and all @jeff-dude @tomfutago — could one of you assign a reviewer, or point me at whoever owns the For context, the whole change is additive: three decoded columns on Ekubo swap rows plus one projection macro, guarded by |
|
Third nudge, and this time tagging the team — @duneanalytics/team-curated-data. I think I see why this has been silent: Status: fully green (build, CLA, all @jeff-dude @tomfutago — could someone assign a reviewer? Happy to make any changes you'd like. |
Summary
Ekubo's swap parser currently reads the token amount deltas but leaves the post-swap pool state undecoded. This PR decodes the final 32 bytes of Core's 116-byte anonymous swap log, making price and active-liquidity history available directly from the liquidity-event tables.
Adds four columns to the Ethereum v1/v3 base models and exposes them through
ekubo.base_liquidity_eventsandekubo.liquidity_events. All four are null for non-swap events.sqrt_ratio_afterSqrtRatiofloatsqrt_ratio_after_fixedsqrt(token1/token0 in raw units) * 2^128tick_afterliquidity_afterImplementation
ekubo_compatible_liquidity_eventsreadssqrtRatioat byte offset 84 (12 bytes),tickat 96 (4 bytes, signed), andliquidityat 100 (16 bytes). Offsets are zero-based; SQL substrings are one-based.ekubo_sqrt_ratio_to_fixedconverts the packed value usingfixed = mantissa << (32 * exponent + 2), matching the contract'stoFixed. The upper two bits hold the exponent; the remaining 94 bits hold the mantissa. FourCASEbranches use uint256 multiplication to implement the shifts.length(data) = 116alongsidetopic0 is nullto reject unexpected anonymous-log lengths before decoding.Compatibility and rollout
Existing columns retain their values. The length guard excluded no rows in the original validation: all 5.27 million anonymous logs checked across both Core versions were 116 bytes.
dex.tradesdoes not consume the new columns.Historical values in the incremental models require Dune's normal post-merge full refresh.
Validation
CoreDataFetcher.poolStateRPC read at the following block: tick-165, fixed sqrt ratio340254383154770049453632155902269194240, and liquidity10035484459699841.