fix(indexer): pin registry USDT for Charts Price (USD) #1273

Merged
PlasticDigits merged 1 commit from issue/1258 into main 2026-09-16 22:52:43 +00:00

Summary

  • P522-Q now prices the pinned columbus-5 registry USDT CW20 at advisory $1 by contract identity (USDT_CW20_ADDRESS overlay on LocalTerra). Ticker USDT, Peg1/hub UST1, VFDUSD/FDUSD, CEX *USDT, and HubQuoteUsd.economic stay unpriced.
  • Ingest stamps price_usd / volume_usd for USDT-quoted swaps (human 6/18). NULL-only SQL + startup backfill rebuild candles; non-NULL history and UST1/cUSTC are untouched.
  • dApp fallback uses the same pin so Charts Price (USD) and 24h OHLC can render when indexer *_usd is still empty. invertUsd / #543 / #680 unchanged.

Fixes #1258

Test plan

  • make verify-issue-1258 (invariants/skills greps, indexer pair_price_usd lib tests, frontend pairPriceUsd + chartsPairStats)
  • cargo test --lib pair_price_usd / candle_mark / hub_usd / economic_usd / startup
  • Coolify/indexer migrate + restart; open Charts for a USDT-quoted pair (cLUNC/USDT) and confirm Price (USD) + 24h OHLC are not empty
  • Spoof symbol=USDT with a different contract stays —; UST1-quoted charts still use hub marks
## Summary - P522-Q now prices the pinned columbus-5 registry USDT CW20 at advisory $1 by contract identity (`USDT_CW20_ADDRESS` overlay on LocalTerra). Ticker `USDT`, Peg1/hub UST1, VFDUSD/FDUSD, CEX `*USDT`, and `HubQuoteUsd.economic` stay unpriced. - Ingest stamps `price_usd` / `volume_usd` for USDT-quoted swaps (human 6/18). NULL-only SQL + startup backfill rebuild candles; non-NULL history and UST1/cUSTC are untouched. - dApp fallback uses the same pin so Charts Price (USD) and 24h OHLC can render when indexer `*_usd` is still empty. invertUsd / #543 / #680 unchanged. Fixes #1258 ## Test plan - [x] `make verify-issue-1258` (invariants/skills greps, indexer `pair_price_usd` lib tests, frontend `pairPriceUsd` + `chartsPairStats`) - [x] `cargo test --lib pair_price_usd` / `candle_mark` / `hub_usd` / `economic_usd` / `startup` - [ ] Coolify/indexer migrate + restart; open Charts for a USDT-quoted pair (cLUNC/USDT) and confirm Price (USD) + 24h OHLC are not empty - [ ] Spoof `symbol=USDT` with a different contract stays `—`; UST1-quoted charts still use hub marks
fix(indexer): pin registry USDT for Charts Price (USD)
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
d682d58e26
P522-Q stamps advisory $1 on the columbus-5 registry USDT CW20 by
contract identity so USDT-quoted tape, candles, and 24h OHLC fill.
Author
Owner

drain skip: no occupying job for rebase/fix-pr/CI-wait; remaining continue

drain skip: no occupying job for rebase/fix-pr/CI-wait; remaining continue
Author
Owner

/agent review

/agent review
Author
Owner

cl8y-agent-control: queued bug_review job a0a75250-d86c-40f0-b2ee-8ade2c107464 (not executed; no Hetzner VM).

cl8y-agent-control: queued `bug_review` job `a0a75250-d86c-40f0-b2ee-8ade2c107464` (not executed; no Hetzner VM).
Author
Owner

/agent review

/agent review
Author
Owner

cl8y-agent-control: queued bug_review job ae20d4df-a1de-43a5-9e6d-0984ac292afc (not executed; no Hetzner VM).

cl8y-agent-control: queued `bug_review` job `ae20d4df-a1de-43a5-9e6d-0984ac292afc` (not executed; no Hetzner VM).
Author
Owner

RECOMMEND: ACCEPT

