fix(swap): wrap-enter native routes must GET /route/solve and drop ~100% hop-spread winners #1218
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#1218
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?
Routing LUNC -> USTR yields a high slippage
Route LUNC>cLUNC>UST1>USTR
But when breaking the step up by 1 trade
Route LUNC>cLUNC
The router then finds a different route with less slippage
Route continued cLUNC>cUSTC>UST1>USTR
The issue - routing may favour less hops, causing high slippage (no cLUNC/UST1 pool), investigate why the cLUNC>cUSTC hop was left out of the original route
+1 on ranking: mainnet Swap still quotes LUNC→USTR as LUNC→cLUNC→UST1→USTR with ~100% worst-hop spread (~50 USTR on a 10M LUNC size) while the wrap-then-cUSTC path is far better.
Missing constraint:
GET_DEFAULT_MAX_HOPSis already 4 (#323), so LUNC→cLUNC→cUSTC→UST1→USTR is enumerable. Independent split of the same size (10M LUNC wrap → 9.8M cLUNC, then cLUNC→cUSTC→UST1→USTR) quotes ~48.4K USTR with ~5.15% worst-hop spread. Rank by simulated output (and reject 100% hop spread) rather than hop count / top-k that drops the wrap+cUSTC candidate.Native wrap-enter / unwrap-exit still never reach GET
/api/v1/route/solve. Swap treats any nativeuluna/uusdpay or receive asnativeRouteInfo(findRouteWithNativeSupport→ first BFS after wrap substitution, thensimulateNativeSwap). Indexer solve inputs remain CW20 contract ids only, so top-K / output ranking on this ticket cannot see wrap-then-cUSTC (or USTC wrap-enteruusd→cUSTC→cLUNC) until native I/O is mapped to wrap-mapper CW20s before solve.Keep execute pool-only on native paths (H596-7); do not attach hybrid to
executeNativeSwap. Unwrap-exit must still setunwrap_outputafter the wrapped ask. Direct 1:1 wrap/unwrap stays mapper-only.+1 on native wrap-enter never reaching GET
/api/v1/route/solve.Missing constraint: native LUNC (
uluna) → listed CL8Y (terra16wtml2q66g82fdkx66tap0qjkahqwp4lwq3ngtygacg5q0kzycgqvhpax3, 18-dec product CW20) is another wrap-enter ask on the same client path (findRouteWithNativeSupport→ first BFSfindRouteafteruluna→cLUNC substitution, thensimulateNativeSwap). It does not go throughquoteCw20ViaRouteSolve. Mixed 6-dec cLUNC / 18-dec CL8Y hops must be ranked by simulated net out (and reject ~100% hop spread), not hop count.Keep H596-7: native execute stays pool-only. Do not fold CW20 USTR→USDT (#1257) into this ticket.
Summary
Retail Swap
/of native LUNC (uluna) → USTR quotes LUNC → cLUNC → UST1 → USTR with ~100% worst-hop spread. Breaking the same size into wrap-then-CW20 (cLUNC → USTR) finds cLUNC → cUSTC → UST1 → USTR with far more USTR. The client prefers the first BFS after wrap substitution; that path never callsGET /api/v1/route/solve.This is one product surface: native wrap-enter / unwrap-exit must use the indexer solver on the wrap-mapper CW20s, then submit those hops pool-only. Bundle (do not split):
uluna→cLUNC anduusd→cUSTC (and the reverse on unwrap-exit) beforequoteCw20ViaRouteSolve.amount_inis post–wrap-fee mint on wrap-enter. Direct 1:1 wrap/unwrap stays mapper-only.executeNativeSwapmust use the solver hop list (stripped ofhybrid/book_input), not a secondfindRouteWithNativeSupportBFS. Unwrap-exit still setsunwrap_outputafter the wrapped ask.terra16wtml2q66g82fdkx66tap0qjkahqwp4lwq3ngtygacg5q0kzycgqvhpax3, 18-dec) is the same client branch. Rank by simulated net out; do not fold mixed-decimal CW20→CW20 honesty (#1257).Given columbus-5 Swap
/with native LUNC pay and USTR ask, wrap env set, and a size in the 10M LUNC classWhen the ticket quotes
Then Route is wrap + cLUNC → cUSTC → UST1 → USTR (or another solver winner with honest hop spread), You Receive is the ~48k-USTR class not ~50 USTR, and submit uses those same pool-only hops plus
wrap_deposit.Probe already on this issue: 10M LUNC wrap → ~9.8M cLUNC; wrap-then-cUSTC quotes ~48.4k USTR / ~5.15% worst-hop spread vs the BFS 2-hop ~50 USTR / ~100% spread.
Recommended title:
fix(swap): wrap-enter native routes must GET /route/solve and drop ~100% hop-spread winners.Not duplicates (do not merge)
estimated_amount_out_net, GET hop cap 4. Native I/O still never reaches it./route/solve. Scale/identity, not wrap-enter.Already implemented — do not re-spec: indexer
route_solve_global_picks_best_path_not_shortest(longer path wins inside top-5);GET_DEFAULT_MAX_HOPS = 4; wrap-fee netting onsimulateNativeSwap/executeNativeSwap; hop-preflightanyHopExceedsMaxSpreadcan disable submit without changing the chosen route.Current codebase
Native pay/receive never calls the solver
SwapPage.tsx: if either token is a native denom and the pair is not direct 1:1 wrap/unwrap,nativeRouteInfo = findRouteWithNativeSupport(...). That branch returns beforequoteCw20ViaRouteSolve.indexerCw20Eligiblerequires both ids to start withterra1and!nativeRouteInfo.findRouteWithNativeSupportsubstitutes wrap-mapper CW20s, thenfindRouteBFS returns the first path (hop cap 4). Afteruluna→cLUNC, a factory edge cLUNC–UST1 makes cLUNC → UST1 → USTR the first hit. cLUNC → cUSTC → UST1 → USTR is never considered.simulateNativeSwap/executeNativeSwapcall that BFS again; submit comment: “Pool-only hops from client BFSfindRoute— never copy hybrid.”Indexer solve inputs are CW20 contract ids only (
docs/route-solver.md). Advanced “Indexer route check” on Swap already errors for natives.Solver ranking (CW20 only)
Optimized
GET /api/v1/route/solvewithamount_in:find_paths_top_k— ≤MAX_PATH_CANDIDATES(5) simple paths, ≤GET_DEFAULT_MAX_HOPS(4), shortest first.OPTIMALITY_SCOPE: optimal within that set, not all simple paths.estimated_amount_out_net(#615).ask_out > ask reserveor 18-dec-into-6-dec full drain) indb_orderbook_sim.rs. That is not applied as a path-enumeration skip when filling top-K, and it does not run on the native BFS path at all.If wrap-enter mapped to
token_in=cLUNC/token_out=USTR, the 3-hop is enumerable. Top-K can still drop it when five shorter (thin) paths fill the cap — hence skip ~100% spread while filling K, not “raise K and hope.”Slippage chrome vs route choice
Hop preflight can set
anyHopExceedsMaxSpreadand Swap disables submit (“Hop spread exceeds slippage protection”). Indexerslippage_percent≥99% is theater (swapQuoteAmountScale.ts) — on the CW20 solve path. Native BFS quotes still paint the 2-hop Route / You Receive. Blocking submit without selecting wrap-then-cUSTC is not a fix.Hub ids (columbus-5, identity by contract):
ulunauusdVITE_LUNC_C_TOKEN_ADDRESS(tokenlist pinterra1437qsl…)terra1nap4dxh9tv35v0ynd9m4k6zt6c0dq6weszc4j5m564kjls56hu7qcr56chterra1f0eqgy9w7e5e7up97vjudqwx38tesf8ylx75x2lv3nwm0clry0pqmgfy72terra1vy3kc0swag2rhn7jz6n72jp0l2ns0p6r6ez5grxq5uhj2rvs97fqfsetxvterra16wtml2q66g82fdkx66tap0qjkahqwp4lwq3ngtygacg5q0kzycgqvhpax3There is no factory pair USTR/cUSTC (#1264); the honest LUNC→USTR hub walk is wrap + cUSTC + UST1.
Why this implementation is needed
Traders who pay native LUNC (the wallet asset) get a ~100% impact quote while the same size, after wrap, is a normal hub walk. Splitting the trade by hand (wrap, then swap) is not retail UX. Hop cap 4 already allows the extra cUSTC hop. The official dApp promised best execution on Swap (#209 / #596) but H596-7 left natives on client BFS, so the solver never sees wrap-then-cUSTC.
Without fail-closed skip of ~100% hop-spread candidates, mapping to GET is not enough: top-5 shortest can still be five thin 1–2 hop paths, and
OPTIMALITY_SCOPEwill honestly never score the 3-hop.Constraints / guardrails
book_inputtoexecuteNativeSwap. Strip solverhybridthe same way wrap+N-hop already strips it (#1264 / #1280).isDirectWrapUnwrap). Do not send those to/route/solve.GET_DEFAULT_MAX_HOPS(already 4) orMAX_PATH_CANDIDATESas the primary fix. Skip unusable paths then fill K. LCD/SOLVE_CONCURRENCYbudget stays documented.amount_infor solve = net CW20 after wrap fee on wrap-enter; You Receive on unwrap-exit still nets mapper unwrap fee + InstantWithdraw burn tax (#512 / #516).Relevant files
frontend-dapp/src/pages/SwapPage.tsxexecuteNativeSwapvsindexerOperationsfrontend-dapp/src/utils/cw20RouteSolveQuote.ts/route/solve+ wallet sim; wrap/native must not copy hop-0 hybrid onto wrap hopsfrontend-dapp/src/services/terraclassic/router.tsfindRoute/findRouteWithNativeSupport/simulateNativeSwap/executeNativeSwapfrontend-dapp/src/utils/swapRouteDisplay.tsfrontend-dapp/src/services/terraclassic/swapRoutePreflight.tsfrontend-dapp/src/utils/swapQuoteAmountScale.tsindexer/src/api/route_paths.rsfind_paths_top_kshortest-first capindexer/src/api/best_execution.rsOPTIMALITY_SCOPE, candidate evalindexer/src/api/route_solver.rsindexer/src/api/db_orderbook_sim.rshop_sim_implausible— reuse for path skip, do not reinventindexer/tests/api_route_solve.rsroute_solve_global_picks_best_path_not_shortestdocs/route-solver.md/docs/adr/0002-global-best-execution-route-solver.mdskills/AGENTS_FRONTEND_HYBRID_ALWAYS_ON.mdskills/AGENTS_FRONTEND_SWAP_ROUTE_DISPLAY.mdscripts/check_route_solver_docs.pyRecommended direction
netCw20AfterNativeWrapwhen paying native, thenquoteCw20ViaRouteSolve. Apply unwrap fee/tax on the ask if receive is native. On indexer/wallet failure, keep today’s BFS fallback (H596-5 degradation) — do not silently submit a 100% spread BFS when a solve winner existed this quote tick.executeNativeSwap(or a sibling that accepts ops). Stop re-running BFS at broadcast. Setunwrap_outputiff native ask.hop_sim_implausible. Continue search until K usable paths or hop cap exhausted. Winner remains maxestimated_amount_out_net. Fixture: 2-hop ~100% drain vs 3-hop honest hub — 3-hop wins;paths_consideredcounts scored usable paths.MAX_PATH_CANDIDATESin this MR unless skip-unusable still cannot admit the wrap-then-cUSTC fixture under a pinned hub graph (then a one-line constant + LCD budget comment + docs drift check — not a second issue).verify-issue-1218script./trade(it should not).Acceptance criteria
/native LUNC → USTR (wrap env on): Route includes cLUNC → cUSTC → UST1 → USTR (or a solver winner with worst-hop spread ≪ 100%), not BFS-only cLUNC → UST1 → USTR, for the 10M LUNC-class size. You Receive is the wrap-then-cUSTC class, not ~50 USTR.executeNativeSwapdoes not BFS a different path than the Route chip.hybrid: null/ nobook_input). Wrap-enter stillwrap_depositthen CW20 send to router. Unwrap-exit stillunwrap_output./route/solve.uusd) wrap-enter to a listed hub CW20 uses the same mapping (symmetric to LUNC). Do not retune #1264 gas here./route/solvewithtoken_in= cLUNC. Rank by simulated net out; reject ~100% hop spread. Mixed 6/18 hops must not print theater You Receive.token_in=uluna(CW20-only graph) unless the chosen design is server-side wrap-map — then document it and keep one mapping site.scripts/qa/verify-issue-1218.sh+ docs/skill pointers.python3 scripts/check_route_solver_docs.pygreen if constants/docs change.Test plan (all paths)
uluna+ USTR callsgetRouteSolve(cLUNC, USTR, netAfterWrap)notsimulateNativeSwapBFS-only. Fixture graph: thin cLUNC–UST1 + deep cLUNC–cUSTC–UST1–USTR. Winner hops include cUSTC.executeNativeSwap(or wrapper) receives those ops; spies show no secondfindRoutethat returns the 2-hop.hybrid: nullon every hop./route/solve.USTR→cLUNC, then unwrap fee/tax on ask;unwrap_outputtrue.uusd→USTRtoken_in= cUSTC; hub walk not first BFS-only.api_route_solveestimated_amount_out;search_truncatedstill honest. Existingroute_solve_global_picks_best_path_not_shorteststays green.anyHopExceedsMaxSpread/ ≥99%).quoteCw20ViaRouteSolveonly; no wrap_deposit.min_returnpre-tax (#615).Negative / skip:
Optional chain: LocalTerra wrap seed + Swap LUNC→hub CW20 when wrap mapper is deployed — quote-only is enough if lcd mock covers P1/P8.
Test plan (attack, hack, and abuse)
Evaluate each vector. Do not write exploits, PoCs, or attack procedures.
book_inputon wrap execute “to fix ranking”token_in=ulunaon public GETulunaas a CW20 nodecode_idstill routedshouldRejectGemBridgeQuotemax_spread/ min receive still bind executeestimated_amount_outwithout sim / floorsVerification criteria
make verify-issue-1218(new): Vitest P1–P7 / P9–P11 class tests + indexer P8 (Postgres + lcd mock / lib as appropriate) + greps thatexecuteNativeSwapno longer maps ops solely fromfindRouteWithNativeSupporton the wrap-enter submit path.route-solver.md/ H596-7 note wrap-enter GET;scripts/check_route_solver_docs.pygreen.Out of scope
First-pass model recommendation
Recommendation: grok-high
Rationale: Cross-cutting retail quote protocol: Swap native branch (
SwapPage.tsx), wrap execute (router.ts), indexer top-K skip-unusable (route_paths.rs/best_execution.rs), and H596-7 (must not “fix” ranking by attaching a book to wrap hops). More than three production files, not a local known edit, and the product patch is not a single constant bump. Composer criteria fail on subsystem count and wrap vs solver ownership. Verify with Vitest + indexer route-solve fixtures, not live-mainnet-only acceptance. Advisory planning text, not a runner argument.DEX Routingto fix(swap): wrap-enter native routes must GET /route/solve and drop ~100% hop-spread winnersMerged as PR #1297. Wrap-enter maps wrap CW20s then GET /route/solve; indexer skip-unusable while filling K. Native wrap hops stay pool-only.
Leftover: LocalTerra
/LUNC→USTR Route wrap-then-cUSTC. Tracked on #1300.Verified on origin/main in the issue worktree: make verify-issue-1218 passed both rounds (10/10 steps), including the native uluna 400 guard, skip-unusable honest 3-hop fixture, 105 frontend tests, and route-solver docs/skill checks. The verifier now isolates SQLx history in dex_indexer_test_1218; its local worktree change is commit
dd9e6301. H1218-1–H1218-8 remain documented and crosslinked in docs/testing.md, docs/frontend.md, docs/integrators.md, docs/indexer-invariants.md, and the frontend/indexer agent skills.The implementation shipped in PR #1297. Post-merge leftovers were tracked by #1300, now closed. The optional LocalTerra E7/E8 transaction check was not run; issue acceptance is covered by the passing fixture and UI checks. Broader remaining solve-failure research stays separate in #1265 / ADR 0007.
Closing #1218.
Follow-up: verifier and documentation improvements are now reviewable in PR #1341 (commit
dd9e6301): #1341. The PR targets main; its Woodpecker PR check is green.