feat: other-DEX v2 LP in route solver / hybrid swap (3% CMM fee, max 1 foreign hop) #690
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#690
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
Add other-DEX v2 LP (Terraport, TerraSwap, Garuda DeFi, Weso World, LuncSwap) to CL8Y Swap / Trade market routing and the indexer route solver, with a 300 bps (3%) CL8Y surcharge on any path that uses an external hop so our market makers keep the solver edge. Fees go to the CMM treasury. Maximum one external pair per swap. IBC USDC as an intermediate is out of scope here — follow-up issue (linked).
Product goal: more tokens and deeper liquidity for retail, without routing flow away from CL8Y books/pools whenever a CL8Y path is within 3% (plus wrap / foreign AMM costs).
Current codebase
CL8Y routing is factory-local, CW20-only, hybrid-first.
Router (execute)
smartcontracts/contracts/router/src/msg.rs—SwapOperationisTerraSwap { offer, ask, hybrid, min_return }or rejectedNativeSwap.ExecuteSwapOperationsis CW20-hook only (Use CW20 Send to initiate swaps).MAX_HOPS = 4. Optionalunwrap_outputsends the final CW20 to the wrap-mapper (Unwrap { recipient }), not native in.smartcontracts/contracts/router/src/contract.rs— each hopresolve_operationqueries our factoryPair { asset_infos }. Foreign pair addresses cannot be executed. Hop output is balance-delta of the ask CW20 on the router (hop_output_from_balance); zero delta reverts. Blacklist guard (blacklist_guard.rs) only understands CW20 TerraSwap ops.NATIVE_TOKEN_WRAPPING.md). Unwrap fee is wrap-mapperfee_unwrap_bps(~51 bps live target) then treasury InstantWithdraw 1.5% burn tax ≈ 2% all-in (#516). Wrap input isfee_wrap_bps(200 live target = 2%).NativeTokenNotSupported,NativeSwapNotSupported,PairNotFound(error.rs).Indexer route solver
assets.contract_address; edges = our factorypairs(docs/route-solver.md, ADR 0002).GET /api/v1/route/solve= global best execution: top-5 paths, ≤ 4 hops, joint hybrid grid, winner = maxestimated_amount_out_net(#209, #615). Frozen F6 hops excluded (#585).Frontend
/route/solve(#501, #596). Single CL8Y hop is pair-directsend→swap; router only whenops.length ≥ 2(swapRouting.ts).unwrap_output.Other-DEX knowledge today
RegisterListedPair(communityTaxMigratePairs.ts,docs/terraport.md). Address:terra1n75fgfc8clsssrm2k0fswgtzsvstdaah7la6sfu96szdu22xta0q57rqqr.terra1jkndu9w5attpz09ut02sgey5dd3e8sq5watzm0. Pair/router schemas are documented indocs/classic-terraswap-upstream-source.md/docs/terraport.md. Terraport/TerraSwap are not Astroport.garuda-defi.org), Weso (wesoworld.io/wesoswap_factorycandidateterra1veqa6znu8lfdmz9kp9v047chfmn84q5k3pacme75gl8ywmplk92q6xnq2k— unverified in this repo), LuncSwap (luncswap.fun): no in-repo factory pin, pair wasm, or swap message shape.[6036, 8266, 10184]plus listed tax pins (11619 / 11626 / 11630 family). F6 pins livecode_idatCreatePairand re-checks on write (#582). Code 3 adopt+list is NO-GO (#627).terra16j5u6ey7a84g40sr3gd94nzg5w5fm45046k9s2347qhfpwm5fr6sem3lr2— not governanceterra1zlmv2….pair_liquidity_usdfor our pairs only (#655).Why this is needed
Retail can only swap tokens that sit on our factory graph. Many Classic tokens have their deepest v2 LP on Terraport / TerraSwap / other AMMs versus LUNC or USTC. Without foreign hops, those markets are invisible on Swap / Trade market even when a 1-hop other-DEX pool would fill the trade.
We still want CL8Y makers to win whenever our hybrid (pool + book) is close. A mandatory 3% CL8Y surcharge on any route that includes an external pair makes the solver prefer our books unless the foreign path is materially better. That surcharge is protocol revenue to CMM, not a penalty on our LPs.
Constraints / guardrails
Invariants for this issue (XDex-1–XDex-14). Do not ship a venue that cannot satisfy them.
MAX_HOPS). A swap may be only that one foreign pair (no CL8Y hop).terra16j5u6ey7a84g40sr3gd94nzg5w5fm45046k9s2347qhfpwm5fr6sem3lr2in that ask asset (CW20 transfer or native bank send). Not governance. Not pairFEE_CONFIG.treasury. No CL8Y fee-tier discount on this 300 bps. Quote ranking uses post-skim amount.estimated_amount_out_net, but foreign-path out must include: (a) foreign AMM / pair fee, (b) 300 bps XDex-2, (c) wrap and/or unwrap costs when those msgs are actually in the execute envelope. A CL8Y-only hybrid path that loses by < 3% + wrap/foreign fees must win. Do not add a “prefer foreign” boost. Do not rank on pre-fee foreign simulation.ContractInfo.code_idthat is factoryIsCodeIdWhitelisted(same set as F6; fail closed on query error); nativeuluna/uusdhave no code_id and are allowed only as the intermediate side; (2) USD TVL ≥ $1,000 using hub/oracle USD of both reserves (humanized decimals); unpriced reserve → ineligible, not$1; (3) at least one leg is LUNC (ulunaor wrap cLUNC) or USTC (uusdor wrap cUSTC). TOKEN/TOKEN with no LUNC/USTC leg is excluded. USDC IBC is not an intermediate on this ticket.simulation/return_amount/ events as truth. Snapshot pre-hop balances of offer+ask (and native denoms in play), execute, measure delta, revert if ask delta< min_return, if offer spent> declared, if unexpected tokens appear, or if delta is zero. Nobelief_pricetrust. Foreignhybrid/ book params are illegal (foreign venues are pool-only).Pair { asset_infos }and uses that contract. Mismatch with a supplied pair addr → revert. Unknown venue id → revert. Pair must still be on the allowlisted factory at execute (not only at index time).WrapDepositthen unwrap back). Skip unwrap when user wants native USTC/LUNC and the last hop’s ask is that native (do not wrap-mapper InstantWithdraw). Skipping must not send native through the CL8Y router in a way that doubles burn tax (seeNATIVE_TOKEN_WRAPPING.md— user→router→pair is two tax events). Prefer: nativeswapdirectly on the pinned foreign pair withto= user (solo hop) orto= CL8Y router (more hops, CW20 ask only).query) is applied in the quote. Stale or missing fee → fail closed for that edge, do not assume 0.RegisterListedPairforeign pairs (#626 M626-10 / #634). Community-tax extra-debit applies on our listed hops only. Foreign hop stays 1:1 on that venue. Do not add pair/router FoT math (H-01).send→swap.POST /route/solveand rawexecute_swap_operationscannot smuggle a second foreign hop or skip the 300 bps skim. Contract is source of truth.getGasLimitForTxmapping (#475). Wrap+foreign+≥2hop and unwrap combos need measured floors (siblings #587 / #679 / #599). Unmapped send hooks must throw, not silent 600k.Relevant files
Contracts:
smartcontracts/contracts/router/src/{msg,contract,state,error,blacklist_guard}.rs·smartcontracts/packages/dex-common/src/{types,factory,wrap_mapper,blacklist}.rs·smartcontracts/tests/src/lib.rs(router + wrap tests)Indexer:
indexer/src/api/{route_solver,route_paths,route_graph,best_execution,hybrid_route_opt}.rs·indexer/src/indexer/(new foreign-pair ingest) ·indexer/tests/api_route_solve*.rsFrontend:
frontend-dapp/src/utils/cw20RouteSolveQuote.ts·directHybridQuote.ts·services/terraclassic/{router,swapRouting,wrapMapper,terraGas,hybridSwapGas}.ts· Swap + Trade market submit · route row (#158)Docs / pins:
docs/route-solver.md·docs/adr/0002-*.md·docs/terraport.md·NATIVE_TOKEN_WRAPPING.md·docs/runbooks/rotate-fee-treasury.md·frontend-dapp/src/utils/communityTaxMigratePairs.ts(TERRAPORT_FACTORY_ADDRESS)Related issues: #189 / #209 / #323 (solver) · #501 / #596 (retail GET hybrid) · #516 / #523 / #343 (wrap fees / unwrap) · #582 / #585 (code_id) · #607 / #615 (tax hops) · #626 / #634 (do not register foreign pairs) · #562 (gems) · #475 / #587 / #679 / #599 (gas)
Recommended direction
{ venue_id, factory, pair_code_ids?, swap_msg_family: terraswap_v2 }. Start with Terraport + Classic TerraSwap. Others append after LCD probe.pairs, storeexternal_pairs(venue, pair addr, asset_infos, reserves, fee_bps, usd_tvl, code_ids, last_seen). Refresh on a slower cadence than CL8Y books. Drop edges that fail XDex-4.simulationonly for quoting; subtract venue fee (if not already in sim), 300 bps, wrap/unwrap when the envelope includes them. Compare to CL8Y hybridestimated_amount_out_net. Cache key includes venue set + fee bps + wrap flags.SwapOperationvariant (name TBD) carryingvenue_id+ asset infos (not a free pair addr). Execute: factory pin (XDex-6) → balance snapshot → CW20send/ native swap on that pair withto: router(or user on last hop) → delta +min_return→ skim 300 bps to CMM → continue remaining CL8Y hops. Re-use hop reply machine; do not re-enterSWAP_STATEfrom a foreign callback.ops.length == 1foreign still uses the router (or a dedicated guarded execute that still skims 300 bps and checks deltas). Do not teach the dApp tosendstraight to a Terraport pair without the skim / invariants.needs_wrap_in,needs_unwrap_out,foreign_first_native,foreign_last_native. Retail copy stays short (#489): e.g. “Other DEX pool · 3% protocol fee” plus existing wrap fee note when wrap runs.solver_versionbump when ranking changes.Do not: proxy through a foreign router (unbounded hops, extra trust); treat Terraport events as our volume for CG/CMC; list foreign TVL on
/pool; turn hybrid off.Acceptance criteria
/route/solvecan return a path with exactly oneexternal(or equivalent) hop when it beats CL8Y-only after 300 bps + wrap/foreign fees; otherwise CL8Y-only wins.code_ids are factory-whitelisted, USD TVL ≥ $1,000, and one leg is LUNC or USTC (native or wrap).min_return/minimum_receiveeven if the foreign pair’s sim looked good.pool_only=trueon the official dApp.make verify-issue-<iid>.Test plan (functional paths)
Contracts (multitest + mocks of a TerraSwap-shaped pair):
minimum_receivepost-skim.min_returnon foreign hop;minimum_receiveon router.hybridon foreign op; reject spoofed pair addr vs factory pin.unwrap_output: true+ native last hop → revert or ignore unwrap (pick one, test it).code_idat execute; wrap-mapper unset when unwrap still requested.Indexer:
code_id; drop unpriced USD.pool_only=trueintegrator path: still max-1 foreign if allowed at all (or explicitly no foreign — document and test).Frontend (Vitest + Playwright when LocalTerra can mock a foreign pair):
Venues: LCD fixtures for Terraport + TerraSwap factory
Pair+ pairsimulation. Garuda/Weso/LuncSwap: probe issue comment or staydisabled.Test plan (attack, hack, abuse)
simulationliesmin_return→ revert; user not left mid-hop (SWAP_STATEcleared or never committed).PairSwapInProgressor no writable re-entry; no double-skim / drain.minimum_receive.traderspoof to zero the 3%traderstill required on CL8Y hops for tax (#607).min_returnholds — prefer execute-time reserve floor so flash LP cannot advertise then empty.ContractInfo+IsCodeIdWhitelisted; revert.uusdBankMsgnative to router then out again.max_spread/min_return/ deadline remain; solver still advisory.Uint128; zero ask reverts before skim.ContractQueryRejectedwithout treating as infinite liquidity; skip edge.swap_eventsas CL8Y volume.Verification criteria
Close when:
make verify-issue-<iid>exists and covers contracts + indexer unit/integration (Postgres) + frontend Vitest for wrap-skip flags and op shapes.code_ids, CMM balance delta on a dust trade (or dry-run withDRY_RUNdocs). Garuda/Weso/LuncSwap listed as enabled or explicitly disabled with probe notes.RegisterListedPairon foreign addrs. Hybrid checkbox not reintroduced. ADR 0002 +docs/route-solver.mdupdated.Blocked on: venue schema pins for non-TerraSwap forks; wrap-mapper live split fees (#516) for honest 5% math; factory whitelist remaining the listing policy (do not whitelist code 3 / 8654).
mentioned in issue #691
marked as related to #691
Follow-up for IBC USDC as a common intermediate (not in this ticket’s admission set): #691.
USDC stays analysis/design until this issue’s execute path (max-1 foreign hop, 300 bps to CMM, balance-delta) is specified. Do not expand XDex-4 to USDC on the implementation MR for this issue.
mentioned in issue #708
Charge 2.9% instead of 3%, on top of any wrap or 3rd party dex fees (do not cover those with the 2.9%)