Indexer stores swap price with no base/quote orientation — 24h high/low and CG/CMC feeds flip between P and 1/P by trade direction #466
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#466
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?
Came out of the pre-launch security/data-integrity sweep (rolls up under the #381 hardening umbrella). This one's a data bug, not a contract exploit, but it's already feeding garbage to CoinGecko/CoinMarketCap and the dapp, so I'm flagging it hot.
What it is
The indexer computes and stores a swap's
pricewithout any reference to the pair's base/quote orientation. Every swap just recordsreturn_amount / offer_amount— but "offer" and "ask" flip depending on which way the trade went. So a buy storesPand the very next sell stores1/Pinto the samepricecolumn. OHLC, 24h high/low, last_price, and bid/ask all end up mixing a number and its reciprocal.Where / the mechanism
indexer/src/indexer/parser.rs:400There's zero reference to
pair.asset_0/pair.asset_1here. The pair is resolved just above (pairs::get_pair_by_address, :375) and both leg asset ids are known (offer_asset_id/ask_asset_id, :397-398), but none of that is used to normalize the ratio. Whatever leg happened to be the offer becomes the denominator.That raw value then:
swap_events.priceviainsert_swap(...)at :433, andcandle_builder::update_candles_for_swap(...)at :448 as the candle price.Downstream it's consumed orientation-blind:
indexer/src/db/queries/swap_events.rs:227-228—MAX(price) AS high,MIN(price) AS lowover the 24h window, plus open/close picked by timestamp order (:242-249). So across a window with trades in both directions,highis the max of {P values and 1/P values} andlowis the min — the "low" is quite literally the reverse-direction price.indexer/src/api/cg.rs:170-188—last_price=close_price,high/lowfrom the stats above, andbid = last * 0.999/ask = last * 1.001(:177-178) built off that same last_price.cmc.rsreads the same stats.Same root cause also poisons the volume rows right there in the query (
SUM(offer_amount) AS volume_base,SUM(return_amount) AS volume_quote, :223-224) — those sum offer/return regardless of direction, so base and quote volume are cross-contaminated too. But the price inversion is the loud one.How to hit it
Nothing exotic — normal two-sided trading triggers it:
asset_1/asset_0(e.g. 50.5).asset_0/asset_1(the reciprocal, ~0.0198) into the same column.MAX(price)picks up the forward-direction ~50,MIN(price)picks up the reverse-direction ~0.0198.You don't even need to reconstruct it — it's live on the QA indexer right now. From
/cg/tickers:Every one of those "lows" is just
1 / (a forward-direction price). That's not a market low, it's the inverse unit sitting in the same column.Impact
Fix direction
Normalize price to a fixed base/quote orientation at write time, in
process_swapbefore it's stored and before it hits the candle builder:pair.asset_0/pair.asset_1(whatever convention CG/CMC ticker_id already uses — cg.rs buildsa0.symbol_a1.symbolwith asset_0 as base).price = return/offer; if the offer leg is the quote asset, invert it (price = offer/return). So the column always means "quote per base" regardless of trade direction.swap_events.price(and the volume split) for existing rows and rebuild candles, since the historical column is already mixed.Happy to write the normalization + a repro test (two-sided swap → assert high/low aren't reciprocals) once we agree on the canonical orientation.
@PlasticDigits — this is going out to CG/CMC on the public feed and it's wrong right now, so I'd treat it as launch-blocking for the data side. Wanted it on your radar before any listing push.
mentioned in commit
ff28fbba19mentioned in merge request !1008
mentioned in commit
a5bbd55e16Verification — #466 (swap price orientation)
Result: PASS — fix merged on
main(ff28fbba/ MRfix/466-swap-price-orientation).Acceptance criteria
swap_events.priceto quote per base (asset_1/asset_0) at index timeindexer/src/indexer/swap_orientation.rs+parser.rscallsorient_swap_legbeforeinsert_swapandupdate_candles_for_swapreturn/offer; offer quote → invert to same canonical pricecargo test swap_orientation --lib— 3/3high/loware not reciprocalscargo test --test swap_price_orientation oriented_two_sided_swaps_high_low_are_not_reciprocals -- --test-threads=1cargo test --test swap_price_orientation oriented_volume_aggregation_sums_base_and_quote_legs -- --test-threads=1;get_24h_stats_for_pair/get_24h_stats_all_pairsSQL usesCASE WHEN offer_asset_id = asset_0_idcg.rsreadsclose_price/ orientedhigh/low/ volumes from stats;cargo test --test api_aggregator_batch -- --test-threads=1— 5/5migrations/20260707000000_normalize_swap_price_orientation.sqlrecomputespriceandTRUNCATE candlesdocs/indexer-invariants.md(#466 row),docs/CG_CMC_COMPLIANCE.mdEnvironment
make setup-indexer-postgresSKIP
/cg/tickersreciprocal-low checkseed-qa --clean(or candle rebuild) on that environment — not available in this verify VMFollow-ups
cargo run -- seed-qa --cleanon QA or per-pair rebuild), then spot-check/cg/tickersand/cmc/summarythatlowis no longer1/high.mentioned in issue #522
marked as related to #522
mentioned in merge request !1055
mentioned in issue #524
marked as related to #524
mentioned in issue #543
mentioned in issue #564