Independent review of PR #1273 (issue/1258 → main) for #1258. Head: d682d58e26f8276e8520c9b8a064650ed2e860ef. Frozen SHA was unset; this is not a merge grant.

Verdict

The production Charts bug is addressed: P522-Q now identity-gates the columbus-5 registry USDT CW20 to advisory $1, live ingest stamps price_usd / volume_usd, and NULL-only SQL + startup candle rebuild can populate 24h OHLC / GET /candles. Spoof symbol=USDT, Peg1/hub UST1, VFDUSD/FDUSD, and HubQuoteUsd.economic stay out of this path. No wasm, no new routes, no settlement change.

Ship after CI green + Coolify indexer migrate/restart smoke on terra17l7eqc5j8vkm09up55etfggpr6y92ka6p03yc765mt3nerqcnhdsl6l7jq (PR test-plan boxes still unchecked).

#1258 acceptance / verify

ID Result Evidence
AC1 stats *_usd after ingest/backfill PASS (code) Ingest price_usd_for_human_quote_per_base_pinned (indexer/src/indexer/parser.rs:503-512). Backfill price_usd = se.price when asset_1 is the pin (indexer/src/db/queries/usdt_quote_usd.rs:26-36; migration indexer/migrations/20260916120000_usdt_quote_usd_null_backfill.sql:8-17).
AC2 GET /candles factory USD PASS (code), untested live Rebuild uses price_usd only (indexer/src/db/queries/candles.rs:130-157). SQL migrate does not rebuild candles (commented at migration 5-6); rust hook does (usdt_quote_usd.rs:73-91). T6 fixture missing.
AC3 invertUsd / ?price= PARTIAL Invert path unchanged; unit invert at $1 (frontend-dapp/src/utils/__tests__/pairPriceUsd.test.ts:283-300). No USDT page/e2e. Charts Last omits quoteContract (ChartsPage.tsx:286-299) — fail-closed; OHLC uses indexer *_usd.
AC4 spoof / VFDUSD / FDUSD / CL8Y PASS quote_usd_kind("USDT") has no arm (pair_price_usd.rs:90-104). Tests 1003-1050.
AC5 6/18 + no compact T PASS (unit) usdt_six_eighteen_human_times_one_is_price_usd (1063-1078); volume 18-dec (1081-1097); formatPairStatsUsdOhlc('0.00004123') (chartsPairStats.test.ts:120-124).
AC6 UST1/cUSTC still hub PASS Peg1 → hub.ust1 (pair_price_usd.rs:141-143); Usdt ≠ Peg1 (986-999). HUB_TICKERS still 4 (hub_usd.rs:25).
AC7 verify-issue-522 PARTIAL 522 target unchanged; verify-issue-1258 does not chain 522/543/556.
T1–T5, T7, T9 PASS Lib/Vitest as above.
T6 candle rebuild fixture FAIL (coverage) No sqlx/integration test.
T8 / T10 PARTIAL / inherited Unit invert; empty/— formatter unchanged. No Playwright.
H1–H10 PASS (code) See security. H6 NULL-only is in SQL (IS NULL) but not fixture-tested.
make verify-issue-1258 grep PASS; cargo/vitest env-blocked on this gate VM (see Tests).

Out of scope correctly left out: #1221 bridge/CMM, #1257 Swap scale, #1220 IBC USDC, CoinGecko USDT oracle, fifth hub cell, CG/CMC last_price USD, wasm.

Security audit

No medium+ external exploit path. Advisory display catalog, not custody/settlement.

