fix: /protocol fee USD omit CL8Y and other economic tokens #683

Closed
opened 2026-08-27 01:00:27 +00:00 by PlasticDigits · 12 comments
PlasticDigits commented 2026-08-27 01:00:27 +00:00 (Migrated from gitlab.com)

Summary

On /protocol, treasury fees taken in CL8Y (live token-mix ticker may show as CL8Y-cb / CL8Y-CB) render USD blank even when the human amount is non-zero (report: > 1.298 received). Trailing Last 24h / 7d / 30d fees omit those rows because rollup SUMs only non-null fee_usd.

Bundle into one issue: extend fee USD pricing to all factory-listed economic tokens (CL8Y first; same path for later listed non-gems). Do not split “token-row em-dash” vs “headline undercount” vs “backfill NULL fee_usd” vs “Llama inherited fees.”

Related: #586 (fee panel), #556 / #570 (hub USD — no CL8Y mark), #522 / #548 (P522-Q catalog), #568 (do not rewrite stamped USD), #577 (window decay), #631 (DeFiLlama daily fees reuse fee_usd), #580 (do not price Terra vFDUSD from CEX FDUSD), #562 / #534 (economic vs gem).

Current codebase

/protocol Protocol fees (protocol-fee-stats) already exists after #586 / #613 / #614. Headlines and the token table are USD-or-em-dash. The hole is the P522-Q catalog, not missing UI.

