feat(factory): allowlisted IBC natives starting with USDC #1220

Open
opened 2026-09-11 06:36:51 +00:00 by PlasticDigits · 1 comment

Summary

Add allowlisted IBC bank denoms as first-class CL8Y factory pair assets, starting with one pinned columbus-5 USDC denom. Retail should be able to create listed pairs, provide/withdraw, and swap CW20 ↔ that USDC (and later other allowlisted IBC natives) on this DEX — not only as a foreign-hop intermediate on other venues.

Today the stack is CW20-only: factory, pair, and router reject AssetInfo::NativeToken. LUNC/USTC reach the graph only via wrap CW20s. Unknown ibc/… strings display as raw denoms and Swap/Create query parsers treat ibc/ as hostile.

This is not #691 (IBC USDC as other-DEX routing intermediate). #691 U7 explicitly leaves CL8Y factory USDC markets to a later ticket. This is that ticket. Do not expand #690/#691 admission here. Circle CCTP (#1215) is a different mint path and stays out of scope.

Definition of Ready (GWT)

  • Given a factory whose native-IBC allowlist contains exactly one pinned columbus-5 USDC denom (ibc/ + 64 hex) and a CW20 whose live code_id is factory-whitelisted

  • When CreatePair is submitted with those two AssetInfos plus the pair-creation fee

  • Then the pair instantiates, is queryable, and F6 pins the CW20 code_id only (native leg has no code_id)

  • Given that pair has liquidity

  • When a user swaps the listed CW20 for the pinned USDC (CW20 send) and the reverse (native IBC attached to pair/router execute)

  • Then execute matches quote within min_return / minimum_receive; the user receives the declared ask denom; unlisted ibc/… denoms revert at create and at execute


Current codebase

CL8Y pairs and routing are factory-local and CW20-only. Native LUNC/USTC never sit on a pair; they wrap to cLUNC/cUSTC first.

Factory / pair / router