Vector Severity Result
Hostile symbol=USDT / native uusdt / empty contract (H1) — Unpriced. is_pinned_usdt_cw20 requires is_cw20 + casefold contract (pair_price_usd.rs:115-131, 285-287). Repro: quote_usd_kind_for_identity("USDT", None, true, Some("terra1spoof…"), …) → None (1003-1014).
Peg via hub_prices.ust1 or CEX FDUSD (H5) — Usdt => Some(1) (149); Peg1 still hub (141-143). No OracleTicker::Vfdusd in this module.
Non-NULL history rewrite (H6 / #568) — price_usd IS NULL / volume_usd IS NULL (usdt_quote_usd.rs:32,60,66; migration 13,40). UST1/cUSTC not in the pin JOIN.
SQL injection — $1 bind (usdt_quote_usd.rs:38,68). Migration pin is a literal bech32.
New API / DB leak / authz — No new routes; same public stats/candles.
XSS (H7) — Numeric formatters only; no new HTML sinks.
Plot human 18-dec as USD when stamp NULL (H4 / P522-4) — Charts OHLC binds indexer *_usd only (ChartsPage.tsx:333-344). Client Last without quoteContract does not apply $1 (pairPriceUsd.ts:18-35). Do not later wire quoteContract on post-#522 human price — fallback still treats price as raw (pairPriceUsd.ts:73-107); that would 6/18-double-scale.
Charts USD as on-chain limit price (H8) — Submit path untouched.
Oracle / wash-trade “manipulation” Informational USD = human quote-per-base × advisory $1. Pool manipulation moves the human print; dollars follow the peg. Issue accepted this vs a CEX poll. Not fund loss.
Operator USDT_CW20_ADDRESS collision with UST1/cUSTC Low (ops) is_pinned_usdt_cw20 wins first (285-287). Wrong env would $1 that contract. Same trust as other HUB_* pins; no unprivileged path.
Prompt injection — N/A to this diff.

Bugbot “high”: every-boot candle rebuild (usdt_quote_usd.rs:73-91). Downgrade to Low ops, do not gate on price_filled > 0. SQL migrate fills swap_events.price_usd and explicitly leaves candles to the rust hook. After migrate, the rust UPDATE is a no-op (rows_affected == 0) but candles are still empty — skipping rebuild would leave GET /candles / #113 empty on first Coolify start. Unconditional rebuild from epoch 0 is the simple correct AC2 glue. Later: skip when USD candles already exist.

Gap analysis

Features: Catalog + ingest + NULL-only backfill + idle marks (candle_mark.rs:56-61) match the ticket. /pool Vol / protocol TVL on columbus-5 will start showing USDT USD because catalog_usd_per_human_identity(..., None) still hits DEFAULT_USDT_CW20_ADDRESS (protocol_tvl.rs:56-67, protocol_fees.rs:431-440). That is catalog expansion, not a hub fifth cell.

PR body overclaim (Low docs): “dApp fallback so Charts Price (USD) and 24h OHLC can render when indexer *_usd is still empty” is false for production pages. 24h OHLC never calls classifyQuoteSymbol. Last/Trade/Limits omit quoteContract (ChartsPage.tsx:288-299, TradePage.tsx ~399, LimitOrdersPage.tsx ~190). Leaving callers unwired is the correct fail-closed choice (see H4). Prefer indexer field after restart.

LocalTerra overlay (Low vs constraint): Indexer reads USDT_CW20_ADDRESS (config.rs:424). Deploy/setup does not write it; default is columbus-5 bech32 (config.rs:25-27). SQL migrate is hardcoded to mainnet (migration:17). TVL/fees pass configured_usdt_address: None, so LocalTerra overlay ingest ≠ TVL. Constraint called for deploy/env overlay; columbus-5 AC does not depend on it.

Dead arm (Low): traderPositionDisplay.ts:354-355 case 'usdt': return 1 is unreachable — classifyQuoteSymbol(pos.asset_1_symbol, pos.asset_1_denom) has no contract (289-290). IndexerPosition has no CW20 field (types/index.ts:790-805). P&L for USDT-quoted positions stays —. Out of Charts AC; delete the arm or pass identity in a follow-up.

DRY / readability: _pinned wrappers duplicate unpinned helpers. Frontend pin is hardcoded columbus-5 (tokenRegistry.ts:6); indexer pin is env. Acceptable for A1.

Gas / wasm: N/A.

Testing gaps: T6 backfill+rebuild sqlx fixture; candle_mark has no USDT pin vs spoof assert (candle_mark.rs:206-233); verify-issue-1258 does not run swap_price_human_usd or 522/543/556; no e2e. docs/testing.md not updated.

UI/UX: After indexer restart, Charts should show dollars; tape Price stays human. Users may also expect portfolio USD for USDT quotes (dead arm above) and LocalTerra Charts without env overlay.

Tests this review ran

  • Vitest pairPriceUsd.test.ts + chartsPairStats.test.ts USDT/OHLC filter: 14 passed, 19 skipped.
  • make verify-issue-1258 greps: PASS. Cargo step failed here (cargo missing then OpenSSL headers / read-only apt). CI must run cd indexer && cargo test --lib pair_price_usd.
  • No LocalTerra / Coolify / Playwright (not provisioned; issue Coolify box still open).

Follow-ups (non-blocking)

  1. Coolify: migrate + new indexer binary + confirm /charts cLUNC/USDT candles and 24h USD OHLC; spoof ticker still —; UST1 charts still hub.
  2. Optional: sqlx T6 (NULL → USD candles, non-NULL UST1 untouched); gate later rebuilds on missing USD candles, not price_filled.
  3. Optional: write USDT_CW20_ADDRESS from LocalTerra deploy; thread overlay into TVL/fees.
  4. Do not wire Charts quoteContract fallback without treating post-#522 price as already human.
RECOMMEND: ACCEPT Independent review of PR **#1273** (`issue/1258` → `main`) for **#1258**. Head: `d682d58e26f8276e8520c9b8a064650ed2e860ef`. Frozen SHA was unset; this is not a merge grant. ## Verdict The production Charts bug is addressed: P522-Q now identity-gates the columbus-5 registry USDT CW20 to advisory **$1**, live ingest stamps `price_usd` / `volume_usd`, and NULL-only SQL + startup candle rebuild can populate 24h OHLC / GET `/candles`. Spoof `symbol=USDT`, Peg1/hub UST1, VFDUSD/FDUSD, and `HubQuoteUsd.economic` stay out of this path. No wasm, no new routes, no settlement change. Ship after CI green + Coolify indexer migrate/restart smoke on `terra17l7eqc5j8vkm09up55etfggpr6y92ka6p03yc765mt3nerqcnhdsl6l7jq` (PR test-plan boxes still unchecked). ## #1258 acceptance / verify | ID | Result | Evidence | |----|--------|----------| | AC1 stats `*_usd` after ingest/backfill | **PASS (code)** | Ingest `price_usd_for_human_quote_per_base_pinned` (`indexer/src/indexer/parser.rs:503-512`). Backfill `price_usd = se.price` when `asset_1` is the pin (`indexer/src/db/queries/usdt_quote_usd.rs:26-36`; migration `indexer/migrations/20260916120000_usdt_quote_usd_null_backfill.sql:8-17`). | | AC2 GET `/candles` factory USD | **PASS (code), untested live** | Rebuild uses `price_usd` only (`indexer/src/db/queries/candles.rs:130-157`). SQL migrate does **not** rebuild candles (commented at migration `5-6`); rust hook does (`usdt_quote_usd.rs:73-91`). T6 fixture **missing**. | | AC3 invertUsd / `?price=` | **PARTIAL** | Invert path unchanged; unit invert at $1 (`frontend-dapp/src/utils/__tests__/pairPriceUsd.test.ts:283-300`). No USDT page/e2e. Charts Last omits `quoteContract` (`ChartsPage.tsx:286-299`) — fail-closed; OHLC uses indexer `*_usd`. | | AC4 spoof / VFDUSD / FDUSD / CL8Y | **PASS** | `quote_usd_kind("USDT")` has no arm (`pair_price_usd.rs:90-104`). Tests `1003-1050`. | | AC5 6/18 + no compact `T` | **PASS (unit)** | `usdt_six_eighteen_human_times_one_is_price_usd` (`1063-1078`); volume 18-dec (`1081-1097`); `formatPairStatsUsdOhlc('0.00004123')` (`chartsPairStats.test.ts:120-124`). | | AC6 UST1/cUSTC still hub | **PASS** | `Peg1` → `hub.ust1` (`pair_price_usd.rs:141-143`); Usdt ≠ Peg1 (`986-999`). `HUB_TICKERS` still 4 (`hub_usd.rs:25`). | | AC7 `verify-issue-522` | **PARTIAL** | 522 target unchanged; `verify-issue-1258` does not chain 522/543/556. | | T1–T5, T7, T9 | **PASS** | Lib/Vitest as above. | | T6 candle rebuild fixture | **FAIL (coverage)** | No sqlx/integration test. | | T8 / T10 | **PARTIAL / inherited** | Unit invert; empty/`—` formatter unchanged. No Playwright. | | H1–H10 | **PASS (code)** | See security. H6 NULL-only is in SQL (`IS NULL`) but not fixture-tested. | | `make verify-issue-1258` | **grep PASS**; cargo/vitest **env-blocked** on this gate VM (see Tests). | Out of scope correctly left out: #1221 bridge/CMM, #1257 Swap scale, #1220 IBC USDC, CoinGecko USDT oracle, fifth hub cell, CG/CMC `last_price` USD, wasm. ## Security audit No medium+ external exploit path. Advisory display catalog, not custody/settlement. | Vector | Severity | Result | |--------|----------|--------| | Hostile `symbol=USDT` / native `uusdt` / empty contract (H1) | — | Unpriced. `is_pinned_usdt_cw20` requires `is_cw20` + casefold contract (`pair_price_usd.rs:115-131, 285-287`). Repro: `quote_usd_kind_for_identity("USDT", None, true, Some("terra1spoof…"), …)` → `None` (`1003-1014`). | | Peg via `hub_prices.ust1` or CEX FDUSD (H5) | — | `Usdt => Some(1)` (`149`); Peg1 still hub (`141-143`). No `OracleTicker::Vfdusd` in this module. | | Non-NULL history rewrite (H6 / #568) | — | `price_usd IS NULL` / `volume_usd IS NULL` (`usdt_quote_usd.rs:32,60,66`; migration `13,40`). UST1/cUSTC not in the pin JOIN. | | SQL injection | — | `$1` bind (`usdt_quote_usd.rs:38,68`). Migration pin is a literal bech32. | | New API / DB leak / authz | — | No new routes; same public stats/candles. | | XSS (H7) | — | Numeric formatters only; no new HTML sinks. | | Plot human 18-dec as USD when stamp NULL (H4 / P522-4) | — | Charts OHLC binds indexer `*_usd` only (`ChartsPage.tsx:333-344`). Client Last without `quoteContract` does not apply `$1` (`pairPriceUsd.ts:18-35`). **Do not** later wire `quoteContract` on post-#522 human `price` — fallback still treats `price` as **raw** (`pairPriceUsd.ts:73-107`); that would 6/18-double-scale. | | Charts USD as on-chain limit price (H8) | — | Submit path untouched. | | Oracle / wash-trade “manipulation” | Informational | USD = human quote-per-base × **advisory $1**. Pool manipulation moves the **human** print; dollars follow the peg. Issue accepted this vs a CEX poll. Not fund loss. | | Operator `USDT_CW20_ADDRESS` collision with UST1/cUSTC | Low (ops) | `is_pinned_usdt_cw20` wins first (`285-287`). Wrong env would $1 that contract. Same trust as other `HUB_*` pins; no unprivileged path. | | Prompt injection | — | N/A to this diff. | **Bugbot “high”: every-boot candle rebuild (`usdt_quote_usd.rs:73-91`).** Downgrade to **Low ops**, do **not** gate on `price_filled > 0`. SQL migrate fills `swap_events.price_usd` and explicitly leaves candles to the rust hook. After migrate, the rust `UPDATE` is a no-op (`rows_affected == 0`) but candles are still empty — skipping rebuild would leave GET `/candles` / #113 empty on first Coolify start. Unconditional rebuild from epoch 0 is the simple correct AC2 glue. Later: skip when USD candles already exist. ## Gap analysis **Features:** Catalog + ingest + NULL-only backfill + idle marks (`candle_mark.rs:56-61`) match the ticket. `/pool` Vol / protocol TVL on columbus-5 will start showing USDT USD because `catalog_usd_per_human_identity(..., None)` still hits `DEFAULT_USDT_CW20_ADDRESS` (`protocol_tvl.rs:56-67`, `protocol_fees.rs:431-440`). That is catalog expansion, not a hub fifth cell. **PR body overclaim (Low docs):** “dApp fallback so Charts Price (USD) and 24h OHLC can render when indexer `*_usd` is still empty” is false for production pages. 24h OHLC never calls `classifyQuoteSymbol`. Last/Trade/Limits omit `quoteContract` (`ChartsPage.tsx:288-299`, `TradePage.tsx` ~399, `LimitOrdersPage.tsx` ~190). Leaving callers unwired is the **correct** fail-closed choice (see H4). Prefer indexer field after restart. **LocalTerra overlay (Low vs constraint):** Indexer reads `USDT_CW20_ADDRESS` (`config.rs:424`). Deploy/setup does not write it; default is columbus-5 bech32 (`config.rs:25-27`). SQL migrate is hardcoded to mainnet (`migration:17`). TVL/fees pass `configured_usdt_address: None`, so LocalTerra overlay ingest ≠ TVL. Constraint called for deploy/env overlay; columbus-5 AC does not depend on it. **Dead arm (Low):** `traderPositionDisplay.ts:354-355` `case 'usdt': return 1` is unreachable — `classifyQuoteSymbol(pos.asset_1_symbol, pos.asset_1_denom)` has no contract (`289-290`). `IndexerPosition` has no CW20 field (`types/index.ts:790-805`). P&L for USDT-quoted positions stays `—`. Out of Charts AC; delete the arm or pass identity in a follow-up. **DRY / readability:** `_pinned` wrappers duplicate unpinned helpers. Frontend pin is hardcoded columbus-5 (`tokenRegistry.ts:6`); indexer pin is env. Acceptable for A1. **Gas / wasm:** N/A. **Testing gaps:** T6 backfill+rebuild sqlx fixture; `candle_mark` has no USDT pin vs spoof assert (`candle_mark.rs:206-233`); `verify-issue-1258` does not run `swap_price_human_usd` or 522/543/556; no e2e. `docs/testing.md` not updated. **UI/UX:** After indexer restart, Charts should show dollars; tape Price stays human. Users may also expect portfolio USD for USDT quotes (dead arm above) and LocalTerra Charts without env overlay. ## Tests this review ran - Vitest `pairPriceUsd.test.ts` + `chartsPairStats.test.ts` USDT/OHLC filter: **14 passed**, 19 skipped. - `make verify-issue-1258` greps: **PASS**. Cargo step failed here (`cargo` missing then OpenSSL headers / read-only apt). CI must run `cd indexer && cargo test --lib pair_price_usd`. - No LocalTerra / Coolify / Playwright (not provisioned; issue Coolify box still open). ## Follow-ups (non-blocking) 1. Coolify: migrate + **new indexer binary** + confirm `/charts` cLUNC/USDT candles and 24h USD OHLC; spoof ticker still `—`; UST1 charts still hub. 2. Optional: sqlx T6 (NULL → USD candles, non-NULL UST1 untouched); gate later rebuilds on missing USD candles, not `price_filled`. 3. Optional: write `USDT_CW20_ADDRESS` from LocalTerra deploy; thread overlay into TVL/fees. 4. Do not wire Charts `quoteContract` fallback without treating post-#522 `price` as already human.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic!1273
No description provided.