Layer Behavior today
Fee UI ProtocolFeeStats.tsx: 24h/7d/30d via formatProtocolUsd(overview.total_fees_*_usd). Token table shows amount_human (so 1.298 CL8Y is visible) and formatProtocolUsd(row.amount_usd) — null / "" → em-dash (—), which operators read as blank. XSS symbols render as text (PFee-4).
Overview scalars GET /overview overview_fee_usd_field: idle (event_count=0) → "0"; activity + no priced USD → null (UI —). Partial priced + unpriced → priced SUM only — unpriced CL8Y is dropped, not listed as a remainder.
Rollup refresh_protocol_fees: SUM(fee_usd) FILTER (WHERE fee_usd IS NOT NULL). Token mix ORDER BY amount_usd DESC NULLS LAST, top 8 + other. clamp_usd drops ≤0 / overflow. GET /overview and /protocol/fees stay O(1) / 60s cache (PFee-8).
Ingest stamp ingest_protocol_fee → fee_usd_for_raw → catalog_usd_per_human_identity. AMM / book take commission is the ask asset (ask_asset_id). Buying CL8Y with UST1 therefore stores the fee in CL8Y. Limit place uses offer-side (offer_asset_id_for_side). Insert is ON CONFLICT DO NOTHING — a later catalog fix does not fill old NULL fee_usd.
P522-Q catalog Only USTC/cUSTC/uusd, LUNC/cLUNC/uluna, UST1, USTR. Unit test is explicit: quote_usd_kind("CL8Y", None) == None. HubQuoteUsd has only ust1 / ustr. load_quote_usd ignores every other hub_prices ticker.
Hub bootstrap hub_usd.rs: cUSTC ← USTC oracle; LUNC ← LUNC oracle; UST1 ← max-TVL cUSTC/UST1 pair; USTR ← max-TVL vs already-priced cUSTC or UST1. No CL8Y pin, no walk of other listed CW20s. Protocol hub card stays four columns (H11).
Volume / TVL (contrast) Same CL8Y/UST1 swap can have volume_usd via the catalogued UST1 offer/quote (volume_usd_for_swap). TVL of a one-sided catalog pair 2× the priced leg (P569-5). Operators therefore see volume/TVL move while fees in CL8Y stay unpriced.
Llama defillama.rs daily fees SUM(fee_usd) with gem-pair exclude. Unpriced economic fees undercount Llama the same way they undercount Protocol.
Identity CL8Y columbus-5 CW20: terra16wtml2q66g82fdkx66tap0qjkahqwp4lwq3ngtygacg5q0kzycgqvhpax3 (VITE_CL8Y_TOKEN_ADDRESS / CL8Y_TOKEN_ADDRESS). Fee-discount I12 already notes wallets may label a bridged asset CL8Y-cb — Protocol must key contract/denom (A1), not the display ticker. LocalTerra TCL8Y is the 18-dec proxy (#383).
Economic vs gem Retail economic set includes CL8Y / wraps / UST1 / USTR / vFDUSD (pairCatalogRank.ts). Gems (EMBER… / columbus-5 gem addrs) are test. vFDUSD is economic for discovery but must not be USD-priced from CEX FDUSD (X4 / #580).
Verify today make verify-issue-586 · 613 · 614 · 556 · 569 · 631. No test that a CL8Y (or uncatalogued economic) ask-side commission stamps fee_usd.

Why the new implementation is needed

  1. Treasury already received the tokens. The token table proves ingest works (amount_human ≈ 1.298). Blank USD is not “no fee” — it is unpriced catalog. Operators and CMM cannot read protocol take from /protocol.
  2. Headlines lie by omission. 24h/7d/30d SUM skips NULL fee_usd. CL8Y (and any later listed economic ask asset) never enters the tiles, source mix USD, or Llama daily fees, even when volume for the same swap is priced off UST1.
  3. P522-Q was scoped to hub stables/wraps. That was correct for volume quotes and UST1/USTR marks. AMM commission is ask-denominated. As soon as the ask is CL8Y, the #586 catalog is insufficient. Hardcoding only CL8Y repeats the bug on the next listed economic token.
  4. CoinGecko is not a substitute. ceramicliberty-com is BSC-only today (#629 / listings). Do not poll CG / invent a Classic platform price for ingest.
  5. Conflict-noop ingest. Existing NULL rows stay NULL until an explicit NULL-only backfill + rollup refresh. Waiting 30d for decay is not acceptable once marks exist.

Constraints / guardrails

  1. Keep PFee-1–PFee-13, P550-5, PFee-8, H11, X4, C568-1. GET /overview and /protocol/fees stay O(1) rollup / 60s cache. Do not SUM protocol_fee_events on GET. Do not add a fifth Protocol hub column or a CEX cl8y tab. Do not rewrite non-null historical fee_usd from the live mark.
  2. Identity is contract/denom (A1). Pin columbus-5 CL8Y (and LocalTerra HUB_CL8Y_ADDRESS / VITE_CL8Y_TOKEN_ADDRESS). Do not price symbol=CL8Y / CL8Y-cb / TCL8Y on a spoof native or random CW20. Homoglyph / javascript: / ../ tickers stay rejected.
  3. DEX reserve spot, not CEX / CG / Venus. After existing hub bootstrap, price an economic token from the largest USD-TVL factory pair vs an already-priced catalog/hub leg (same ranking as USTR: humanized TVL, HUB_USD_TVL_FLOOR, stale/dust/same-asset skip). Never $1 CL8Y. Never ceramicliberty-com. Never vFDUSD/FDUSD / Venus into fee USD.
  4. All factory-listed economic tokens, not gems. Include CL8Y and later listed non-gem CW20s that have a qualifying pair. Exclude #562 gems (addr + ticker). Exclude vFDUSD as a USD handle. Unlisted / foreign pairs stay out (P1). Community-tax templates are economic only if factory-listed and not in the gem set.
  5. Partial windows stay priced SUM. Priced economic + still-unpriced gem → headline is the priced total (not —, not $0). All activity unpriced → still null / —. Idle → $0. Never Infinity.
  6. NULL-only backfill. One-shot (or aggregator) fill of protocol_fee_events.fee_usd IS NULL using current economic marks. Never UPDATE a non-null stamp. Then refresh_protocol_fees. Document as as-of backfill, not historical mark-to-market.
  7. Hub card unchanged. Internal marks may live in hub_prices extra rows or a sibling table. GET /api/v1/hub-prices allowlist stays custc | lunc | ust1 | ustr unless product later asks for a CL8Y column (out of scope). load_quote_usd / fee_usd_for_raw must actually read the new marks.
  8. Llama inherits indexer USD. Do not invent a second Llama pricer. Gem exclude on Llama stays.
  9. Retail copy (#489). No lecture that “CL8Y uses a pool mark.” Token row: human + USD. Still “reference / indexed,” not settlement.
  10. Out of scope. Changing on-chain fee bps; pricing gems; CEX CL8Y tab; rewriting volume/TVL 2× rules; Keplr/CG listings; fee-discount I12 (bridged CL8Y-cb still does not register a tier).
  11. Do not bind-mount indexer/ into root Docker for cargo (workspace rule). Host cargo test / make verify-issue-*.

Relevant files

Path Role
indexer/src/indexer/pair_price_usd.rs P522-Q; quote_usd_kind("CL8Y") is None; extend lookup after catalog
indexer/src/indexer/protocol_fees.rs fee_usd_for_raw / overview_fee_usd_field
indexer/src/indexer/parser.rs ingest_protocol_fee (ask-asset AMM; offer-side place)
indexer/src/indexer/hub_usd.rs Bootstrap + ranking; add economic-token walk
indexer/src/db/queries/hub_prices.rs load_quote_usd today only copies ust1/ustr
indexer/src/db/queries/protocol_fees.rs Window / token SUM(fee_usd); NULL-only backfill hook
indexer/src/config.rs Pin HUB_CL8Y_ADDRESS (columbus-5 default + LocalTerra override)
indexer/src/db/queries/defillama.rs Inherited daily fee USD
indexer/tests/indexer_protocol_fees.rs Unpriced-null today; add CL8Y / economic priced cases
frontend-dapp/src/components/protocol/ProtocolFeeStats.tsx Already displays USD when JSON is set
frontend-dapp/src/pages/ProtocolPage.test.tsx Fee panel + XSS token row
frontend-dapp/src/utils/pairCatalogRank.ts Economic vs gem (do not copy gem addrs into the USD catalog as priced)
skills/AGENTS_FRONTEND_PROTOCOL_STATS.md Extend PFee-7
skills/AGENTS_INDEXER_HUB_USD.md Internal CL8Y / economic marks; hub card stays four cells
docs/indexer-invariants.md Protocol fees row
docs/runbooks/overview-global-stats-brin.md Rollup + unpriced semantics
scripts/qa/verify-issue-586.sh Keep green; add new verify script
  1. Reuse USTR ranking, do not invent a CEX. After cUSTC / LUNC / UST1 / USTR resolve, walk factory pair_reserves for pairs whose one leg is already priced and the other is a factory-listed economic CW20 (CL8Y pin required; then other non-gem listed contracts). Winner = max humanized USD-TVL, then lex pair address. Persist usd_per_human + source_pair + asset_id.
  2. Wire ingest. Extend the mark bundle fee_usd_for_raw sees (today HubQuoteUsd + oracles). Lookup order: P522-Q identity → economic mark by contract/denom → None. Humanize × mark; overflow / ≤0 / missing pair → None.
  3. NULL-only backfill + rollup. Fill fee_usd IS NULL only; refresh global_stats_24h + source/token tables. Coolify: set LocalTerra / prod CL8Y pin; no Vite-only pin.
  4. Leave the hub card and CEX tabs alone. No GET /hub-prices/cl8y unless a later issue asks. Frontend needs no new tiles if JSON amount_usd / total_fees_*_usd become decimal strings.
  5. Docs / skill. PFee-7 becomes “P522-Q + hub + factory economic marks.” Keep “never vFDUSD / never $1 / stamp at ingest / NULL-only backfill.”

Acceptance criteria

  • AC1. AMM / book / place / wrap-family fees in the pinned CL8Y CW20 stamp non-null fee_usd when a qualifying CL8Y↔priced-hub pair exists above the TVL floor and hub/oracle for the other leg are up.
  • AC2. Token table for that window shows human units and a finite $ (not —) for CL8Y / CL8Y-cb display symbols. XSS / javascript: symbols stay text.
  • AC3. total_fees_{24h,7d,30d}_usd includes those CL8Y (and other priced economic) amounts. A window that is only unpriced gems stays null. Idle stays "0".
  • AC4. Other factory-listed economic tokens with a qualifying pair are priced the same way — not a CL8Y-only if symbol == "CL8Y".
  • AC5. Gems stay unpriced (or omitted from USD). vFDUSD is not priced from CEX FDUSD / Venus. Spoof symbol=CL8Y native / wrong contract stays unpriced.
  • AC6. Historical non-null fee_usd is unchanged. Prior NULL rows are filled once marks exist (NULL-only).
  • AC7. GET /overview and /protocol/fees still do not scan protocol_fee_events. window= allowlist unchanged. Hub JSON allowlist unchanged. Protocol hub card still four cells.
  • AC8. DeFiLlama daily fees pick up the same stamped USD (gem exclude unchanged).
  • AC9. make verify-issue-586 / 613 / 614 / 556 / 569 / 631 stay green. New make verify-issue-<this> covers catalog + ingest + rollup + UI.

Test plan (all paths)

Indexer unit / lib

  1. quote_usd_kind / catalog identity: CL8Y symbol alone still None; pinned contract resolves via the new mark helper.
  2. fee_usd_for_raw: CL8Y raw × mark → USD; missing mark / stale pair / below TVL floor / overflow → None; UST1/USTR/cUSTC/LUNC unchanged.
  3. Spoof native symbol=CL8Y / CL8Y-cb with no pin contract → None.
  4. Gem CW20 (EMBER addr) with a UST1 pair → still None for fee USD.
  5. vFDUSD × CEX FDUSD / Venus → None.
  6. Ranking: two CL8Y pairs vs UST1 vs cUSTC — winner is max USD-TVL then lex address; dust under floor skipped.

Indexer integration (Postgres; make setup-indexer-postgres)

  1. Insert swap_amm fee in pinned CL8Y with hub CL8Y mark present → protocol_fee_events.fee_usd set; rollup 24h/7d/30d includes it; GET /protocol/fees?window=24h token row has amount_human + amount_usd.
  2. Same events with no CL8Y pair / oracle down on the other leg → fee_usd NULL; headline null if nothing else priced; token row human + USD null.
  3. Mixed window: priced UST1 + unpriced gem + priced CL8Y → headline = UST1+CL8Y only.
  4. NULL-only backfill: pre-existing NULL CL8Y row fills; pre-existing non-null USD unchanged when the live mark moves.
  5. ON CONFLICT replay does not double-count.
  6. GET /protocol/fees?window=javascript: / ust1-window / empty → 400. Cache key still allowlisted window only.
  7. Llama daily agg: CL8Y fee on a non-gem pair counted; gem-pair swap fee still excluded.

Frontend

  1. ProtocolPage.test.tsx: token row symbol: 'CL8Y-cb', amount_human: '1.298', amount_usd: '12.34' → visible $ (not only human). amount_usd: null still em-dash.
  2. XSS symbol + human amount + null USD: text only, no img / innerHTML.
  3. Missing fee fields on old indexer still hide the panel (PFee-10). Nested card-glass still forbidden.

LocalTerra (optional, not a skip if Postgres tests pass)

  1. After make setup-cloud-localterra, seed a TCL8Y/UST1 (or CL8Y pin) pair above dust, swap so commission is TCL8Y, wait aggregator: /protocol token USD and 24h tile include it.

Test plan (attack, hack, and abuse)

  1. Symbol spoof. Factory-unlisted CW20 or native with symbol=CL8Y / CL8Y-cb / TCL8Y must not mint fee USD. Contract pin + factory provenance (P1) win.
  2. Wash / thin pool. Attacker seeds a $1 CL8Y/UST1 dust pool to print a fantasy mark. TVL floor + staleness must skip it; no mark → None, not $1.
  3. Circular quote. Do not price CL8Y from a CL8Y/ONYX pair using an unpriced ONYX. Other leg must already be catalog/hub-priced. No recursive unbounded walk on GET.
  4. Hub table injection. Extra hub_prices.ticker (cl8y, ../ustr, javascript:) must not appear on GET /hub-prices/{ticker} (still 400) and must not expand CEX tabs.
  5. Rewrite attack. Poller / Coolify restart must not overwrite non-null fee_usd when the live CL8Y mark jumps (wash the source pool). NULL-only backfill only.
  6. Double count. Backfill + conflict replay + hybrid L7 (pool + book once) unchanged. Do not also add book_commission_amount.
  7. DoS. No GET-path SUM of 60d protocol_fee_events. Economic walk is aggregator / hub-refresh only, bounded by pair count + floor. Token mix still top 8 + other.
  8. Cache bust. Junk query on /protocol/fees must not bypass the 60s allowlisted-window cache.
  9. XSS / HTML in symbol or amount_human. Render as text. No eval / innerHTML / javascript: hrefs.
  10. CoinGecko / UA. Do not add a live CG poll for ceramicliberty-com on the fee path (#579 / BSC-only).
  11. vFDUSD sneak. Economic-token walker must not treat CEX FDUSD or Venus exchangeRateCurrent as usd(vFDUSD).
  12. Gem inflate. Listing a gem as “economic” via spoofed token_info.symbol=UST1 is already a hide-by-address rule on the dApp; indexer USD must use gem addr set, not symbol.

Verification criteria

make verify-issue-<this>   # new harness: catalog + ingest + NULL-only backfill + rollup + Protocol token USD
make verify-issue-586
make verify-issue-613
make verify-issue-614
make verify-issue-556
make verify-issue-569
make verify-issue-631

Pass when: (1) a CL8Y ask-side commission with a qualifying hub pair stamps fee_usd and appears in 24h/7d/30d + token $; (2) gems / vFDUSD / spoof symbols stay unpriced; (3) GET paths stay rollup-only; (4) hub card still four cells; (5) existing #586 semantics (idle $0, all-unpriced —, no Inf) hold.

Notes

  • Live display ticker CL8Y-cb is a wallet / listing label. Treat it as the columbus-5 CL8Y CW20 unless a different contract is proven. Do not create a second CG id.
  • Volume/TVL already count CL8Y/UST1 via the UST1 leg. This issue is fee-token USD, not a second volume catalog.
## Summary On `/protocol`, treasury fees taken in **CL8Y** (live token-mix ticker may show as **CL8Y-cb** / **CL8Y-CB**) render **USD blank** even when the human amount is non-zero (report: **> 1.298** received). Trailing **Last 24h / 7d / 30d fees** omit those rows because rollup `SUM`s only non-null `fee_usd`. Bundle into **one** issue: extend fee USD pricing to **all factory-listed economic tokens** (CL8Y first; same path for later listed non-gems). Do **not** split “token-row em-dash” vs “headline undercount” vs “backfill NULL `fee_usd`” vs “Llama inherited fees.” Related: [#586](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/586) (fee panel), [#556](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/556) / [#570](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/570) (hub USD — **no** CL8Y mark), [#522](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/522) / [#548](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/548) (P522-Q catalog), [#568](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/568) (do not rewrite stamped USD), [#577](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/577) (window decay), [#631](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/631) (DeFiLlama daily fees reuse `fee_usd`), [#580](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/580) (do not price Terra vFDUSD from CEX FDUSD), [#562](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/562) / [#534](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/534) (economic vs gem). ## Current codebase `/protocol` **Protocol fees** (`protocol-fee-stats`) already exists after #586 / #613 / #614. Headlines and the token table are **USD-or-em-dash**. The hole is the **P522-Q catalog**, not missing UI. | Layer | Behavior today | |-------|----------------| | **Fee UI** | [`ProtocolFeeStats.tsx`](frontend-dapp/src/components/protocol/ProtocolFeeStats.tsx): 24h/7d/30d via `formatProtocolUsd(overview.total_fees_*_usd)`. Token table shows `amount_human` (so **1.298 CL8Y** is visible) and `formatProtocolUsd(row.amount_usd)` — `null` / `""` → **em-dash** (`—`), which operators read as blank. XSS symbols render as text (**PFee-4**). | | **Overview scalars** | `GET /overview` [`overview_fee_usd_field`](indexer/src/indexer/protocol_fees.rs): idle (`event_count=0`) → `"0"`; activity + no priced USD → **`null`** (UI `—`). Partial priced + unpriced → **priced SUM only** — unpriced CL8Y is dropped, not listed as a remainder. | | **Rollup** | [`refresh_protocol_fees`](indexer/src/db/queries/protocol_fees.rs): `SUM(fee_usd) FILTER (WHERE fee_usd IS NOT NULL)`. Token mix `ORDER BY amount_usd DESC NULLS LAST`, top 8 + `other`. `clamp_usd` drops `≤0` / overflow. GET `/overview` and `/protocol/fees` stay O(1) / 60s cache (**PFee-8**). | | **Ingest stamp** | [`ingest_protocol_fee`](indexer/src/indexer/parser.rs) → [`fee_usd_for_raw`](indexer/src/indexer/protocol_fees.rs) → [`catalog_usd_per_human_identity`](indexer/src/indexer/pair_price_usd.rs). **AMM / book take commission is the ask asset** (`ask_asset_id`). Buying CL8Y with UST1 therefore stores the fee **in CL8Y**. Limit place uses offer-side (`offer_asset_id_for_side`). Insert is `ON CONFLICT DO NOTHING` — a later catalog fix does **not** fill old `NULL` `fee_usd`. | | **P522-Q catalog** | Only USTC/cUSTC/`uusd`, LUNC/cLUNC/`uluna`, UST1, USTR. Unit test is explicit: `quote_usd_kind("CL8Y", None) == None`. `HubQuoteUsd` has **only** `ust1` / `ustr`. [`load_quote_usd`](indexer/src/db/queries/hub_prices.rs) ignores every other `hub_prices` ticker. | | **Hub bootstrap** | [`hub_usd.rs`](indexer/src/indexer/hub_usd.rs): cUSTC ← USTC oracle; LUNC ← LUNC oracle; UST1 ← max-TVL cUSTC/UST1 pair; USTR ← max-TVL vs already-priced cUSTC or UST1. **No CL8Y pin**, no walk of other listed CW20s. Protocol hub card stays **four** columns (**H11**). | | **Volume / TVL (contrast)** | Same CL8Y/UST1 swap **can** have `volume_usd` via the catalogued UST1 offer/quote (`volume_usd_for_swap`). TVL of a one-sided catalog pair **2×** the priced leg (**P569-5**). Operators therefore see volume/TVL move while **fees in CL8Y stay unpriced**. | | **Llama** | [`defillama.rs`](indexer/src/db/queries/defillama.rs) daily fees `SUM(fee_usd)` with gem-pair exclude. Unpriced economic fees undercount Llama the same way they undercount Protocol. | | **Identity** | CL8Y columbus-5 CW20: `terra16wtml2q66g82fdkx66tap0qjkahqwp4lwq3ngtygacg5q0kzycgqvhpax3` (`VITE_CL8Y_TOKEN_ADDRESS` / `CL8Y_TOKEN_ADDRESS`). Fee-discount I12 already notes wallets may label a bridged asset **CL8Y-cb** — Protocol must key **contract/denom (A1)**, not the display ticker. LocalTerra **TCL8Y** is the 18-dec proxy ([#383](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/383)). | | **Economic vs gem** | Retail economic set includes CL8Y / wraps / UST1 / USTR / vFDUSD ([`pairCatalogRank.ts`](frontend-dapp/src/utils/pairCatalogRank.ts)). Gems (EMBER… / columbus-5 gem addrs) are test. vFDUSD is economic for **discovery** but **must not** be USD-priced from CEX FDUSD (**X4** / #580). | | **Verify today** | `make verify-issue-586` · `613` · `614` · `556` · `569` · `631`. No test that a CL8Y (or uncatalogued economic) ask-side commission stamps `fee_usd`. | ## Why the new implementation is needed 1. **Treasury already received the tokens.** The token table proves ingest works (`amount_human` ≈ 1.298). Blank USD is not “no fee” — it is **unpriced catalog**. Operators and CMM cannot read protocol take from `/protocol`. 2. **Headlines lie by omission.** 24h/7d/30d `SUM` skips `NULL` `fee_usd`. CL8Y (and any later listed economic ask asset) never enters the tiles, source mix USD, or Llama daily fees, even when volume for the same swap is priced off UST1. 3. **P522-Q was scoped to hub stables/wraps.** That was correct for volume quotes and UST1/USTR marks. AMM commission is **ask-denominated**. As soon as the ask is CL8Y, the #586 catalog is insufficient. Hardcoding only CL8Y repeats the bug on the next listed economic token. 4. **CoinGecko is not a substitute.** `ceramicliberty-com` is **BSC-only** today ([#629](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/629) / listings). Do not poll CG / invent a Classic platform price for ingest. 5. **Conflict-noop ingest.** Existing NULL rows stay NULL until an explicit **NULL-only** backfill + rollup refresh. Waiting 30d for decay is not acceptable once marks exist. ## Constraints / guardrails 1. **Keep PFee-1–PFee-13, P550-5, PFee-8, H11, X4, C568-1.** GET `/overview` and `/protocol/fees` stay O(1) rollup / 60s cache. Do **not** `SUM protocol_fee_events` on GET. Do **not** add a fifth Protocol hub column or a CEX `cl8y` tab. Do **not** rewrite **non-null** historical `fee_usd` from the live mark. 2. **Identity is contract/denom (A1).** Pin columbus-5 CL8Y (and LocalTerra `HUB_CL8Y_ADDRESS` / `VITE_CL8Y_TOKEN_ADDRESS`). Do **not** price `symbol=CL8Y` / `CL8Y-cb` / `TCL8Y` on a spoof native or random CW20. Homoglyph / `javascript:` / `../` tickers stay rejected. 3. **DEX reserve spot, not CEX / CG / Venus.** After existing hub bootstrap, price an economic token from the **largest USD-TVL** factory pair vs an **already-priced** catalog/hub leg (same ranking as USTR: humanized TVL, `HUB_USD_TVL_FLOOR`, stale/dust/same-asset skip). Never `$1` CL8Y. Never `ceramicliberty-com`. Never vFDUSD/FDUSD / Venus into fee USD. 4. **All factory-listed economic tokens, not gems.** Include CL8Y and later listed non-gem CW20s that have a qualifying pair. **Exclude** #562 gems (addr + ticker). **Exclude** vFDUSD as a USD handle. Unlisted / foreign pairs stay out (**P1**). Community-tax templates are economic only if factory-listed and not in the gem set. 5. **Partial windows stay priced SUM.** Priced economic + still-unpriced gem → headline is the priced total (not `—`, not `$0`). All activity unpriced → still `null` / `—`. Idle → `$0`. Never `Infinity`. 6. **NULL-only backfill.** One-shot (or aggregator) fill of `protocol_fee_events.fee_usd IS NULL` using current economic marks. **Never** `UPDATE` a non-null stamp. Then `refresh_protocol_fees`. Document as **as-of backfill**, not historical mark-to-market. 7. **Hub card unchanged.** Internal marks may live in `hub_prices` extra rows or a sibling table. `GET /api/v1/hub-prices` allowlist stays `custc` \| `lunc` \| `ust1` \| `ustr` unless product later asks for a CL8Y column (out of scope). `load_quote_usd` / `fee_usd_for_raw` must actually **read** the new marks. 8. **Llama inherits indexer USD.** Do not invent a second Llama pricer. Gem exclude on Llama stays. 9. **Retail copy (#489).** No lecture that “CL8Y uses a pool mark.” Token row: human + USD. Still “reference / indexed,” not settlement. 10. **Out of scope.** Changing on-chain fee bps; pricing gems; CEX CL8Y tab; rewriting volume/TVL 2× rules; Keplr/CG listings; fee-discount I12 (bridged CL8Y-cb still does not register a tier). 11. **Do not bind-mount `indexer/` into root Docker for cargo** (workspace rule). Host `cargo test` / `make verify-issue-*`. ## Relevant files | Path | Role | |------|------| | [`indexer/src/indexer/pair_price_usd.rs`](indexer/src/indexer/pair_price_usd.rs) | P522-Q; `quote_usd_kind("CL8Y")` is `None`; extend lookup after catalog | | [`indexer/src/indexer/protocol_fees.rs`](indexer/src/indexer/protocol_fees.rs) | `fee_usd_for_raw` / `overview_fee_usd_field` | | [`indexer/src/indexer/parser.rs`](indexer/src/indexer/parser.rs) | `ingest_protocol_fee` (ask-asset AMM; offer-side place) | | [`indexer/src/indexer/hub_usd.rs`](indexer/src/indexer/hub_usd.rs) | Bootstrap + ranking; add economic-token walk | | [`indexer/src/db/queries/hub_prices.rs`](indexer/src/db/queries/hub_prices.rs) | `load_quote_usd` today only copies ust1/ustr | | [`indexer/src/db/queries/protocol_fees.rs`](indexer/src/db/queries/protocol_fees.rs) | Window / token `SUM(fee_usd)`; NULL-only backfill hook | | [`indexer/src/config.rs`](indexer/src/config.rs) | Pin `HUB_CL8Y_ADDRESS` (columbus-5 default + LocalTerra override) | | [`indexer/src/db/queries/defillama.rs`](indexer/src/db/queries/defillama.rs) | Inherited daily fee USD | | [`indexer/tests/indexer_protocol_fees.rs`](indexer/tests/indexer_protocol_fees.rs) | Unpriced-null today; add CL8Y / economic priced cases | | [`frontend-dapp/src/components/protocol/ProtocolFeeStats.tsx`](frontend-dapp/src/components/protocol/ProtocolFeeStats.tsx) | Already displays USD when JSON is set | | [`frontend-dapp/src/pages/ProtocolPage.test.tsx`](frontend-dapp/src/pages/ProtocolPage.test.tsx) | Fee panel + XSS token row | | [`frontend-dapp/src/utils/pairCatalogRank.ts`](frontend-dapp/src/utils/pairCatalogRank.ts) | Economic vs gem (do not copy gem addrs into the USD catalog as priced) | | [`skills/AGENTS_FRONTEND_PROTOCOL_STATS.md`](skills/AGENTS_FRONTEND_PROTOCOL_STATS.md) | Extend **PFee-7** | | [`skills/AGENTS_INDEXER_HUB_USD.md`](skills/AGENTS_INDEXER_HUB_USD.md) | Internal CL8Y / economic marks; hub card stays four cells | | [`docs/indexer-invariants.md`](docs/indexer-invariants.md) | Protocol fees row | | [`docs/runbooks/overview-global-stats-brin.md`](docs/runbooks/overview-global-stats-brin.md) | Rollup + unpriced semantics | | [`scripts/qa/verify-issue-586.sh`](scripts/qa/verify-issue-586.sh) | Keep green; add new verify script | ## Recommended direction 1. **Reuse USTR ranking, do not invent a CEX.** After cUSTC / LUNC / UST1 / USTR resolve, walk factory `pair_reserves` for pairs whose **one** leg is already priced and the **other** is a factory-listed economic CW20 (CL8Y pin required; then other non-gem listed contracts). Winner = max humanized USD-TVL, then lex pair address. Persist `usd_per_human` + `source_pair` + `asset_id`. 2. **Wire ingest.** Extend the mark bundle `fee_usd_for_raw` sees (today `HubQuoteUsd` + oracles). Lookup order: P522-Q identity → economic mark by **contract/denom** → `None`. Humanize × mark; overflow / `≤0` / missing pair → `None`. 3. **NULL-only backfill + rollup.** Fill `fee_usd IS NULL` only; refresh `global_stats_24h` + source/token tables. Coolify: set LocalTerra / prod CL8Y pin; no Vite-only pin. 4. **Leave the hub card and CEX tabs alone.** No `GET /hub-prices/cl8y` unless a later issue asks. Frontend needs **no** new tiles if JSON `amount_usd` / `total_fees_*_usd` become decimal strings. 5. **Docs / skill.** **PFee-7** becomes “P522-Q + hub + factory economic marks.” Keep “never vFDUSD / never `$1` / stamp at ingest / NULL-only backfill.” ## Acceptance criteria - [ ] **AC1.** AMM / book / place / wrap-family fees in the **pinned CL8Y** CW20 stamp non-null `fee_usd` when a qualifying CL8Y↔priced-hub pair exists above the TVL floor and hub/oracle for the other leg are up. - [ ] **AC2.** Token table for that window shows human units **and** a finite `$` (not `—`) for CL8Y / CL8Y-cb display symbols. XSS / `javascript:` symbols stay text. - [ ] **AC3.** `total_fees_{24h,7d,30d}_usd` **includes** those CL8Y (and other priced economic) amounts. A window that is only unpriced gems stays `null`. Idle stays `"0"`. - [ ] **AC4.** Other factory-listed **economic** tokens with a qualifying pair are priced the same way — not a CL8Y-only `if symbol == "CL8Y"`. - [ ] **AC5.** Gems stay unpriced (or omitted from USD). vFDUSD is **not** priced from CEX FDUSD / Venus. Spoof `symbol=CL8Y` native / wrong contract stays unpriced. - [ ] **AC6.** Historical **non-null** `fee_usd` is unchanged. Prior NULL rows are filled once marks exist (NULL-only). - [ ] **AC7.** GET `/overview` and `/protocol/fees` still do not scan `protocol_fee_events`. `window=` allowlist unchanged. Hub JSON allowlist unchanged. Protocol hub card still four cells. - [ ] **AC8.** DeFiLlama daily fees pick up the same stamped USD (gem exclude unchanged). - [ ] **AC9.** `make verify-issue-586` / `613` / `614` / `556` / `569` / `631` stay green. New `make verify-issue-<this>` covers catalog + ingest + rollup + UI. ## Test plan (all paths) **Indexer unit / lib** 1. `quote_usd_kind` / catalog identity: CL8Y **symbol alone** still `None`; pinned **contract** resolves via the new mark helper. 2. `fee_usd_for_raw`: CL8Y raw × mark → USD; missing mark / stale pair / below TVL floor / overflow → `None`; UST1/USTR/cUSTC/LUNC unchanged. 3. Spoof native `symbol=CL8Y` / `CL8Y-cb` with no pin contract → `None`. 4. Gem CW20 (EMBER addr) with a UST1 pair → still `None` for fee USD. 5. vFDUSD × CEX FDUSD / Venus → `None`. 6. Ranking: two CL8Y pairs vs UST1 vs cUSTC — winner is max USD-TVL then lex address; dust under floor skipped. **Indexer integration (Postgres; `make setup-indexer-postgres`)** 7. Insert swap_amm fee in pinned CL8Y with hub CL8Y mark present → `protocol_fee_events.fee_usd` set; rollup 24h/7d/30d includes it; `GET /protocol/fees?window=24h` token row has `amount_human` + `amount_usd`. 8. Same events with **no** CL8Y pair / oracle down on the other leg → `fee_usd` NULL; headline `null` if nothing else priced; token row human + USD `null`. 9. Mixed window: priced UST1 + unpriced gem + priced CL8Y → headline = UST1+CL8Y only. 10. NULL-only backfill: pre-existing NULL CL8Y row fills; pre-existing non-null USD **unchanged** when the live mark moves. 11. `ON CONFLICT` replay does not double-count. 12. `GET /protocol/fees?window=javascript:` / `ust1-window` / empty → **400**. Cache key still allowlisted window only. 13. Llama daily agg: CL8Y fee on a non-gem pair counted; gem-pair swap fee still excluded. **Frontend** 14. `ProtocolPage.test.tsx`: token row `symbol: 'CL8Y-cb'`, `amount_human: '1.298'`, `amount_usd: '12.34'` → visible `$` (not only human). `amount_usd: null` still em-dash. 15. XSS symbol + human amount + null USD: text only, no `img` / `innerHTML`. 16. Missing fee fields on old indexer still hide the panel (**PFee-10**). Nested `card-glass` still forbidden. **LocalTerra (optional, not a skip if Postgres tests pass)** 17. After `make setup-cloud-localterra`, seed a TCL8Y/UST1 (or CL8Y pin) pair above dust, swap so commission is TCL8Y, wait aggregator: `/protocol` token USD and 24h tile include it. ## Test plan (attack, hack, and abuse) 1. **Symbol spoof.** Factory-unlisted CW20 or native with `symbol=CL8Y` / `CL8Y-cb` / `TCL8Y` must not mint fee USD. Contract pin + factory provenance (**P1**) win. 2. **Wash / thin pool.** Attacker seeds a $1 CL8Y/UST1 dust pool to print a fantasy mark. **TVL floor** + staleness must skip it; no mark → `None`, not `$1`. 3. **Circular quote.** Do not price CL8Y from a CL8Y/ONYX pair using an unpriced ONYX. Other leg must already be catalog/hub-priced. No recursive unbounded walk on GET. 4. **Hub table injection.** Extra `hub_prices.ticker` (`cl8y`, `../ustr`, `javascript:`) must not appear on `GET /hub-prices/{ticker}` (still 400) and must not expand CEX tabs. 5. **Rewrite attack.** Poller / Coolify restart must not overwrite non-null `fee_usd` when the live CL8Y mark jumps (wash the source pool). NULL-only backfill only. 6. **Double count.** Backfill + conflict replay + hybrid L7 (pool + book once) unchanged. Do not also add `book_commission_amount`. 7. **DoS.** No GET-path `SUM` of 60d `protocol_fee_events`. Economic walk is aggregator / hub-refresh only, bounded by pair count + floor. Token mix still top 8 + `other`. 8. **Cache bust.** Junk query on `/protocol/fees` must not bypass the 60s allowlisted-window cache. 9. **XSS / HTML in symbol or amount_human.** Render as text. No `eval` / `innerHTML` / `javascript:` hrefs. 10. **CoinGecko / UA.** Do not add a live CG poll for `ceramicliberty-com` on the fee path (#579 / BSC-only). 11. **vFDUSD sneak.** Economic-token walker must not treat CEX FDUSD or Venus `exchangeRateCurrent` as `usd(vFDUSD)`. 12. **Gem inflate.** Listing a gem as “economic” via spoofed `token_info.symbol=UST1` is already a hide-by-address rule on the dApp; indexer USD must use **gem addr set**, not symbol. ## Verification criteria ```bash make verify-issue-<this> # new harness: catalog + ingest + NULL-only backfill + rollup + Protocol token USD make verify-issue-586 make verify-issue-613 make verify-issue-614 make verify-issue-556 make verify-issue-569 make verify-issue-631 ``` Pass when: (1) a CL8Y ask-side commission with a qualifying hub pair stamps `fee_usd` and appears in 24h/7d/30d + token `$`; (2) gems / vFDUSD / spoof symbols stay unpriced; (3) GET paths stay rollup-only; (4) hub card still four cells; (5) existing #586 semantics (idle `$0`, all-unpriced `—`, no Inf) hold. ## Notes - Live display ticker **CL8Y-cb** is a wallet / listing label. Treat it as the columbus-5 CL8Y CW20 unless a **different** contract is proven. Do not create a second CG id. - Volume/TVL already count CL8Y/UST1 via the UST1 leg. This issue is **fee-token USD**, not a second volume catalog.
PlasticDigits commented 2026-08-27 01:40:37 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1186

mentioned in merge request !1186
PlasticDigits commented 2026-08-27 04:53:22 +00:00 (Migrated from gitlab.com)

mentioned in commit 82ae5b9bf6

mentioned in commit 82ae5b9bf6d0cdac0e0dd7a1c24122c207b02eae
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-27 04:53:23 +00:00
PlasticDigits commented 2026-08-27 05:29:52 +00:00 (Migrated from gitlab.com)

mentioned in issue #686

mentioned in issue #686
PlasticDigits commented 2026-08-27 05:29:56 +00:00 (Migrated from gitlab.com)

marked as related to #686

marked as related to #686
PlasticDigits commented 2026-08-27 05:30:29 +00:00 (Migrated from gitlab.com)

Merged via !1186 (82ae5b9b) onto main 36d64528.

make verify-issue-683 PASS (11/11, including related 586 / 613 / 614 / 556 / 569 / 631). Coolify leftover: apply 20260827140000_economic_token_marks.sql, set HUB_CL8Y_ADDRESS, redeploy indexer. NULL-only backfill fills prior CL8Y fee_usd; non-null stamps stay put. GET /hub-prices stays four cells. Duplicate ticket #682 should stay closed. Post-merge ops: #686.

Merged via !1186 (`82ae5b9b`) onto `main` `36d64528`. `make verify-issue-683` **PASS** (11/11, including related 586 / 613 / 614 / 556 / 569 / 631). **Coolify leftover:** apply `20260827140000_economic_token_marks.sql`, set `HUB_CL8Y_ADDRESS`, redeploy indexer. NULL-only backfill fills prior CL8Y `fee_usd`; non-null stamps stay put. GET `/hub-prices` stays four cells. Duplicate ticket #682 should stay closed. Post-merge ops: #686.
PlasticDigits commented 2026-08-27 05:30:34 +00:00 (Migrated from gitlab.com)

mentioned in issue #682

mentioned in issue #682
PlasticDigits commented 2026-08-27 06:03:02 +00:00 (Migrated from gitlab.com)

mentioned in issue #687

mentioned in issue #687
PlasticDigits commented 2026-08-27 06:03:03 +00:00 (Migrated from gitlab.com)

marked as related to #687

marked as related to #687
PlasticDigits commented 2026-08-27 06:33:37 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1187

mentioned in merge request !1187
PlasticDigits commented 2026-08-27 07:36:45 +00:00 (Migrated from gitlab.com)

mentioned in commit 298c92ba9a

mentioned in commit 298c92ba9a3075a5182a75375b728e8fd89793fa
PlasticDigits commented 2026-08-27 07:43:02 +00:00 (Migrated from gitlab.com)

mentioned in issue #688

mentioned in issue #688
PlasticDigits commented 2026-08-27 09:00:10 +00:00 (Migrated from gitlab.com)

mentioned in issue #689

mentioned in issue #689
Sign in to join this conversation.
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#683
No description provided.