fix: GeckoTerminal /gt/events must emit per-event post-trade reserves, not the live pair_reserves snapshot #684
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#684
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Parent
Child / follow-up of #646 (GeckoTerminal Non-EVM
/gt/*). Listing catalog parent: #639. Do not reopen#646to change form copy or/cg/*.Summary
GeckoTerminal
/gt/eventsattaches the livepair_reservessnapshot to every swap / join / exit. Integrators therefore see identicalreserves.asset0/reserves.asset1across many blocks even whenasset0In/asset1Outprove the pool moved. That breaks GT liquidity, price impact, and chart reconstruction.Bundle persist post-event reserves at ingest, serve them from
/gt/events, backfill, and docs / verify in one issue. Do not split “SQL column” vs “GET JSON” vs “backfill script.”Report (anonymized)
An on-chain listing integrator crawling
GET https://indexer.dex.cl8y.com/gt/eventson a columbus-5 factory pair observed two swaps on the same pool with unchanged reserves:blockNumber3012891830129103(~185 blocks later)eventTypeswapswapterra1q5karg99p5hc0jvplgn2fd8akg39r4ss00cu5rwkm600yqvmssgqhwqvq0asset0In/asset1Out0.831452971704399333/8016.7814930.775464141105197631/7412.024205reserves.asset0185.074384741721120019reserves.asset11793851.188616If reserves were post-trade AMM state, Event B must differ from Event A by at least Event A’s pool delta (hybrid book may take some of the notional; the pool legs still cannot leave both reserves bit-identical after a non-zero
pool_return_amount).Do not paste integrator chat, handles, or emails on this issue.
Current codebase
#646shipped the Integration API (spec):/gt/latest-block,/gt/asset,/gt/pair,/gt/events. The module documents the defect as intended:GET /gt/eventsindexer/src/api/gt.rsgt_events: loads swaps + join/exit in[fromBlock, toBlock](max 2000), decimalizes amounts, then callscurrent_reserves()which looks up one(reserve_0, reserve_1)perpair_idfromSELECT pair_id, reserve_0, reserve_1 FROM pair_reserves. Every event for that pair in the window gets the same humanized snapshot. Missing row →"0"/"0".pair_reservesindexer/migrations/20260605010000_pair_reserves.sql: one current row per pair (PKpair_id). Written bybook_snapshot.rs(~BOOK_SNAPSHOT_INTERVAL_MS, default 10s) from LCDPool {}. Built for the hybrid solver (#279 / #322), not a reserve history. Upsert replaces the prior row.swap_eventsoffer_amount,return_amount, optional hybridpool_return_amount/book_return_amount/limit_book_offer_consumed(insert_swap). Noreserve_0/reserve_1. Parser:parser.rsParsedSwap— same gap.liquidity_eventsadd/removewithasset_0_amount/asset_1_amount/lp_amount. No post-event reserves.smartcontracts/contracts/pair/src/contract.rsupdatesRESERVESon swap / provide / withdraw. Swap attrs areoffer_amount,return_amount,pool_return_amount,book_return_amount,commission_amount, … — noreserve_0/reserve_1. Commission stays in the pool (gross_outputleaves reserves; fee is not an extra reserve drain). Book fills do not mutateRESERVES. Sweep does not mutateRESERVES.pool_input = offer_amount - limit_book_offer_consumed(when book attrs exist; else treat as pool-only).gross_output = pool_return_amount + commission_amount(pool commission attr). Applying fulloffer/returnto reserves is wrong on hybrid swaps.docs/indexer-invariants.mdrow GeckoTerminal Integration API (#646) still says “Reserves = currentpair_reservessnapshot.”scripts/qa/verify-issue-646.sh+make verify-issue-646+indexer/tests/api_gt.rsassert a single snapshot ("2") on the seeded swap — they lock in the bug./cg/tickersliquidity_in_usdis mislabeled 24h volume (#224 / #569) — different crawler. Protocol TVL /pair_liquidity_usdstay current-state rollups. Do not point GT at/cg/*.Why a new implementation is needed
reservesas post-event pool state. Their crawler builds liquidity and impact from the event stream. A static snapshot makes every historical swap look like it printed against today’s (or last-snapshot) curve.pair_reservesis a solver mirror, not a tape. One row, replaced every ~10s. Even if the snapshot loop were instant,GET /gt/events?fromBlock=A&toBlock=Bwould still stamp the same current row on Event A and Event B.offer_amount/return_amountalone. Hybrid book takes part of the notional; commission stays in the pool; tax / wrap hops are out of band. NaiveR += in; R -= outwill poison GT liquidity on any pair with resting limits.https://indexer.dex.cl8y.com/gtwas submitted on the#646DEX Addition form. This is a production integrator defect, not a docs nit.Constraints / guardrails
pair_reservessemantics. It remains the current LCD mirror for Phase 1c / hub / protocol TVL. GT must not JOIN it for historical events.RESERVESafter that event (factory asset_0 / asset_1, decimalized like amounts). Book escrow, parked dust, wallet balances, wrap-mapper inventory, and CW20 donations are out. Sweep /Skimdoes not change reported reserves.book_return_amount, zeropool_return_amount) must leave reserves unchanged vs the previous event on that pair.reserves.asset0/asset1follow factoryasset_0/asset_1, not offer/ask. Same decimalize /format_decas#646(no scientific notation).#562). Do not leak them while adding columns.Pool {}per event. Nopair_reserves ⋈on the request path for event reserves. No unbounded walk ofswap_eventsto rebuild history per GET.eventType,txnId,reserves.asset0/asset1). Do not rename fields. Do not add USD / TVL / hub marks to/gt/events.pair_reservesusing stored hybrid legs; if a row cannot be reconstructed, omit reserves or emit"0"only if the spec forbids omit — pick one, document it, test it. Do not invent$1UST1 /2.5×USTR.swap/provide_liquidity/withdraw_liquiditygainreserve_0/reserve_1attrs, parser must take them only from the runtime-reserved_contract_address(#285). Forged attrs from another contract are ignored. A factory migrate is not required to close this issue if ingest reconstruction is correct going forward.fromBlock/toBlockstayi64,to >= from, span ≤ 2000,from > latest→[]. Invalid range → 400. No raw SQL from query strings./cg/tickersliquidity_in_usdor Protocol TVL here.Relevant files
indexer/src/api/gt.rscurrent_reserves→ per-event columns; module docsindexer/src/api/mod.rs/gt/*routes (no new path required)indexer/src/indexer/parser.rsindexer/src/db/queries/swap_events.rsinsert_swap+ columnsindexer/src/db/queries/liquidity.rsinsert_liquidity_event+ columnsindexer/src/indexer/book_snapshot.rsindexer/src/db/queries/pair_reserves.rsindexer/migrations/reserve_0/reserve_1onswap_events+liquidity_events(nullable for pre-fix rows); optional backfill SQL/jobindexer/tests/api_gt.rssmartcontracts/contracts/pair/src/contract.rsreserve_0/reserve_1docs/indexer-invariants.mddocs/CG_CMC_COMPLIANCE.mdscripts/geckoterminal/README.mdscripts/qa/verify-issue-646.sh#646routes; this ticket getsverify-issue-NNNskills/AGENTS_LISTINGS.md#639Recommended direction
swap_events.reserve_0/reserve_1andliquidity_events.reserve_0/reserve_1(NUMERIC(38,0), nullable). Values are raw factory units (same asoffer_amount), not human.Pool {}/pair_reservesonly as seed at first event after deploy). For each swap in block/tx/id order: if wasm attrs present and#285-scoped, use them; elsepool_input = offer - coalesce(limit_book_offer_consumed, 0),gross_out = pool_return + pool_commission(NULL hybrid → pool-only:pool_input = offer,gross_outfrom CPAMM orreturn + commission). Apply to the offer/ask legs, then map back to asset_0/asset_1. Join/exit:± asset_0_amount/± asset_1_amount. Persist after the event.gt_events: SELECT the row’sreserve_*, decimalize. Do not callcurrent_reserves()againstpair_reserves.pair_reserves(or LCDPool {}at tip), walk events newest → oldest, invert the same deltas, write columns. Skip / leave NULL when hybrid legs are missing and inversion is ambiguous. Never write “today’s snapshot” onto every historical row.make verify-issue-NNN. Optional pair wasm attrs can land in the same MR or a follow-up migrate; indexer must work without them.Do not query LCD
Pool {}per event on GET. Do not treatreturn_amount(pool+book) as the reserve drain.Acceptance criteria
/gt/eventswindow have differentreserveswhenpool_return_amount > 0(or pool-only offer/return > 0). Values match post-event AMMRESERVES(decimalized), not the livepair_reservesrow.reservesequal Event Nreservesplus Event N’s pool delta (input added to offer leg,gross_outputremoved from ask leg). Hybrid book notional does not move reserves.reservesequal the previous event (or seed) for that pair.amount0/amount1; exit decreases. Same persistence path as swaps.GET /gt/eventsdoes not SELECT frompair_reservesfor eventreserves(test /EXPLAIN/ grep). Solver snapshot loop unchanged.dexKeystayscl8y. Span > 2000 / inverted range still 400.format_decunchanged (6-dec and 18-dec). No scientific notation. Orientation is factory asset_0/asset_1.0policy. Backfill must not copy the current snapshot onto all rows of a pair.scripts/geckoterminal/README.md+make verify-issue-NNN.#646verify still passes for routes/form pack.Test plan (all paths)
Indexer API (
api_gt+ new ingest tests)reservesdiffer; second = first + deltapair_reservesupserted to an unrelated snapshotpool_return_amount = 0,book_return_amount > 0)swap_index0, 1)#646decimalizeasset1In/asset0Out; reserve map not swapped vs factory"0"or omit — never today’s snapshotfromBlock > latestevents: []toBlock < fromBlock/ span 2001/gt/pairstill 404events: []price <= 0or zero offer/return#646filter)GET /gt/latest-block//asset//pairIngest / backfill
+/-both legsON CONFLICT DO NOTHING(tx, pair, swap_index)Contracts (only if wasm attrs land)
reserve_0/reserve_1afterRESERVES.saveTest plan (attack, hack, abuse)
reserve_0from a non-pair contract (#285)_contract_addressfromBlock=1;DROP/toBlock=__proto__i64fromBlock=0&toBlock=2^63swap_eventshistory to rebuild reservespair_reserves/gt/eventshistory unchanged; solver mirror may move (out of scope)RESERVES, notbalanceOf(matches sweep design)fromBlockwindowNUMERIC(38,0); GETformat_decdoes not emite+that breaks GT parse/gt/eventscache)#615)$1, or2.5×to GT reservesVerification criteria
Live (after Coolify indexer migrate), without publishing chat:
docs/indexer-invariants.mdno longer says GT reserves are the current snapshot.pair_reservesstill one-row upsert; route/solve hybrid tests unchanged.Out of scope
/cg/*tickerliquidity_in_usdrename#569/#655)GET /api/v2/networks(vendor)/gt/eventsmarked as related to #646
mentioned in issue #646
mentioned in issue #685
marked as related to #685
mentioned in issue #686
mentioned in commit
9b3fa7fff9mentioned in merge request !1188
mentioned in merge request !1189
mentioned in commit
6ae8b7270bmentioned in commit
d9aa9432a0mentioned in commit
cf1c0a63e6mentioned in issue #688
marked as related to #688
Merged as !1189 onto
main(d9aa9432). Local AC1–AC10 passed (make verify-issue-684,make verify-issue-646). Conflicts vs #686/#687 leftovers resolved (kept GT post-event reserves row + Llama priced-SUM rows).Leftovers (do not reopen this issue unless a merged invariant is wrong): Coolify migrate
20260827160000_gt_event_post_reserves.sql, indexer redeploy,cl8y-dex-indexer backfill-gt-event-reserves, then live AC11 two-swap window. Tracked on #688.CI was
failed(ci_quota_exceeded); not waited; not a product signal.Non-blocking: first catch-up events stay NULL until backfill; same-tx apply order is all swaps then liquidity.
mentioned in merge request !1193