fix(swap): USTR→USDT quotes size-inconsistent through UST1/cLUNC #1257
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#1257
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
Home Swap (
/) quotes listed USTR → listed USDT with implausible, size-inconsistent You Receive values while showing CL8Y-internal multi-hop routes through UST1 and cLUNC (sometimes inserting cUSTC). Both assets are product CW20s intokenRegistry(USTR 18, USDT 18). The session is quote-only (wallet disconnected).Observed on
/?from=USTR&to=…(canonical Swap query, #711 / #713):A constant-product path on fixed token ids cannot improve per-unit output as size grows, and it cannot print billions of USDT for 1 USTR while 10k USTR prints ~80. Something in scale, token identity, or solver scoring is lying. Execution is LIMIT BOOK + POOL (always-on hybrid, #596).
This is not closed #15 (Swap used to send human strings with no
10^decimals). This is not closed #464 (18-decUint128k-overflow; that bricks quotes, it does not print billions). This is not closed #564 / #544 (Charts / pool volume chrome). This is not closed #529 (limit-price cap on UST1/USTR). This is not closed #678 (unfunded-size acquire copy). This is not open #1218 (LUNC→USTR hop-count / native wrap never reachingGET /route/solve). This is not open #1255 (unlisted factory CW20getDecimals ?? 6; that ticket already carves out listed USTR/USDT). This is not ops #1221 (bridge + CMM seed USDT/cLUNC). This is not #690 (other-DEX v2 hops). Do not fold those tickets into this one.Venue 24h volume rankings (CL8Y vs Terraport / TerraSwap / Garuda / Weso / …) are context only. They do not make foreign hops the fix. Ship a correct CL8Y-graph quote first.
Current codebase
USTR and USDT are registry product tokens:
frontend-dapp/src/utils/tokenRegistry.ts— USTR 18, USDT 18; CW20 map includesterra1vy3kc0swag2rhn7jz6n72jp0l2ns0p6r6ez5grxq5uhj2rvs97fqfsetxv(USTR) andterra1z0xe7t5ymmltg4vju8tghkq0pewy4et548ta23nlu9zxtl950uyqkv8mv4(USDT).tokenRegistry+ wrap mapper). First hop is mixed 18/6 (USTR/UST1). Last hop is mixed 6/18 (cLUNC/USDT) once a USDT/cLUNC factory pair exists (#1221).CW20 Swap/Trade market quotes go through indexer global best-execution, then a wallet sim of the returned ops:
quoteCw20ViaRouteSolve(frontend-dapp/src/utils/cw20RouteSolveQuote.ts) —GET /api/v1/route/solve+simulateMultiHopSwap+ hop preflight. You Receive is walletreturn_amount(net of catalog buy split, #615), humanized withgetDecimals/fromRawAmount.SwapPage.tsx—offerDecimals/receiveDecimals;simRaw = toRawAmount(amount, offerDecimals). Disconnected sessions still quote (#678).find_paths_top_k(≤5 simple paths, shortest-first), then maxestimated_amount_out_net(docs/route-solver.md,best_execution.rs,route_paths.rs).GET_DEFAULT_MAX_HOPS = 4.AMOUNT_CACHE_BUCKET = 1_000_000raw offer units./route/solve. If both the 3-hop and 4-hop still print billions, ranking among those candidates is not enough — the per-hop sim / humanize is wrong or the USDT identity is not the registry id.Slippage chrome (
docs/frontend.md,swapRouteSlippage.ts,docs/swap-max-spread-ux.md): >5% banner, >30% Expert block, ≥99% extreme alert. Unit-size 9e9 USDT out did not fail-close. Eitherspot_amount_out/slippage_percentis scaled with the same lie, or the 99% gate is not applied to indexer multihop receive chrome.Picker chrome: some quotes showed non-registry USDT art, later quotes the tokenlist Tether logo. Identity is contract / denom, never ticker (#715 X1 / QS-4). A second factory CW20 named USDT is a check (then #1255 on the ask leg). It does not replace mixed-decimal hop tests on the registry pair.
Why the new implementation is needed
GET /route/solve+ wallet sim + You Receive.Constraints / guardrails
findRouteWithNativeSupport/simulateNativeSwap. Keep H596-7 (native paths pool-only) untouched.CW20_MAPUSDT, that is #1255 — fail closed here until decimals resolve; do not duplicate that helper.tokenlist.jsondecimalsas the amount source (C542-11). Registry is the fast path for these two product ids; hop legs use indexer pair-leg / LCDtoken_infoin0…18.contract_addr/ denom. Two CW20s withsymbol=USDTmust not share a quote. Deep linkfrom=USTR&to=USDTfollows #715 unique-symbol CI; colliding USDT → unique-symbol rules, not “first USDT in the picker.”estimated_amount_outthat humanizes 10^9× away from hub/oracle cross (USTR hub vs USDT). Do not show it as You Receive. Prefer no quote / Calculating / extreme-slippage block over theater.estimated_amount_out_net), not hop count.MAX_PATH_CANDIDATES = 5and hop cap 4 stay unless a test proves the honest USTR→USDT path is outside top-5 shortest — then raise K with a LCD-budget note indocs/route-solver.md, do not silently take first BFS.AMOUNT_CACHE_BUCKETmust not alias distinct 18-dec sizes or mix 6-dec and 18-dec keys. 1 USTR (10^18) must not share a cache entry with a 6-dec1000000offer on another pair.estimated_amount_outmust agree on raw USDT. You Receive uses the same decimals assimulateSwap/ router execute. No “display 18, submit 6.”/ust1mint/redeem. Do not add window copy to this ticket.?showGems=1.Relevant files
frontend-dapp/src/pages/SwapPage.tsxsimRaw, You Receive, route chrome, quote-onlyfrontend-dapp/src/utils/cw20RouteSolveQuote.tsreturn_amountvs spotfrontend-dapp/src/utils/formatAmount.tsgetDecimals/toRawAmount/fromRawAmountfrontend-dapp/src/utils/tokenRegistry.tsfrontend-dapp/src/utils/swapRouteSlippage.tsspot_amount_outfrontend-dapp/src/utils/swapRouteDisplay.tsintermediate_tokensfrontend-dapp/src/services/terraclassic/swapRoutePreflight.tsindexerroute_paths.rs/best_execution.rs/hybrid_route_opt.rs/route_solver.rsdocs/route-solver.mddocs/frontend.mddocs/swap-max-spread-ux.mdskills/AGENTS_FRONTEND_SWAP_ROUTE_DISPLAY.mdRecommended direction
CW20_MAPterra1z0xe7…(or documented unique-symbol winner). If LCDtoken_info.decimals≠ 18, fail closed (do not quote). Log/test the id used fortoken_outandreceiveDecimals.token_info, not a single 6. Vitest/Rust: offer10^18USTR into UST1/cLUNC hops must not treat offer as10^6. Last hop cLUNC → USDT must return USDT raw at 18.fromRawAmount(walletSim, 18)for registry USDT. A fixture where sim raw is9.091e15must display0.009091, never9.091B. Inverse: human1USTR →simRaw = 10^18.Expected vs actual
10^18raw. You Receive is USDT at 18-dec, in a plausible band vs hub USTR USD and USDT ~$1 (order-of-magnitude, not a peg promise). Larger pay on the same ids cannot print a higher per-unit USDT out. 10^9-scale receive is blocked.Acceptance criteria
Given picker pay = registry USTR and receive = registry USDT (both LCD decimals 18), user on
/When they enter human
1and the CW20 quote settlesThen
simRaw/amount_inis10^18, You Receive is not 10^9-scale, and it equalsfromRawAmount(walletSim, 18)wherewalletSimmatches indexerestimated_amount_out(pre-buy-tax).Given the same token ids and human amounts
100,1000,10000When three quotes settle
Then USDT-per-USTR is monotonically non-increasing, or the disclosed route (ops /
intermediate_tokens) changed and each quote still passes the reserve/spread fail-closed checks. None of the three display 10^9-scale USDT.Given a candidate path whose last hop (cLUNC→USDT or UST1→…→USDT) simulates ask out greater than that hop’s ask reserve, or worst-hop spread ~100%
When
GET /route/solveranks pathsThen that path is not the winner; Swap shows no theater You Receive for it (no quote / next-best honest path / extreme block).
Given
spot_amount_outand wallet out that differ by ≥99% on this pairWhen the quote paints
Then the existing ≥99% extreme path is visible and submit stays blocked (Expert does not waive 99%).
Given two factory CW20s with
symbol=USDTWhen quoting USTR → USDT from
/?from=USTR&to=USDTThen
token_outis the unique-symbol winner (#715), never a silent other USDT; decimals follow that contract.AC: Direct listed 6-dec pairs (UST1/cUSTC) and USTR→UST1 1-hop (if in top-K) do not regress. Native LUNC wrap remains #1218.
AC: Docs: Swap USTR/USDT (18↔6 hops) use per-leg decimals; implausible hop sims are dropped.
make verify-issue-<iid>green.Test plan (functional paths)
110^18; receive human not 10^9-scale100/1000/100009.091e15USDT 18-dec0.009091, not9.091B10^18symbol=USDT1Test plan — attack, hack, and abuse
fromRawAmount(..., 6)is used for registry USDT.AMOUNT_CACHE_BUCKETacross 6 vs 18?showGems=1shouldRejectGemBridgeQuote).token_info.decimals25510**255.simRawmatch the same decimals (#356).Verification criteria
Manual: on
/with USTR→USDT, type1then10000. Unit size must not show billions while 10k shows tens of USDT. Route chip must match wallet ops. 99% / hop preflight must catch leftover theater.Out of scope
mainvia #464).First-pass model recommendation
Recommendation: grok-high
Rationale: Root cause is not a one-helper patch. The failing surface spans indexer top-K + mixed 18/6 hop simulation (
route_paths/best_execution/ hop sim), Swap humanize (SwapPage,formatAmount,cw20RouteSolveQuote), and 99% / preflight fail-closed. That is more than three production files, crosses subsystems, and is funds-adjacent quote/execute scale (wrong You Receive is a money bug). Comparable DEX work is #1218 / #690 (solver + Swap), not a single RCA doc or test-helper change. Verification needs fixtures at four sizes plus hop-reserve checks, not a single deterministic local edit. Composer’s bar fails on file count, cross-cutting protocol/quote state, and uncertain ranking-vs-scale diagnosis. Grok high for the first pass./agent implement
cl8y-agent-control: queued
implementjob5c405e06-c2b4-4a4c-9db7-37efc763ceac(not executed; no Hetzner VM).