feat: DEX hub USD for cUSTC/UST1/USTR (largest liquidity) + Protocol USTR #556
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#556
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?
Summary
Replace the hardcoded P522-Q UST1 = $1 and USTR = 2.5 × USTC pegs with DEX-derived hub USD, ranked by largest USD liquidity. Anchor 1 cUSTC = 1 USTC from the existing #515 CEX USTC oracle, then price UST1 from the deepest cUSTC/UST1 pool, then USTR (and other economic tokens) from their deepest pair against an already-priced hub.
Show USTR (and the other hubs) on
/protocolas a DEX reference card — not as a fourth CEX oracle ticker.Related: #522 (pair
price_usd+ catalog), #515 / #550 (CEXustc|lunc|vfdusdonly), #524 / #543 (UI invert of factory USD), #548 (volume_usdingest), #508 (UST1 secondary AMM seed peg). Tape Amount in / Amount out / Price humanization is #557 (rawformatNum).Current codebase
Quote USD catalog (P522-Q)
Indexer
pair_price_usd.rsand dApppairPriceUsd.tsconvert human quote-per-base to USD of 1 humanasset_0using a static catalog:$1(QuoteUsdKind::Peg1)uusduluna2.5 ×USTC (USTR_PER_USTC)NULL(do not invent)That catalog is also
volume_usdingest (#548 C8 / X4). Tests inpair_price_usd.rsassert UST1/cUSTC last print ≈ $1 because quote is cUSTC × oracle, and UST1/USTR ≈ $1 because 79.72 × 2.5 × USTC — not because the pool was marked to a DEX USTR price.Charts / pair APIs
GET /api/v1/pairs/{addr}/trades—priceis human quote-per-base;price_usdis USD of factoryasset_0via P522-Q.GET /api/v1/pairs/{addr}/candles— OHLC is factory USD fromprice_usdonly (#543); additive*_humanfor invert./chartsand/tradeheadline Price (USD) usesresolveTapeLastPriceUsd/ invert (#524). Default invert on UST1-as-base pairs shows the other token’s USD, which today falls back to the same peg catalog wheninvertUsdcannot run.last_pricestays human quote-per-base (P522-5) — do not silently switch aggregators to USD.Protocol page
ProtocolPage.tsxafter #550: Global stats → one CEX oracle card with tabs USTC / LUNC / vFDUSD only (PROTOCOL_ORACLE_TICKERS). Copy: “CEX reference prices. Not settlement.” There is no USTR, no UST1, no cUSTC DEX mark.GET /api/v1/oracle/price/{ticker}is CEX-only (ustc|lunc|vfdusd). Unknown ticker (includingustr) is 400. Overview exposesustc_price_usdonly.Liquidity already mirrored
pair_reserves(book-snapshot loop, #322) has rawreserve_0/reserve_1per factory pair. Nothing ranks “largest USD pool for token T” or publishes a hub USD table.GET /api/v1/tokenshas noprice_usd.Why this is wrong in production
$1of UST1 (or USTR as2.5 × USTC) hides the actual pool mark.$1(UST1) or2.5× USTC(USTR) makes Charts Price (USD), candles, pair Open/Close, and any downstream USD notional disagree with the book.Why the new implementation is needed
Constraints / guardrails
/ust1window rate. Short “DEX reference” label (#489) — no essays.ustr(orust1/custc) to CEXOracleTicker. Keep X1–X2:/oracle/pricecatalog staysustc|lunc|vfdusd.ustron that path stays 400. New DEX prices use a different route (and/or additive overview fields).volume_usd(P550-10). UST1 window ≠ CEX FDUSD ≠ pool UST1.ustcoracle (wrap assumption). Nativeuusdsame. If the USTC oracle is down, cUSTC/UST1/USTR USD is NULL (—), not$0and not a leftover$1peg.symbol=USTRmust not price. Hub CW20s must match tokenlist / known hub addresses (cUSTC, UST1, USTR), not ticker string. Factory provenance (#311) already gates pair insert — do not price unlisted clones.pair_reserves: degrade — skip that pair in ranking (same spirit as book-snapshot degrade-not-error). Do not treat zero reserves as infinite price.swap_eventson every GET. Materialize hub USD on a cadence (book-snapshot and/or volume aggregator, ≤ same order as snapshot interval). Cache reads O(1). Cache-miss must not walk all trades.last_pricestays human quote-per-base (P522-5). Pairpricefield stays human. Onlyprice_usd/ candles USD / Protocol DEX card / overview hub fields change./ust1window math, wrap-mapper fees, limitvalidate_limit_order_price, or ops scriptUSTR_PER_USTCused only to size UST1/USTR LP (rebalance-mint-ust1-lp.sh) — that seed is not a display oracle.asset_0. After hub USD is correct, invert continues to useinvertUsd(price_usd, human)— do not1/xUSD candles.javascript:,../, HTML, unknown → 400 (API) or default/omit (UI). Never interpolate raw query input into SQL or fetch URLs.fits_numeric_38_18and X6. No NaN/Inf in DB or JSON.Relevant files
indexer/src/indexer/pair_price_usd.rsPeg1/Ustr/usd_per_human_quote/volume_usd_for_swapindexer/src/indexer/swap_orientation.rsprice_usdat ingestindexer/src/db/queries/candles.rsindexer/src/db/queries/pair_reserves.rsindexer/src/indexer/book_snapshot.rsindexer/src/api/oracle.rsustrhereindexer/src/indexer/oracle.rsindexer/src/api/overview.rsindexer/src/api/tokens.rsindexer/src/api/pairs.rs*_usdindexer/tests/swap_price_human_usd.rsindexer/tests/volume_usd_catalog.rsindexer/tests/api_oracle.rsfrontend-dapp/src/utils/pairPriceUsd.tsfrontend-dapp/src/utils/protocolOracleTicker.tsfrontend-dapp/src/pages/ProtocolPage.tsxfrontend-dapp/src/components/protocol/ProtocolOracleCard.tsxfrontend-dapp/src/pages/ChartsPage.tsxfrontend-dapp/src/pages/TradePage.tsxdocs/indexer-invariants.mdskills/AGENTS_INDEXER_PAIR_PRICE_USD.mdskills/AGENTS_FRONTEND_PROTOCOL_STATS.mddocs/runbooks/indexer-external-oracle.mdtokenlist/Recommended direction
1. Hub USD resolver (indexer)
New module (name bikeshed-ok:
hub_usd.rs/dex_token_usd.rs):usd(cUSTC) = usd(uusd) = ustc_oracle.usd(UST1) = human_quote_per_base × usd(other_leg)using the spot from reserves (constant-productreserve_quote_human / reserve_base_humanafter decimal scale) — not the last swap print alone (prints can be dust). Last print may be a sanity check / fallback if reserves are missing and the pair still has a recentprice_usd— document which; prefer reserves.Persist a small
token_usd(orhub_prices) snapshot:asset_id,price_usd,source_pair_id,tvl_usd,updated_at. Refresh on book-snapshot success (and on oracle tick so cUSTC moves with USTC without waiting for a reserve change).2. Wire into P522 ingest
Replace
QuoteUsdKind::Peg1and::Ustrlookups withtoken_usdfor that quote asset (still classify so unknown gems stay NULL).volume_usd_for_swapuses the same table. Backfill recentswap_events.price_usd+ candles (idempotent migration / job) from stored humanprice× new quote USD as-of backfill time (advisory; document).Keep
USTR_PER_USTCout of ingest. ClientpairPriceUsd.tsfallback for pre-upgrade indexers may keep a documented legacy path; new indexer must sendprice_usdfrom hub table.3. APIs
Pick one primary read (additive JSON):
GET /api/v1/hub-prices→{ metadata, prices: [{ ticker, asset, price_usd, source_pair, tvl_usd, updated_at }] }with allowlisted tickerscustc|ust1|ustr(and maybeclunc).GET /api/v1/overview:custc_price_usd,ust1_price_usd,ustr_price_usd(null when unresolved). Keepustc_price_usdas the CEX USTC ticker.Do not break existing overview field names.
4. Protocol UI
Add a DEX hub prices card (
data-testid="protocol-dex-hub-prices") between Global stats and the CEX oracle card or immediately after the CEX card — pick one, document indocs/frontend.md. Show cUSTC, UST1, USTR (compact$+formatPairPrice, neverformatNumT). Optional: source pair shortened + explorer (token-identity style, #541). Label DEX reference — not CEX, not settlement.Do not add USTR as a fourth tab on
ProtocolOracleCard. Do not change?ticker=CEX allowlist.5. Charts / Trade audit
After ingest uses hub USD:
$1unless the pool is actually there.$0.00x), USD of 1 USTR ≈ pool USTR mark (today’s order of$0.01class if 2.5× still happens to be near the pool — must track the pool if it diverges).usd(UST1)/100, not2.5 × ustc.Acceptance criteria
usd(cUSTC) = ustc_oracle(and native USTC). Oracle down → NULL, not peg.usd(UST1)comes from the largest USD-TVL factory cUSTC/UST1 pool (hub addresses), via humanized reserves. Not$1.usd(USTR)comes from the largest USD-TVL factory pair vs cUSTC or UST1. Not2.5 × USTC.asset_0USD in API; invert still UI-only)./protocolshows USTR (and cUSTC, UST1) DEX marks. CEX tabs remain exactlyustc|lunc|vfdusd.GET /api/v1/oracle/price/ustr(andust1,custc) remain 400.volume_usdingest uses hub USD for UST1/USTR quotes; still not vFDUSD.docs/indexer-invariants.mdP522-Q updated;make verify-issue-<iid>exists.last_priceunchanged (human).Test plan — functional paths
Indexer unit / integration (
make setup-indexer-postgres)asset_0vsasset_1).pair_reservesskipped.price_usduses hub UST1 (when UST1 is quote) / hub cUSTC (when cUSTC is quote) — not Peg1.price_usduses hub USTR when USTR is quote — not2.5 × ustc.volume_usd_for_swapon USTR offer uses hub USTR.swap_eventsscan).api_oracletests: catalog length 3;ustr400;fdusd400.Frontend RTL
getOraclePrice('ustr')is not called.—, not$0/$1/2.5×.pairPriceUsd/ chart headline: UST1/USTR inverted Last is hub USTR, not$1.formatPairPricenotformatNumcompactTon hub USD.Charts / Trade (mocked indexer)
invertUsd, not1/xof USD (#543).Manual / LocalTerra
make setup-cloud-localterra, Protocol shows hub marks; Charts UST1/cUSTC and UST1/USTR Last vs a hand-computed reserve ratio × oracle (tolerance documented, e.g. 1%).dex.cl8y.com/protocolUSTR visible;/oracle/price/ustrstill 400.Test plan — attack, hack, and abuse
symbol=USTRor clone CW20 tickerpairsrows (never LCD-discovered orphans).pair_reservesonly.—. Do not freeze last$1peg.reserve_*=0) skipped (same as route-solver).fits_numeric_38_18→ NULL, no 500.?ticker=javascript:…still CEX-only allowlist →ustc. Hub API path traversal / extra underscores → 400. No SQL string paste./oracle/pricenumeric body.Verification criteria
Cloud Agent:
make setup-indexer-postgresfor indexer tests; full Charts/Protocol against chain needsmake setup-cloud-localterra. Do not report skip without provisioning.Out of scope
formatNum(#557).total_volume(#553).USTR_PER_USTCin rebalance scripts.changed the description
marked as related to #557
changed the description
mentioned in issue #557
mentioned in merge request !1082
mentioned in merge request !1083
mentioned in commit
f6a06fcd1eMerged in !1083. H3 documented as requested from the !1082 note: USTR is set by the market (largest USD-TVL vs priced cUSTC/UST1), not a fixed
2.5 ×USTC peg. Launch 2.5× remains ops LP seed only (USTR_PER_USTC).Shipped: hub_prices ingest +
GET /api/v1/hub-prices; Protocol DEX card; CEX/oracle/price/ustr|ust1|custcstay 400;volume_usduses hub for UST1/USTR; refresh does not freeze last$1.Problem remaining: portfolio/trader Total realized P&L USD (#551) still uses the pre-#556 client catalog (
quoteTokenUsdpegs) instead of hub prices. Indexer Charts/Tradeprice_usdis hub; the P&L header is not. Follow-up after !1080–!1084.Still open ops/manual: indexer migrate
hub_prices; Coolify;dex.cl8y.com/protocolUSTR visible; LocalTerra reserve-ratio hand check. CI was quota-exceeded.mentioned in issue #551
mentioned in issue #560
marked as related to #560
Post-merge follow-up: #560
mentioned in issue #553
Post-merge review extras:
docs/frontend.md/protocolone-liner still says “one CEX oracle card” and omits the DEX hub card. The Protocol section below it is correct.HUB_CUSTC_ADDRESS/HUB_UST1_ADDRESS/HUB_USTR_ADDRESSon production + LocalTerra indexer env, then migratehub_prices.quoteTokenUsd$1/2.5×fallback whenprice_usdis absent remains the mixed-version path; portfolio P&L still uses that helper (#560).mentioned in issue #565
mentioned in issue #568
marked as related to #568
mentioned in issue #569
marked as related to #570
mentioned in issue #570
mentioned in issue #571
mentioned in issue #572
mentioned in merge request !1093
mentioned in issue #573
mentioned in issue #580
marked as related to #580
mentioned in issue #586
mentioned in issue #589
mentioned in issue #597
mentioned in issue #613
mentioned in issue #618
mentioned in issue #655
marked as related to #655
mentioned in issue #664
mentioned in issue #682
mentioned in issue #683
mentioned in issue #690
mentioned in issue #691
mentioned in issue #692