Indexer / oracles / tokenlist

  • Route-solver nodes are CW20 contract addresses (docs/route-solver.md). Native uluna/uusd are not routable except via wrap CW20s.
  • indexer/src/indexer/asset_resolver.rs — retail labels only uluna→LUNC and uusd→USTC. Unknown natives including ibc/… insert as raw denom (N630-5). Native upsert hardcodes 6 decimals.
  • USD oracles: USTC / LUNC / CEX-FDUSD (vfdusd). No USDC feed. Do not treat vFDUSD, UST1, or $1 as USDC (#580, #691 U4).
  • tokenlist/tokenlist.json — natives are LUNC/USTC only. Duplicate tickers fail CI (#715 TL-1).

Frontend

Upstream contrast

Classic TerraSwap does support AssetInfo::NativeToken including ibc/[A-F0-9]{64} and typically 0 burn tax on IBC bank sends (is_ibc_token() in docs/classic-terraswap-upstream-source.md). That is not CL8Y behavior today.

Classic USDC is not one denom. Axelar-bridged uusdc vs Noble native USDC are different ibc/… hashes. Phoenix-1 hashes must not be copied onto columbus-5.


Why this is needed

Retail and LPs cannot hold or trade IBC USDC on CL8Y pools/books. Wrap cannot paper over this: there is no CMM USDC wrap. Other-DEX TOKEN/USDC liquidity (#691) does not create a CL8Y USDC market, fee, or TVL. Allowlisting one well-identified USDC denom unblocks honest USDC pairs without opening every IBC hash.


Constraints / guardrails

ID Rule
I1 — allowlist, not all IBC Factory stores an explicit native-denom allowlist (governance / 2-of-3, same authority as AddWhitelistedCodeId). Unlisted ibc/…, uluna, uusd, factory/…, and junk strings revert at CreatePair and at pair/router execute.
I2 — one USDC pin First listed IBC denom is one columbus-5 USDC hash (document channel, counterparty, decimals, LCD evidence). Axelar vs Noble are not fungible. A second hash = new issue or an explicit multi-pin table with no implicit 1:1. Do not copy phoenix-1 denoms.
I3 — no fake wrap Do not invent cUSDC or a wrap-mapper mapping. IBC in/out is bank send on the pair/router. Do not send IBC through treasury InstantWithdraw. uluna/uusd stay on the existing wrap path; this ticket does not list LUNC/USTC as native pair legs.
I4 — F6 mixed pairs CW20 leg: live code_id still must be whitelisted and pinned. Native IBC leg: no code_id pin (skip or sentinel that cannot be confused with a real id). RefreshAssetCodeIds / freeze still apply to the CW20 leg. TOKEN/USDC where TOKEN wasm is unlisted stays ineligible.
I5 — denom regex Accept only ^ibc/[A-F0-9]{64}$ (case-normalize to uppercase hex as Terra bank does). Reject lowercase mix, truncated hashes, ibc/USDC, CW20 terra1 in the native slot.
I6 — burn tax Confirm columbus-5 tax on the pinned denom (expect 0). Quotes and execute must use the same tax. Fail closed if the querier disagrees. Never apply USTC 1.5% to an IBC send by denom mix-up.
I7 — oracle USD(USDC) is a real feed (CEX USDC or documented stable oracle), fail closed if down. Not $1, not vFDUSD, not UST1 peg.
I8 — ticker Retail USDC only for the pinned denom. Any other ibc/… stays raw (N630-5). Unique-symbol CI (TL-1): one USDC row.
I9 — query params Hostile ibc/ ignore remains for unlisted hashes. The pinned denom (and later allowlisted denoms) may appear in Swap from/to and Create a/b after factory allowlist + tokenlist pin. Never echo raw hostile query into combobox/share.
I10 — not #691 Do not add IBC USDC as a foreign Terraport/TerraSwap intermediate here. Do not relax #690 XDex-4. Do not RegisterListedPair on foreign pair addrs.
I11 — 2-of-3 Adding/removing a native denom on columbus-5 is governance. Code can ship with empty allowlist + LocalTerra fixtures; mainnet pin is an ops/governance follow-through, not a hidden deploy in this MR.
I12 — pair-creation fee #276 uluna fee still required. Native IBC does not skip the fee or F6 on the other leg.

Relevant files

Path Why
smartcontracts/packages/dex-common/src/types.rs Native vs CW20; stop using assert_is_token() as the only gate
smartcontracts/packages/dex-common/src/factory.rs CreatePair + new add/remove/query native-denom msgs
smartcontracts/contracts/factory/src/contract.rs + state.rs Allowlist map; mixed-asset CreatePair; instantiate pair with native leg
smartcontracts/contracts/pair/src/contract.rs Provide / swap / withdraw / hybrid with one native IBC reserve (BankMsg)
smartcontracts/contracts/pair/src/asset_code_id_guard.rs Skip code_id for native IBC; keep CW20 pin
smartcontracts/contracts/pair/src/blacklist_guard.rs Native denom in blacklist checks
smartcontracts/contracts/router/src/contract.rs + msg.rs Native IBC hop in/out without wrap-mapper; funds on first hop
smartcontracts/contracts/router/src/blacklist_guard.rs Native offer/ask
smartcontracts/tests/ Mixed-pair multitest: create, LP, swap both directions, unlisted denom
indexer/src/indexer/asset_resolver.rs Pin USDC labels + real decimals (not assume 6)
indexer route-solver / pair ingest Native IBC graph node for factory pairs
tokenlist/tokenlist.json + logo One USDC native row; TL-1
frontend-dapp/src/utils/createPairTokenCatalog.ts Allow pinned IBC native on Create (exception to C542-2)
frontend-dapp/src/utils/swapQueryParams.ts + createPairQuery.ts Allowlisted ibc/HASH is not hostile
frontend-dapp Swap / Pool provide / Trade Native pay/receive without wrap UI
docs/frontend.md + NATIVE_TOKEN_WRAPPING.md IBC native ≠ wrap; C542-2 / N630-5 / Q711-3 amendments
skills/AGENTS_FRONTEND_CREATE_PAIR_PICKER.md Catalog may include pinned IBC native
skills/AGENTS_FRONTEND_NATIVE_TICKERS.md USDC ticker only for pin
docs/runbooks/ or factory listing policy Native-denom governance vs CW20 code_id

  1. Pin — LCD on columbus-5: trace the USDC denom that actually exists (IBC denom trace / bank metadata / any existing Terraport pairs). Record hash, channel, counterparty, decimals. Reuse #691 LCD tables if they already exist; do not block this ticket on #691 go/no-go for foreign hops.
  2. Factory allowlist — AddWhitelistedNativeDenom / RemoveWhitelistedNativeDenom / IsNativeDenomWhitelisted (names TBD), governance-only. Regex-validate. Empty by default in tests unless the test inserts the pin.
  3. CreatePair — accept exactly one of:
    • two listed CW20s (unchanged), or
    • one listed CW20 + one allowlisted IBC native.
      Reject two natives in v1 (no USDC/LUNC native-native pair; LUNC stays wrapped). Reject uluna/uusd as the native leg.
  4. Pair wasm — reserves: CW20 balance + bank balance of the pinned denom. Provide: CW20 send/transfer + info.funds for IBC. Swap: existing CW20 hook when offer is CW20; ExecuteMsg::Swap with funds when offer is the pinned native. Output native → BankMsg::Send. Hybrid/book: native rung amounts are bank denoms; do not wrap mid-hop.
  5. F6 — pin array stays two slots: CW20 code_id + 0 (or a dedicated None) for the native slot. Guards that call assert_is_token() must branch. Unpinned native slot is not a freeze of the CW20 leg.
  6. Router — allow SwapOperation native IBC offer/ask only if factory Pair { asset_infos } returns that pair (same pin-not-caller-addr rule as foreign hops, but this is our factory). First hop native: execute with attached coins, not CW20 hook. Last hop native: bank send to recipient; unwrap_output illegal. No wrap-mapper.
  7. Indexer — ingest factory pairs with a native IBC asset; graph node key = denom string; label USDC only for the pin; decimals from LCD/tokenlist not hardcoded 6 if the pin differs. Solver can route CW20↔USDC on our pairs. USDC oracle ticker (#515-style), fail closed.
  8. Frontend / tokenlist — add native USDC row. Create Pair catalog includes it (still no uluna/uusd). Swap picker shows USDC. Deep links: allow only the pinned hash (or a USDC symbol that resolves uniquely — TL-1). Provide/withdraw UI must attach bank funds. No wrap toggle for USDC.
  9. Docs/skills — amend C542-2 (exception: allowlisted IBC only), Q711-3 (pinned hash not hostile), N630-5 (USDC ticker for pin only), wrapping doc (IBC ≠ treasury wrap).

Do not: permissionless IBC listing; treating all stables as one hub; IBC transfer/bridge UI; CCTP mint; copying TerraSwap router unbounded native support for uluna.


Acceptance criteria

  • AC1. Factory create of listed CW20 + pinned USDC succeeds; create with any other ibc/… or uluna/uusd native reverts.
  • AC2. Governance add/remove native denom is authorized the same way as code_id whitelist; unauthorized caller reverts.
  • AC3. Pair provide, withdraw, swap both directions, and (if enabled) limit/hybrid on the mixed pair work; CW20-only pairs unchanged.
  • AC4. Router 1-hop and multi-hop that include the USDC leg execute with balance-delta / minimum_receive; unwrap_output + USDC ask reverts or is ignored (pick one, test it).
  • AC5. F6: migrating the CW20 still freezes writes; native leg cannot be used to skip whitelist on the CW20.
  • AC6. Tokenlist + indexer + dApp show USDC for the pin only; other ibc hashes stay raw; TL-1 unique symbol.
  • AC7. Create Pair picker offers the pinned USDC; still excludes LUNC/USTC natives. Hostile ibc/ query ignored; pinned hash may prefill.
  • AC8. Swap/Trade market quote+execute for a factory USDC pair without wrap msgs. Gas inventory includes native-in execute.
  • AC9. Oracle: unpriced/down USDC feed does not emit $1 USD TVL; fail closed for USD stats that need it.
  • AC10. Docs/skills + make verify-issue-<id>. Mainnet allowlist remains empty until governance; LocalTerra/tests inject the pin.

Test plan (functional)

# Path Expect
T1 CreatePair CW20 + pinned USDC Pair exists; query asset_infos mixed
T2 CreatePair CW20 + unlisted ibc/… Revert
T3 CreatePair two natives / uluna+CW20 Revert
T4 Provide mixed LP Reserves: CW20 + bank USDC
T5 Swap CW20 → USDC User bank balance of pin increases; min_return
T6 Swap USDC → CW20 (Swap + funds) CW20 out; extra/wrong denom funds ignored or revert (document)
T7 Router 2-hop via USDC Delta + minimum_receive; no wrap-mapper
T8 Remove denom from allowlist New create reverts; existing pair write policy documented (prefer fail-closed swap if denom dropped)
T9 F6 migrate CW20 off whitelist Writes revert; native reserve cannot bypass
T10 Indexer ingest mixed pair Asset row symbol USDC, decimals from pin, graph edge
T11 Unknown ibc/FOO display Raw denom, not USDC
T12 Create Pair UI: pick USDC + listed CW20 Submit encodes native_token; still charges uluna fee
T13 Swap ?from=<pin>&to=<cw20> Applies; random ibc/ still ignored
T14 Existing CW20-only swap / wrap LUNC Unchanged
T15 Duplicate tokenlist symbol USDC CI fail (TL-1)

Contracts: multitest with a fake IBC denom in the allowlist (need not be a live hash). Indexer: unit tests on native_retail_meta + pair ingest fixture. Frontend: Vitest catalog + query-parser tables; Playwright when LocalTerra can mint a native denom or bank-fund a test ibc/….


Test plan (attack, hack, abuse)

Wasm / listing / wallet surface — required.

Vector Expect
Denom spoof — Axelar hash vs Noble hash vs truncated ibc/ Only the allowlisted pin creates or swaps
CW20 named USDC Still F6 code_id; not the native pin; no ticker collision in tokenlist (TL-1)
Caller pair addr not factory Pair Router re-queries factory; mismatch reverts
Wrong coin in info.funds Credit only the declared offer denom; extras do not mint LP or fill minimum_receive
Donation of a second ibc denom to the pair Ignore; reserves/delta only on declared assets
$1 peg / vFDUSD as USDC Oracle fail-closed; forbidden
Skip F6 by putting junk wasm opposite USDC Create still requires CW20 whitelist
Permissionless listing of arbitrary IBC No path except governance add
Wrap-mapper unwrap on USDC ask Revert / ignore; user must not pay InstantWithdraw
Burn-tax mix-up (treat USDC as uusd) Quotes would be wrong; tests prove 0 tax path for pin vs 1.5% USTC wrap path
Query XSS ibc/<script> Still hostile; never echo
Blacklist bypass via native hop Native denom included in blacklist checks
Reentrancy on BankMsg then CW20 hook Existing SwapInProgress / no writable re-entry
Phoenix-1 denom in tokenlist Reject unless columbus-5 LCD matches

Verification criteria

Close when:

  1. make verify-issue-<iid> covers factory allowlist + mixed CreatePair + pair swap both ways + router no-wrap + indexer labels/decimals + frontend catalog/parser tests.
  2. LocalTerra (or multitest with bank denom) executes T1–T7 and T14. Unauthorized denom add fails.
  3. Docs: C542-2 exception, Q711-3 allowlist, N630-5 USDC pin, wrapping doc states IBC ≠ treasury wrap.
  4. Columbus-5 pin is written on the issue (hash, channel, decimals) before any mainnet governance add. Empty on-chain allowlist is OK for the merge.

Blocked on: a verified columbus-5 USDC denom (LCD). Code may land with a test-only pin. Mainnet AddWhitelistedNativeDenom is governance, not this MR's hidden side effect.


Out of scope

  • #691 / #690 other-DEX USDC intermediate and 300 bps foreign skim.
  • CCTP / Circle mint (#1215).
  • IBC transfer UI, packet timeout handling, relayer ops.
  • cUSDC wrap in ustr-cmm.
  • Native uluna/uusd CL8Y pair legs (keep wrap).
  • Second IBC ticker (ATOM, axlUSDC-as-second-hash, etc.) after USDC.
  • Permissionless IBC listing.
  • Treating UST1 / vFDUSD / USTC as USDC.

First-pass model recommendation

Recommendation: grok-high

Rationale: This breaks the CW20-only invariant across factory, pair, router, dex-common, indexer, tokenlist, and Swap/Create frontend (far more than three production files, multiple subsystems). It is founder-required wasm plus 2-of-3 listing of a bank denom, with burn-tax, F6 mixed pins, and oracle/ticker identity risk. There is no existing local pattern for native IBC on CL8Y pairs (wrap-mapper is LUNC/USTC only). Verification needs contract multitest, indexer ingest, and dApp fund-attachment paths — not a single-file deterministic edit. Composer criteria fail on file/subsystem count, protocol change, and contracts/listing surface.

## Summary Add **allowlisted IBC bank denoms** as first-class CL8Y factory pair assets, starting with **one pinned columbus-5 USDC denom**. Retail should be able to create listed pairs, provide/withdraw, and swap **CW20 ↔ that USDC** (and later other allowlisted IBC natives) on **this** DEX — not only as a foreign-hop intermediate on other venues. Today the stack is **CW20-only**: factory, pair, and router reject `AssetInfo::NativeToken`. LUNC/USTC reach the graph only via wrap CW20s. Unknown `ibc/…` strings display as raw denoms and Swap/Create query parsers treat `ibc/` as hostile. This is **not** [#691](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/691) (IBC USDC as **other-DEX routing intermediate**). #691 **U7** explicitly leaves CL8Y factory USDC markets to a later ticket. This is that ticket. Do not expand #690/#691 admission here. Circle CCTP ([#1215](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1215)) is a different mint path and stays out of scope. ### Definition of Ready (GWT) - **Given** a factory whose native-IBC allowlist contains exactly one pinned columbus-5 USDC denom (`ibc/` + 64 hex) and a CW20 whose live `code_id` is factory-whitelisted - **When** `CreatePair` is submitted with those two `AssetInfo`s plus the pair-creation fee - **Then** the pair instantiates, is queryable, and F6 pins the **CW20** `code_id` only (native leg has no `code_id`) - **Given** that pair has liquidity - **When** a user swaps the listed CW20 for the pinned USDC (CW20 send) and the reverse (native IBC attached to pair/router execute) - **Then** execute matches quote within `min_return` / `minimum_receive`; the user receives the declared ask denom; unlisted `ibc/…` denoms revert at create and at execute --- ## Current codebase CL8Y pairs and routing are **factory-local and CW20-only**. Native LUNC/USTC never sit on a pair; they wrap to cLUNC/cUSTC first. ### Factory / pair / router - [`smartcontracts/packages/dex-common/src/types.rs`](smartcontracts/packages/dex-common/src/types.rs) — `AssetInfo::NativeToken` exists for wire compatibility. `assert_is_token()` errors: *Native tokens are not supported; use CW20 wrapped tokens instead*. - [`smartcontracts/packages/dex-common/src/factory.rs`](smartcontracts/packages/dex-common/src/factory.rs) — `CreatePair` docs: only `AssetInfo::Token`; natives rejected. - [`smartcontracts/contracts/factory/src/contract.rs`](smartcontracts/contracts/factory/src/contract.rs) `execute_create_pair` — both legs `assert_is_token()` → `NativeTokenNotSupported`. Listing gate is **F6 `WHITELISTED_CODE_IDS`**. IBC denoms have **no** `code_id`. - Governance listing today: `AddWhitelistedCodeId` / `RemoveWhitelistedCodeId` only. **No native-denom map.** - [`smartcontracts/contracts/pair/src/asset_code_id_guard.rs`](smartcontracts/contracts/pair/src/asset_code_id_guard.rs) — both assets must be CW20; native → `NativeTokenNotSupported`. Write paths re-check pin + whitelist ([#582](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/582)). - Pair `ExecuteMsg::Swap` rejects native offer and tells CW20 users to `Send` + hook. Pair wasm has **no** `BankMsg` native in/out path. - Router execute is **CW20-hook only**. `NativeTokenNotSupported` in [`router/src/contract.rs`](smartcontracts/contracts/router/src/contract.rs) and [`blacklist_guard.rs`](smartcontracts/contracts/router/src/blacklist_guard.rs). Wrap-mapper maps **`uluna` ↔ cLUNC** and **`uusd` ↔ cUSTC` only** ([`NATIVE_TOKEN_WRAPPING.md`](NATIVE_TOKEN_WRAPPING.md)). **No cUSDC product.** ### Indexer / oracles / tokenlist - Route-solver nodes are CW20 contract addresses ([`docs/route-solver.md`](docs/route-solver.md)). Native `uluna`/`uusd` are not routable except via wrap CW20s. - [`indexer/src/indexer/asset_resolver.rs`](indexer/src/indexer/asset_resolver.rs) — retail labels only `uluna`→LUNC and `uusd`→USTC. Unknown natives including `ibc/…` insert as **raw denom** (**N630-5**). Native upsert hardcodes **6** decimals. - USD oracles: USTC / LUNC / CEX-FDUSD (`vfdusd`). **No USDC feed.** Do not treat vFDUSD, UST1, or `$1` as USDC ([#580](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/580), [#691](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/691) **U4**). - [`tokenlist/tokenlist.json`](tokenlist/tokenlist.json) — natives are LUNC/USTC only. Duplicate tickers fail CI ([#715](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/715) **TL-1**). ### Frontend - Create Pair **C542-2**: no native `uluna`/`uusd` (factory would reject). Catalog is bundled CW20s ([`skills/AGENTS_FRONTEND_CREATE_PAIR_PICKER.md`](skills/AGENTS_FRONTEND_CREATE_PAIR_PICKER.md)). - Swap query parser **Q711-3** / Create query: `ibc/` is **hostile** and ignored ([`swapQueryParams.ts`](frontend-dapp/src/utils/swapQueryParams.ts), [`createPairQuery.ts`](frontend-dapp/src/utils/createPairQuery.ts)). - Display **N630-5**: unknown `ibc/…` stays raw unless indexer has a symbol. Do not invent **USDC** for vFDUSD / UST1 / a second IBC hash. ### Upstream contrast Classic TerraSwap **does** support `AssetInfo::NativeToken` including `ibc/[A-F0-9]{64}` and typically **0 burn tax** on IBC bank sends (`is_ibc_token()` in [`docs/classic-terraswap-upstream-source.md`](docs/classic-terraswap-upstream-source.md)). That is **not** CL8Y behavior today. Classic USDC is **not one denom**. Axelar-bridged `uusdc` vs Noble native USDC are different `ibc/…` hashes. Phoenix-1 hashes must not be copied onto columbus-5. --- ## Why this is needed Retail and LPs cannot hold or trade IBC USDC **on CL8Y pools/books**. Wrap cannot paper over this: there is no CMM USDC wrap. Other-DEX TOKEN/USDC liquidity (#691) does not create a CL8Y USDC market, fee, or TVL. Allowlisting **one** well-identified USDC denom unblocks honest USDC pairs without opening every IBC hash. --- ## Constraints / guardrails | ID | Rule | |----|------| | **I1 — allowlist, not all IBC** | Factory stores an explicit native-denom allowlist (governance / 2-of-3, same authority as `AddWhitelistedCodeId`). Unlisted `ibc/…`, `uluna`, `uusd`, `factory/…`, and junk strings revert at `CreatePair` and at pair/router execute. | | **I2 — one USDC pin** | First listed IBC denom is **one** columbus-5 USDC hash (document channel, counterparty, decimals, LCD evidence). Axelar vs Noble are **not** fungible. A second hash = new issue or an explicit multi-pin table with no implicit 1:1. Do not copy phoenix-1 denoms. | | **I3 — no fake wrap** | Do not invent cUSDC or a wrap-mapper mapping. IBC in/out is **bank send** on the pair/router. Do not send IBC through treasury InstantWithdraw. `uluna`/`uusd` stay on the existing wrap path; this ticket does **not** list LUNC/USTC as native pair legs. | | **I4 — F6 mixed pairs** | CW20 leg: live `code_id` still must be whitelisted and pinned. Native IBC leg: **no** `code_id` pin (skip or sentinel that cannot be confused with a real id). `RefreshAssetCodeIds` / freeze still apply to the CW20 leg. TOKEN/USDC where TOKEN wasm is unlisted stays ineligible. | | **I5 — denom regex** | Accept only `^ibc/[A-F0-9]{64}$` (case-normalize to uppercase hex as Terra bank does). Reject lowercase mix, truncated hashes, `ibc/USDC`, CW20 `terra1` in the native slot. | | **I6 — burn tax** | Confirm columbus-5 tax on the pinned denom (expect 0). Quotes and execute must use the same tax. Fail closed if the querier disagrees. Never apply USTC 1.5% to an IBC send by denom mix-up. | | **I7 — oracle** | USD(USDC) is a **real** feed (CEX USDC or documented stable oracle), fail closed if down. **Not** `$1`, **not** vFDUSD, **not** UST1 peg. | | **I8 — ticker** | Retail **USDC** only for the pinned denom. Any other `ibc/…` stays raw (**N630-5**). Unique-symbol CI (**TL-1**): one `USDC` row. | | **I9 — query params** | Hostile `ibc/` ignore remains for **unlisted** hashes. The **pinned** denom (and later allowlisted denoms) may appear in Swap `from`/`to` and Create `a`/`b` **after** factory allowlist + tokenlist pin. Never echo raw hostile query into combobox/share. | | **I10 — not #691** | Do not add IBC USDC as a **foreign** Terraport/TerraSwap intermediate here. Do not relax #690 XDex-4. Do not `RegisterListedPair` on foreign pair addrs. | | **I11 — 2-of-3** | Adding/removing a native denom on columbus-5 is governance. Code can ship with empty allowlist + LocalTerra fixtures; mainnet pin is an ops/governance follow-through, not a hidden deploy in this MR. | | **I12 — pair-creation fee** | `#276` uluna fee still required. Native IBC does not skip the fee or F6 on the other leg. | --- ## Relevant files | Path | Why | |------|-----| | `smartcontracts/packages/dex-common/src/types.rs` | Native vs CW20; stop using `assert_is_token()` as the only gate | | `smartcontracts/packages/dex-common/src/factory.rs` | `CreatePair` + new add/remove/query native-denom msgs | | `smartcontracts/contracts/factory/src/contract.rs` + `state.rs` | Allowlist map; mixed-asset `CreatePair`; instantiate pair with native leg | | `smartcontracts/contracts/pair/src/contract.rs` | Provide / swap / withdraw / hybrid with one native IBC reserve (`BankMsg`) | | `smartcontracts/contracts/pair/src/asset_code_id_guard.rs` | Skip code_id for native IBC; keep CW20 pin | | `smartcontracts/contracts/pair/src/blacklist_guard.rs` | Native denom in blacklist checks | | `smartcontracts/contracts/router/src/contract.rs` + `msg.rs` | Native IBC hop in/out without wrap-mapper; funds on first hop | | `smartcontracts/contracts/router/src/blacklist_guard.rs` | Native offer/ask | | `smartcontracts/tests/` | Mixed-pair multitest: create, LP, swap both directions, unlisted denom | | `indexer/src/indexer/asset_resolver.rs` | Pin USDC labels + real decimals (not assume 6) | | `indexer` route-solver / pair ingest | Native IBC graph node for **factory** pairs | | `tokenlist/tokenlist.json` + logo | One USDC native row; **TL-1** | | `frontend-dapp/src/utils/createPairTokenCatalog.ts` | Allow **pinned** IBC native on Create (exception to C542-2) | | `frontend-dapp/src/utils/swapQueryParams.ts` + `createPairQuery.ts` | Allowlisted `ibc/HASH` is not hostile | | `frontend-dapp` Swap / Pool provide / Trade | Native pay/receive without wrap UI | | `docs/frontend.md` + `NATIVE_TOKEN_WRAPPING.md` | IBC native ≠ wrap; C542-2 / N630-5 / Q711-3 amendments | | `skills/AGENTS_FRONTEND_CREATE_PAIR_PICKER.md` | Catalog may include pinned IBC native | | `skills/AGENTS_FRONTEND_NATIVE_TICKERS.md` | USDC ticker only for pin | | `docs/runbooks/` or factory listing policy | Native-denom governance vs CW20 `code_id` | --- ## Recommended direction 1. **Pin** — LCD on columbus-5: trace the USDC denom that actually exists (IBC denom trace / bank metadata / any existing Terraport pairs). Record hash, channel, counterparty, decimals. Reuse #691 LCD tables **if they already exist**; do not block this ticket on #691 go/no-go for *foreign hops*. 2. **Factory allowlist** — `AddWhitelistedNativeDenom` / `RemoveWhitelistedNativeDenom` / `IsNativeDenomWhitelisted` (names TBD), governance-only. Regex-validate. Empty by default in tests unless the test inserts the pin. 3. **CreatePair** — accept exactly one of: - two listed CW20s (unchanged), or - one listed CW20 + one allowlisted IBC native. Reject two natives in v1 (no USDC/LUNC native-native pair; LUNC stays wrapped). Reject `uluna`/`uusd` as the native leg. 4. **Pair wasm** — reserves: CW20 balance + bank balance of the pinned denom. Provide: CW20 send/transfer + `info.funds` for IBC. Swap: existing CW20 hook when offer is CW20; `ExecuteMsg::Swap` **with funds** when offer is the pinned native. Output native → `BankMsg::Send`. Hybrid/book: native rung amounts are bank denoms; do not wrap mid-hop. 5. **F6** — pin array stays two slots: CW20 `code_id` + `0` (or a dedicated `None`) for the native slot. Guards that call `assert_is_token()` must branch. Unpinned native slot is not a freeze of the CW20 leg. 6. **Router** — allow `SwapOperation` native IBC offer/ask **only** if factory `Pair { asset_infos }` returns that pair (same pin-not-caller-addr rule as foreign hops, but this is **our** factory). First hop native: execute with attached coins, not CW20 hook. Last hop native: bank send to recipient; `unwrap_output` illegal. No wrap-mapper. 7. **Indexer** — ingest factory pairs with a native IBC asset; graph node key = denom string; label **USDC** only for the pin; decimals from LCD/tokenlist not hardcoded 6 if the pin differs. Solver can route CW20↔USDC on **our** pairs. USDC oracle ticker (#515-style), fail closed. 8. **Frontend / tokenlist** — add native USDC row. Create Pair catalog includes it (still no `uluna`/`uusd`). Swap picker shows **USDC**. Deep links: allow only the pinned hash (or a `USDC` symbol that resolves uniquely — **TL-1**). Provide/withdraw UI must attach bank funds. No wrap toggle for USDC. 9. **Docs/skills** — amend C542-2 (exception: allowlisted IBC only), Q711-3 (pinned hash not hostile), N630-5 (USDC ticker for pin only), wrapping doc (IBC ≠ treasury wrap). Do **not**: permissionless IBC listing; treating all stables as one hub; IBC transfer/bridge UI; CCTP mint; copying TerraSwap router unbounded native support for `uluna`. --- ## Acceptance criteria - [ ] **AC1.** Factory create of listed CW20 + pinned USDC succeeds; create with any other `ibc/…` or `uluna`/`uusd` native reverts. - [ ] **AC2.** Governance add/remove native denom is authorized the same way as code_id whitelist; unauthorized caller reverts. - [ ] **AC3.** Pair provide, withdraw, swap both directions, and (if enabled) limit/hybrid on the mixed pair work; CW20-only pairs unchanged. - [ ] **AC4.** Router 1-hop and multi-hop that include the USDC leg execute with balance-delta / `minimum_receive`; `unwrap_output` + USDC ask reverts or is ignored (pick one, test it). - [ ] **AC5.** F6: migrating the CW20 still freezes writes; native leg cannot be used to skip whitelist on the CW20. - [ ] **AC6.** Tokenlist + indexer + dApp show **USDC** for the pin only; other ibc hashes stay raw; **TL-1** unique symbol. - [ ] **AC7.** Create Pair picker offers the pinned USDC; still excludes LUNC/USTC natives. Hostile `ibc/` query ignored; pinned hash may prefill. - [ ] **AC8.** Swap/Trade market quote+execute for a factory USDC pair without wrap msgs. Gas inventory includes native-in execute. - [ ] **AC9.** Oracle: unpriced/down USDC feed does not emit `$1` USD TVL; fail closed for USD stats that need it. - [ ] **AC10.** Docs/skills + `make verify-issue-<id>`. Mainnet allowlist remains empty until governance; LocalTerra/tests inject the pin. --- ## Test plan (functional) | # | Path | Expect | |---|------|--------| | T1 | `CreatePair` CW20 + pinned USDC | Pair exists; query `asset_infos` mixed | | T2 | `CreatePair` CW20 + unlisted `ibc/…` | Revert | | T3 | `CreatePair` two natives / `uluna`+CW20 | Revert | | T4 | Provide mixed LP | Reserves: CW20 + bank USDC | | T5 | Swap CW20 → USDC | User bank balance of pin increases; min_return | | T6 | Swap USDC → CW20 (`Swap` + funds) | CW20 out; extra/wrong denom funds ignored or revert (document) | | T7 | Router 2-hop via USDC | Delta + `minimum_receive`; no wrap-mapper | | T8 | Remove denom from allowlist | New create reverts; existing pair write policy documented (prefer fail-closed swap if denom dropped) | | T9 | F6 migrate CW20 off whitelist | Writes revert; native reserve cannot bypass | | T10 | Indexer ingest mixed pair | Asset row symbol USDC, decimals from pin, graph edge | | T11 | Unknown `ibc/FOO` display | Raw denom, not USDC | | T12 | Create Pair UI: pick USDC + listed CW20 | Submit encodes `native_token`; still charges uluna fee | | T13 | Swap `?from=<pin>&to=<cw20>` | Applies; random `ibc/` still ignored | | T14 | Existing CW20-only swap / wrap LUNC | Unchanged | | T15 | Duplicate tokenlist symbol USDC | CI fail (**TL-1**) | Contracts: multitest with a fake IBC denom in the allowlist (need not be a live hash). Indexer: unit tests on `native_retail_meta` + pair ingest fixture. Frontend: Vitest catalog + query-parser tables; Playwright when LocalTerra can mint a native denom or bank-fund a test `ibc/…`. --- ## Test plan (attack, hack, abuse) Wasm / listing / wallet surface — required. | Vector | Expect | |--------|--------| | **Denom spoof** — Axelar hash vs Noble hash vs truncated `ibc/` | Only the allowlisted pin creates or swaps | | **CW20 named USDC** | Still F6 `code_id`; not the native pin; no ticker collision in tokenlist (**TL-1**) | | **Caller pair addr** not factory `Pair` | Router re-queries factory; mismatch reverts | | **Wrong coin in `info.funds`** | Credit only the declared offer denom; extras do not mint LP or fill `minimum_receive` | | **Donation** of a second ibc denom to the pair | Ignore; reserves/delta only on declared assets | | **`$1` peg / vFDUSD as USDC** | Oracle fail-closed; forbidden | | **Skip F6** by putting junk wasm opposite USDC | Create still requires CW20 whitelist | | **Permissionless listing** of arbitrary IBC | No path except governance add | | **Wrap-mapper unwrap** on USDC ask | Revert / ignore; user must not pay InstantWithdraw | | **Burn-tax mix-up** (treat USDC as `uusd`) | Quotes would be wrong; tests prove 0 tax path for pin vs 1.5% USTC wrap path | | **Query XSS** `ibc/<script>` | Still hostile; never echo | | **Blacklist bypass** via native hop | Native denom included in blacklist checks | | **Reentrancy** on `BankMsg` then CW20 hook | Existing `SwapInProgress` / no writable re-entry | | **Phoenix-1 denom** in tokenlist | Reject unless columbus-5 LCD matches | --- ## Verification criteria Close when: 1. `make verify-issue-<iid>` covers factory allowlist + mixed CreatePair + pair swap both ways + router no-wrap + indexer labels/decimals + frontend catalog/parser tests. 2. LocalTerra (or multitest with bank denom) executes T1–T7 and T14. Unauthorized denom add fails. 3. Docs: C542-2 exception, Q711-3 allowlist, N630-5 USDC pin, wrapping doc states IBC ≠ treasury wrap. 4. Columbus-5 pin is **written on the issue** (hash, channel, decimals) before any mainnet governance add. Empty on-chain allowlist is OK for the merge. **Blocked on:** a verified columbus-5 USDC denom (LCD). Code may land with a test-only pin. Mainnet `AddWhitelistedNativeDenom` is governance, not this MR's hidden side effect. --- ## Out of scope - #691 / #690 other-DEX USDC intermediate and 300 bps foreign skim. - CCTP / Circle mint ([#1215](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1215)). - IBC transfer UI, packet timeout handling, relayer ops. - cUSDC wrap in ustr-cmm. - Native `uluna`/`uusd` CL8Y pair legs (keep wrap). - Second IBC ticker (ATOM, axlUSDC-as-second-hash, etc.) after USDC. - Permissionless IBC listing. - Treating UST1 / vFDUSD / USTC as USDC. --- ## First-pass model recommendation Recommendation: grok-high Rationale: This breaks the CW20-only invariant across **factory, pair, router, dex-common, indexer, tokenlist, and Swap/Create frontend** (far more than three production files, multiple subsystems). It is founder-required **wasm** plus **2-of-3 listing** of a bank denom, with burn-tax, F6 mixed pins, and oracle/ticker identity risk. There is no existing local pattern for native IBC on CL8Y pairs (wrap-mapper is LUNC/USTC only). Verification needs contract multitest, indexer ingest, and dApp fund-attachment paths — not a single-file deterministic edit. Composer criteria fail on file/subsystem count, protocol change, and contracts/listing surface.
Author
Owner

cl8y-agent-control: queued implement job 09e159ce-5239-4229-99a6-55bc31ceb0cf (not executed; no Hetzner VM).

cl8y-agent-control: queued `implement` job `09e159ce-5239-4229-99a6-55bc31ceb0cf` (not executed; no Hetzner VM).
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#1220
No description provided.