feat: Create Pair picker for listed CW20s (token list + custom paste) #542

Closed
opened 2026-08-17 03:45:48 +00:00 by PlasticDigits · 24 comments
PlasticDigits commented 2026-08-17 03:45:48 +00:00 (Migrated from gitlab.com)

Summary

/create (CreatePairPage) only accepts two raw terra1… contract fields. Operators and retail users who want a market for a listed CW20 (cLUNC, cUSTC, CL8Y, USTR, UST1, vFDUSD, ALPHA, …) must copy-paste long addresses by hand.

Ship a listed-CW20 picker on Create Pair, sourced from the repo token list / dApp registry (not Swap’s factory graph), while keeping a custom-address paste path for tokens that are not listed yet. Factory CreatePair stays the security boundary.

Related: #382 (bech32 checksum), #376 (code-ID whitelist ≠ token safety), #378 (logo host allowlist), #481 (Swap TokenSearchSelect — wrong universe for this page), #508 (UST1 AMM ≠ oracle mint), #518 (digit LP ticker), #534 (economic-first catalog).

Current codebase

Layer Behavior today
/create UI Two labeled text inputs (Token A/B Contract Address), placeholder="terra1...". Submit enabled only when both pass getTerraAddressInputError and tokenA !== tokenB (#382). Wallet disconnected → CTA Connect Wallet To Create.
Code-ID check useCodeIdCheck queries factory get_whitelisted_code_ids + LCD getChainContractInfo. Non-whitelisted → amber warning + “transaction will likely fail”. Does not disable submit. Query disabled until the field looks like terra1 and length > 5.
Factory CreatePair Permissionless but CW20-only (NativeTokenNotSupported). Both code IDs must be on WHITELISTED_CODE_IDS. Pair must not exist. Decimals ≤ MAX_PAIR_ASSET_DECIMALS_BOOTSTRAP (18). One create per block. Uluna pair-creation fee when pair_creation_fee_uluna > 0 (#276).
Tx builder createPair(wallet, tokenA, tokenB) → tokenAssetInfo (terra1… → CW20; anything else → native denom). A picker that emits uluna / uusd would submit a tx the factory rejects.
Repo token list tokenlist/tokenlist.json + tokenlist/README.md. Community MR process. CW20 entries have address, symbol, name, decimals, logoURI. Natives LUNC/USTC have denom only. Not consumed by Create Pair.
dApp registry tokenRegistry.ts is a display map (logos / symbols). CW20_MAP has hardcoded columbus-5 addresses for CL8Y, USTR, ALPHA, USTRIX, SpaceUSD, plus env overlays for cLUNC / cUSTC / UST1 / vFDUSD. TOKENS[] entries themselves have no address field.
Swap / Mint pickers Swap TokenSearchSelect is gated to getAllTokens(pairs) (already-listed factory markets). Mint TokenSelect is the faucet set. Neither is the create-pair universe: Swap omits tokens that do not yet have a pair; Mint is gems only.
LocalTerra Deploy writes VITE_LUNC_C_TOKEN_ADDRESS, VITE_USTC_C_TOKEN_ADDRESS, VITE_UST1_TOKEN_ADDRESS, VITE_VFDUSD_TOKEN_ADDRESS, VITE_CL8Y_TOKEN_ADDRESS, VITE_TOKEN_*_ADDRESS (EMBER…). Mainnet addresses in tokenlist.json are wrong on LocalTerra unless env overlays win.
Copy Subtitle: “Add a new CW20 market once both token contracts are verified and approved.” UST1 notice (#508): AMM ≠ /ust1 mint/redeem.
Known drift tokenlist.json lists CL8Y decimals: 6; tokenRegistry / on-chain CL8Y is 18 (#476). Create Pair must not trust tokenlist decimals for any on-chain amount.

Why this is needed

  1. Address paste is the wrong primary UX for tokens we already publish. Creating cLUNC/UST1, UST1/USTR, or CL8Y/cUSTC should be a symbol pick, not a 64-character transcription.
  2. The page already claims “verified and approved” but offers no list. Users invent addresses or paste from unofficial sources.
  3. Swap’s picker cannot be reused as-is. Its universe is factory-routable tokens (pairs that already exist). Create Pair is for tokens that often do not have a pair yet.
  4. Ops / LocalTerra still need a custom paste for faucet gems and unlisted CW20s. Picker + paste is one feature, not two issues.

Constraints / guardrails

  1. CW20 only in the picker. Never offer native uluna / uusd / LUNC / USTC. Factory rejects natives; tokenAssetInfo would encode them as native_token.
  2. Token list is convenience, not a security boundary. Factory code-ID whitelist + on-chain TokenInfo remain authoritative (#376, docs/runbooks/cw20-code-id-ops.md). Do not skip or weaken useCodeIdCheck. Do not copy that implies a listed ticker is “safe” or audited.
  3. Keep custom paste. Unlisted CW20s (new listings, LocalTerra gems, governance-whitelisted templates not yet in tokenlist.json) must still be enterable. Paste still runs #382 format + checksum gates before submit.
  4. Do not fetch a remote token list at runtime (GitLab raw, CoinGecko, indexer GET /api/v1/tokens). Bundle / import the repo list (or a generated TS module from it). Remote fetch is a supply-chain and availability risk and would violate Swap’s “no arbitrary external list” rule if copied there.
  5. Do not feed this catalog into Swap / Trade token pickers. Swap stays factory-gated (getAllTokens(pairs) + wrap enrichment) — skills/AGENTS_FRONTEND_TOKEN_SEARCH.md.
  6. LocalTerra / env overlay wins. When VITE_* wrap / UST1 / CL8Y / faucet token addresses are set, the picker must use those addresses, not columbus-5 tokenlist.json values. Empty env → mainnet published address (same pattern as CW20_MAP).
  7. Picker onChange only emits addresses from the gated CW20 catalog (or the validated paste field). Search tricks must not select the other leg (excludeToken). Same-address A/B stays a hard disable.
  8. Logos go through resolveTrustedTokenLogoUrl / resolveAllowedTokenLogoUri (#378). Symbols/names render as text only (no innerHTML). Search input truncated (TOKEN_SEARCH_MAX_QUERY_LENGTH = 128).
  9. A11y — useId() + htmlFor / aria-label on both legs (#143). Combobox keyboard parity with Swap (#350 / #481) if a search control is used.
  10. #489 copy. Labels short (Token A, Token B, or Select token). No “this is not an AMM essay”. Keep the existing UST1 one-liner (#508 U1). Do not add a permanent “token list verified by governance” banner.
  11. Do not change factory CreatePair, whitelist, pair-creation fee, or LP ticker rules (#518).
  12. Do not use tokenlist decimals for any amount / fee math. Create Pair does not send amounts; do not start.
  13. Clickwrap / NFA / pause unchanged (#517, #138, #120).
  14. LocalTerra for any on-chain create path. Do not close with a false “no LocalTerra”.

Relevant files

File Role
frontend-dapp/src/pages/CreatePairPage.tsx Two raw address fields + whitelist warnings + submit
frontend-dapp/src/pages/CreatePairPage.test.tsx #382 checksum + #508 notice
frontend-dapp/src/services/terraclassic/factory.ts createPair, getWhitelistedCodeIds
frontend-dapp/src/utils/terraAddressValidation.ts Format + bech32 checksum
frontend-dapp/src/utils/tokenRegistry.ts Display registry + CW20_MAP
frontend-dapp/src/utils/constants.ts VITE_* wrap / UST1 / faucet gem addresses
tokenlist/tokenlist.json Published CW20 + native catalog
tokenlist/README.md Community listing process
frontend-dapp/src/components/trade/TokenSearchSelect.tsx Reuse control, not Swap token universe
frontend-dapp/src/components/ui/TokenSelect.tsx Button listbox (Mint) — acceptable if the listed set stays small
frontend-dapp/src/utils/tokenSearchQuery.ts Debounce / min-chars / 128-char cap
frontend-dapp/src/utils/tokenLogoAllowlist.ts Logo host allowlist
frontend-dapp/src/types/index.ts tokenAssetInfo (native vs CW20)
frontend-dapp/src/utils/ust1SecondaryMarket.ts UST1 create-pair notice
smartcontracts/contracts/factory/src/contract.rs execute_create_pair (CW20 + whitelist) — read-only for this issue
docs/frontend.md § Create pair Address-validation invariants to extend
docs/security-model.md § Code ID whitelist On-chain gate (unchanged)

Frontend-only. One catalog helper + Create Pair UI. No factory / indexer API change.

Catalog (new helper, e.g. createPairTokenCatalog.ts)

Single function getCreatePairCw20Options():

  1. Start from repo CW20 rows (tokenlist.json bundled, or tokenRegistry CW20_MAP + TOKENS — pick one source of truth and generate/test the other so they cannot drift on address).
  2. Overlay VITE_LUNC_C_* / VITE_USTC_C_* / VITE_UST1_* / VITE_VFDUSD_* / VITE_CL8Y_* when set.
  3. Optionally append SOFT_LAUNCH_MINTABLE_TOKENS when those env addresses are present (LocalTerra gems).
  4. Drop natives. Drop entries with empty / invalid bech32 addresses.
  5. Dedupe by lowercase address. Sort economic-first then symbol (#534 P534-7 style) — gems last.

Do not import getAllTokens(pairs).

UI

Each leg: searchable listed-token control (reuse TokenSearchSelect with the catalog ids, or TokenSelect if the list stays ~15) plus a progressive-disclosure Custom contract text field (closed by default when a listed token is selected).

  • Selecting a listed token fills the address used on submit and shows symbol + allowlisted logo.
  • Opening Custom and pasting a valid terra1… is the unlisted path; picker may show “Custom” / raw address.
  • excludeToken omits the other leg’s address from options.
  • Existing whitelist / checksum / fee / UST1 notice rows stay.

Docs

Extend docs/frontend.md § Create pair with catalog invariants. Short agent playbook only if the helper is non-obvious. Do not rewrite tokenlist/README.md listing process except a one-line “Create Pair picker reads this list (CW20 only)”.

Acceptance criteria

  • C1. /create can select Token A and Token B from listed CW20s (at least cLUNC, cUSTC, CL8Y, USTR, UST1, vFDUSD when addresses resolve) without pasting.
  • C2. Native LUNC / USTC / uluna / uusd never appear as selectable create-pair tokens.
  • C3. Custom paste path remains: valid checksummed terra1… still enables Create Pair; #382 format/checksum errors still disable submit.
  • C4. Selecting the same listed token (or same address via paste) on both legs disables submit with the existing “must be different” copy.
  • C5. LocalTerra / env overlays: when wrap/UST1/CL8Y env addresses are set, picker submits those addresses, not mainnet tokenlist.json values.
  • C6. Code-ID whitelist warning still runs for both listed and pasted addresses; listed ≠ skip check.
  • C7. Submit still calls existing createPair with two CW20 AssetInfos. No native encoding. No factory/indexer API change.
  • C8. Swap / Mint / Trade pickers unchanged (factory / faucet universes).
  • C9. Logos allowlisted; symbols/names text-only; search query capped.
  • C10. UST1 AMM ≠ oracle notice remains (#508).
  • C11. docs/frontend.md § Create pair documents the catalog + paste dual path. make verify-issue-<iid> covers unit + (when stack is up) Playwright smoke.

Test plan (all paths)

Unit / RTL (no chain)

ID Path Expect
T1 Catalog includes published CW20s, excludes natives Addresses present; no uluna/uusd
T2 Env overlay replaces mainnet address VITE_LUNC_C_TOKEN_ADDRESS=terra1local… → catalog uses local
T3 Empty env → published mainnet address for wrap/UST1/CL8Y Same as today’s CW20_MAP fallbacks
T4 Soft-launch gems appear only when VITE_TOKEN_* set Absent on mainnet-like env
T5 Invalid / empty address rows dropped No empty option, no bad bech32 in catalog
T6 Dedupe lowercase One row per contract
T7 Pick listed A + listed B (distinct) Submit enabled when wallet connected; createPair called with those addrs
T8 Pick same token both legs Submit disabled + existing error copy
T9 Paste valid unlisted CW20 Submit enabled; checksum OK
T10 Paste #382 bad checksum / bad format Inline error; submit disabled (regression)
T11 Paste native-looking uluna into custom field Rejected (not a Terra address); cannot submit native
T12 excludeToken Other leg’s address omitted; search cannot select it
T13 Code-ID not whitelisted (listed or paste) Amber warning; submit still enabled (today’s behavior)
T14 Disconnected wallet CTA Connect; no tx
T15 #508 UST1 notice still rendered Copy unchanged
T16 XSS-ish symbol/name in catalog fixture Rendered as text; no HTML execution
T17 Search paste > 128 chars Truncated
T18 Swap TokenSearchSelect tests still pass Factory universe unchanged

Playwright / LocalTerra (when make has-localterra)

ID Path Expect
P1 Open /create, pick two LocalTerra listed/env CW20s that have no pair yet Addresses match .env.local; Create Pair enabled
P2 Custom paste of a LocalTerra gem CW20 Same as today’s paste path
P3 Pick two tokens that already have a pair Wallet/tx error surfaces; no false success
P4 Keyboard: open list, Arrow/Enter, Escape Combobox/#350 parity if search control used
P5 Phone width (~390px) No CLS / clipped list (reuse #498 rules if combobox)

On-chain create (optional, one-per-block + fee): only if the pair does not exist and fee uluna is available. Do not spam CreatePair in CI on a shared LocalTerra.

Test plan — attack, hack, and abuse

ID Vector Expect
A1 Look-alike ticker in a future tokenlist MR (CL8Y vs CL8Y / U5T1) Catalog is review-gated via MR, not runtime user input. UI must not say “verified safe”. Code-ID check still runs.
A2 Malicious logoURI (javascript:, http://, non-allowlisted host) resolveAllowedTokenLogoUri drops it; blockie fallback; no script load
A3 XSS in symbol / name (<img onerror>) Text nodes only
A4 Paste attacker CW20 (fee-on-transfer, fake TokenInfo) Paste allowed (permissionless factory). Warning if code ID not whitelisted. Factory still rejects non-whitelisted code IDs. Do not add a client-only “block all unlisted” gate that pretends to be security.
A5 Hijack Swap universe (PR “reuse getAllTokens”) Rejected by C8 / unit: Create Pair catalog test must not import router pair graph
A6 Remote list swap (fetch GitLab raw / indexer tokens, attacker MITM or poisoned CDN) No runtime HTTP for the catalog. Bundle only.
A7 Native smuggling (select LUNC, or paste uluna) Cannot encode native_token on this page
A8 Same-token both legs via picker + paste mix (listed CL8Y + pasted same addr different case) Compare case-insensitive; submit disabled
A9 Search injection / DoS (huge paste, regex-ish query) 128-char cap; client filter only; no indexer ILIKE
A10 onChange with id not in options (tampered handler / extra option) Ignore; submit uses only validated addresses
A11 Homograph / wrong-prefix address (terra1 checksum fail, terravaloper, other HRP) #382 errors; no submit
A12 Clickjack / prefill via query string (/create?a=&b=) Out of scope unless implemented. If deep-link is added, still require checksum + user-visible symbols before enable. Default: no query prefill in this issue (open-redirect / phishing aid).
A13 Social-engineering copy (“Official verified token — safe to create”) Forbidden. Listed = in our published catalog only.
A14 LocalTerra mainnet-address foot-gun Env overlay required (C5). Test that picker does not offer columbus-5 cLUNC when VITE_LUNC_C_TOKEN_ADDRESS is the local wrap token.

Verification criteria

Close only when all of the following are true:

  1. make test-frontend (or scoped Vitest for catalog + CreatePairPage + existing #382/#508 cases) passes.
  2. make verify-issue-<iid> exists and covers T1–T18 (and P1–P5 when LocalTerra is up — provision with make setup-cloud-localterra / make has-localterra, do not SKIP).
  3. Manual: /create on LocalTerra — pick two env CW20s; confirm filled addresses match .env.local; paste path still works; natives absent; Swap pay/receive list unchanged.
  4. docs/frontend.md § Create pair updated; no factory/indexer behavior change in the diff.
  5. QA sign-off on C1–C11 and A1–A14 (table in the MR).
## Summary `/create` (`CreatePairPage`) only accepts two raw `terra1…` contract fields. Operators and retail users who want a market for a **listed** CW20 (cLUNC, cUSTC, CL8Y, USTR, UST1, vFDUSD, ALPHA, …) must copy-paste long addresses by hand. Ship a **listed-CW20 picker** on Create Pair, sourced from the repo token list / dApp registry (not Swap’s factory graph), while **keeping a custom-address paste path** for tokens that are not listed yet. Factory `CreatePair` stays the security boundary. Related: [#382](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/382) (bech32 checksum), [#376](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/376) (code-ID whitelist ≠ token safety), [#378](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/378) (logo host allowlist), [#481](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/481) (Swap `TokenSearchSelect` — **wrong universe** for this page), [#508](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/508) (UST1 AMM ≠ oracle mint), [#518](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/518) (digit LP ticker), [#534](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/534) (economic-first catalog). ## Current codebase | Layer | Behavior today | |-------|----------------| | **`/create` UI** | Two labeled text inputs (`Token A/B Contract Address`), `placeholder="terra1..."`. Submit enabled only when both pass `getTerraAddressInputError` and `tokenA !== tokenB` ([#382](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/382)). Wallet disconnected → CTA **Connect Wallet To Create**. | | **Code-ID check** | `useCodeIdCheck` queries factory `get_whitelisted_code_ids` + LCD `getChainContractInfo`. Non-whitelisted → amber warning + “transaction will likely fail”. **Does not** disable submit. Query disabled until the field looks like `terra1` and length `> 5`. | | **Factory `CreatePair`** | Permissionless but **CW20-only** (`NativeTokenNotSupported`). Both code IDs must be on `WHITELISTED_CODE_IDS`. Pair must not exist. Decimals ≤ `MAX_PAIR_ASSET_DECIMALS_BOOTSTRAP` (18). One create per block. Uluna pair-creation fee when `pair_creation_fee_uluna > 0` ([#276](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/276)). | | **Tx builder** | `createPair(wallet, tokenA, tokenB)` → `tokenAssetInfo` (`terra1…` → CW20; anything else → **native denom**). A picker that emits `uluna` / `uusd` would submit a tx the factory rejects. | | **Repo token list** | [`tokenlist/tokenlist.json`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/tokenlist/tokenlist.json) + [`tokenlist/README.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/tokenlist/README.md). Community MR process. CW20 entries have `address`, `symbol`, `name`, `decimals`, `logoURI`. Natives LUNC/USTC have `denom` only. **Not consumed by Create Pair.** | | **dApp registry** | [`tokenRegistry.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/tokenRegistry.ts) is a **display** map (logos / symbols). `CW20_MAP` has hardcoded columbus-5 addresses for CL8Y, USTR, ALPHA, USTRIX, SpaceUSD, plus env overlays for cLUNC / cUSTC / UST1 / vFDUSD. `TOKENS[]` entries themselves have **no** `address` field. | | **Swap / Mint pickers** | Swap `TokenSearchSelect` is gated to `getAllTokens(pairs)` (already-listed factory markets). Mint `TokenSelect` is the faucet set. **Neither** is the create-pair universe: Swap omits tokens that do not yet have a pair; Mint is gems only. | | **LocalTerra** | Deploy writes `VITE_LUNC_C_TOKEN_ADDRESS`, `VITE_USTC_C_TOKEN_ADDRESS`, `VITE_UST1_TOKEN_ADDRESS`, `VITE_VFDUSD_TOKEN_ADDRESS`, `VITE_CL8Y_TOKEN_ADDRESS`, `VITE_TOKEN_*_ADDRESS` (EMBER…). Mainnet addresses in `tokenlist.json` are **wrong** on LocalTerra unless env overlays win. | | **Copy** | Subtitle: “Add a new CW20 market once both token contracts are verified and approved.” UST1 notice ([#508](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/508)): AMM ≠ `/ust1` mint/redeem. | | **Known drift** | `tokenlist.json` lists CL8Y `decimals: 6`; `tokenRegistry` / on-chain CL8Y is **18** ([#476](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/476)). Create Pair must not trust tokenlist decimals for any on-chain amount. | ## Why this is needed 1. **Address paste is the wrong primary UX** for tokens we already publish. Creating cLUNC/UST1, UST1/USTR, or CL8Y/cUSTC should be a symbol pick, not a 64-character transcription. 2. **The page already claims “verified and approved”** but offers no list. Users invent addresses or paste from unofficial sources. 3. **Swap’s picker cannot be reused as-is.** Its universe is factory-routable tokens (pairs that already exist). Create Pair is for tokens that often **do not** have a pair yet. 4. **Ops / LocalTerra** still need a custom paste for faucet gems and unlisted CW20s. Picker + paste is one feature, not two issues. ## Constraints / guardrails 1. **CW20 only in the picker.** Never offer native `uluna` / `uusd` / LUNC / USTC. Factory rejects natives; `tokenAssetInfo` would encode them as `native_token`. 2. **Token list is convenience, not a security boundary.** Factory code-ID whitelist + on-chain `TokenInfo` remain authoritative ([#376](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/376), [`docs/runbooks/cw20-code-id-ops.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/runbooks/cw20-code-id-ops.md)). Do **not** skip or weaken `useCodeIdCheck`. Do **not** copy that implies a listed ticker is “safe” or audited. 3. **Keep custom paste.** Unlisted CW20s (new listings, LocalTerra gems, governance-whitelisted templates not yet in `tokenlist.json`) must still be enterable. Paste still runs [#382](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/382) format + checksum gates before submit. 4. **Do not fetch a remote token list at runtime** (GitLab raw, CoinGecko, indexer `GET /api/v1/tokens`). Bundle / import the repo list (or a generated TS module from it). Remote fetch is a supply-chain and availability risk and would violate Swap’s “no arbitrary external list” rule if copied there. 5. **Do not feed this catalog into Swap / Trade token pickers.** Swap stays factory-gated (`getAllTokens(pairs)` + wrap enrichment) — [`skills/AGENTS_FRONTEND_TOKEN_SEARCH.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/skills/AGENTS_FRONTEND_TOKEN_SEARCH.md). 6. **LocalTerra / env overlay wins.** When `VITE_*` wrap / UST1 / CL8Y / faucet token addresses are set, the picker must use those addresses, not columbus-5 `tokenlist.json` values. Empty env → mainnet published address (same pattern as `CW20_MAP`). 7. **Picker `onChange` only emits addresses from the gated CW20 catalog** (or the validated paste field). Search tricks must not select the other leg (`excludeToken`). Same-address A/B stays a hard disable. 8. **Logos** go through `resolveTrustedTokenLogoUrl` / `resolveAllowedTokenLogoUri` ([#378](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/378)). Symbols/names render as **text only** (no `innerHTML`). Search input truncated (`TOKEN_SEARCH_MAX_QUERY_LENGTH` = 128). 9. **A11y** — `useId()` + `htmlFor` / `aria-label` on both legs ([#143](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/143)). Combobox keyboard parity with Swap (#350 / #481) if a search control is used. 10. **#489 copy.** Labels short (`Token A`, `Token B`, or `Select token`). No “this is not an AMM essay”. Keep the existing UST1 one-liner ([#508](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/508) **U1**). Do not add a permanent “token list verified by governance” banner. 11. **Do not change** factory `CreatePair`, whitelist, pair-creation fee, or LP ticker rules ([#518](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/518)). 12. **Do not use tokenlist `decimals`** for any amount / fee math. Create Pair does not send amounts; do not start. 13. **Clickwrap / NFA / pause** unchanged ([#517](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/517), [#138](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/138), [#120](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/120)). 14. **LocalTerra** for any on-chain create path. Do not close with a false “no LocalTerra”. ## Relevant files | File | Role | |------|------| | [`frontend-dapp/src/pages/CreatePairPage.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/CreatePairPage.tsx) | Two raw address fields + whitelist warnings + submit | | [`frontend-dapp/src/pages/CreatePairPage.test.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/CreatePairPage.test.tsx) | #382 checksum + #508 notice | | [`frontend-dapp/src/services/terraclassic/factory.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/factory.ts) | `createPair`, `getWhitelistedCodeIds` | | [`frontend-dapp/src/utils/terraAddressValidation.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/terraAddressValidation.ts) | Format + bech32 checksum | | [`frontend-dapp/src/utils/tokenRegistry.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/tokenRegistry.ts) | Display registry + `CW20_MAP` | | [`frontend-dapp/src/utils/constants.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/constants.ts) | `VITE_*` wrap / UST1 / faucet gem addresses | | [`tokenlist/tokenlist.json`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/tokenlist/tokenlist.json) | Published CW20 + native catalog | | [`tokenlist/README.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/tokenlist/README.md) | Community listing process | | [`frontend-dapp/src/components/trade/TokenSearchSelect.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/trade/TokenSearchSelect.tsx) | Reuse **control**, not Swap token universe | | [`frontend-dapp/src/components/ui/TokenSelect.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/ui/TokenSelect.tsx) | Button listbox (Mint) — acceptable if the listed set stays small | | [`frontend-dapp/src/utils/tokenSearchQuery.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/tokenSearchQuery.ts) | Debounce / min-chars / 128-char cap | | [`frontend-dapp/src/utils/tokenLogoAllowlist.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/tokenLogoAllowlist.ts) | Logo host allowlist | | [`frontend-dapp/src/types/index.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/types/index.ts) | `tokenAssetInfo` (native vs CW20) | | [`frontend-dapp/src/utils/ust1SecondaryMarket.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/ust1SecondaryMarket.ts) | UST1 create-pair notice | | [`smartcontracts/contracts/factory/src/contract.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/smartcontracts/contracts/factory/src/contract.rs) | `execute_create_pair` (CW20 + whitelist) — **read-only for this issue** | | [`docs/frontend.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/frontend.md) § Create pair | Address-validation invariants to extend | | [`docs/security-model.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/security-model.md) § Code ID whitelist | On-chain gate (unchanged) | ## Recommended direction **Frontend-only.** One catalog helper + Create Pair UI. No factory / indexer API change. ### Catalog (new helper, e.g. `createPairTokenCatalog.ts`) Single function `getCreatePairCw20Options()`: 1. Start from repo CW20 rows (`tokenlist.json` bundled, **or** `tokenRegistry` `CW20_MAP` + `TOKENS` — pick one source of truth and generate/test the other so they cannot drift on **address**). 2. Overlay `VITE_LUNC_C_*` / `VITE_USTC_C_*` / `VITE_UST1_*` / `VITE_VFDUSD_*` / `VITE_CL8Y_*` when set. 3. Optionally append `SOFT_LAUNCH_MINTABLE_TOKENS` when those env addresses are present (LocalTerra gems). 4. Drop natives. Drop entries with empty / invalid bech32 addresses. 5. Dedupe by lowercase address. Sort economic-first then symbol ([#534](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/534) **P534-7** style) — gems last. Do **not** import `getAllTokens(pairs)`. ### UI Each leg: searchable listed-token control (reuse `TokenSearchSelect` **with the catalog ids**, or `TokenSelect` if the list stays ~15) **plus** a progressive-disclosure **Custom contract** text field (closed by default when a listed token is selected). - Selecting a listed token fills the address used on submit and shows symbol + allowlisted logo. - Opening Custom and pasting a valid `terra1…` is the unlisted path; picker may show “Custom” / raw address. - `excludeToken` omits the other leg’s address from options. - Existing whitelist / checksum / fee / UST1 notice rows stay. ### Docs Extend [`docs/frontend.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/frontend.md) § Create pair with catalog invariants. Short agent playbook only if the helper is non-obvious. Do not rewrite `tokenlist/README.md` listing process except a one-line “Create Pair picker reads this list (CW20 only)”. ## Acceptance criteria - [ ] **C1.** `/create` can select Token A and Token B from listed CW20s (at least cLUNC, cUSTC, CL8Y, USTR, UST1, vFDUSD when addresses resolve) without pasting. - [ ] **C2.** Native LUNC / USTC / `uluna` / `uusd` never appear as selectable create-pair tokens. - [ ] **C3.** Custom paste path remains: valid checksummed `terra1…` still enables Create Pair; #382 format/checksum errors still disable submit. - [ ] **C4.** Selecting the same listed token (or same address via paste) on both legs disables submit with the existing “must be different” copy. - [ ] **C5.** LocalTerra / env overlays: when wrap/UST1/CL8Y env addresses are set, picker submits **those** addresses, not mainnet `tokenlist.json` values. - [ ] **C6.** Code-ID whitelist warning still runs for both listed and pasted addresses; listed ≠ skip check. - [ ] **C7.** Submit still calls existing `createPair` with two CW20 `AssetInfo`s. No native encoding. No factory/indexer API change. - [ ] **C8.** Swap / Mint / Trade pickers unchanged (factory / faucet universes). - [ ] **C9.** Logos allowlisted; symbols/names text-only; search query capped. - [ ] **C10.** UST1 AMM ≠ oracle notice remains ([#508](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/508)). - [ ] **C11.** `docs/frontend.md` § Create pair documents the catalog + paste dual path. `make verify-issue-<iid>` covers unit + (when stack is up) Playwright smoke. ## Test plan (all paths) ### Unit / RTL (no chain) | ID | Path | Expect | |----|------|--------| | T1 | Catalog includes published CW20s, excludes natives | Addresses present; no `uluna`/`uusd` | | T2 | Env overlay replaces mainnet address | `VITE_LUNC_C_TOKEN_ADDRESS=terra1local…` → catalog uses local | | T3 | Empty env → published mainnet address for wrap/UST1/CL8Y | Same as today’s `CW20_MAP` fallbacks | | T4 | Soft-launch gems appear only when `VITE_TOKEN_*` set | Absent on mainnet-like env | | T5 | Invalid / empty address rows dropped | No empty option, no bad bech32 in catalog | | T6 | Dedupe lowercase | One row per contract | | T7 | Pick listed A + listed B (distinct) | Submit enabled when wallet connected; `createPair` called with those addrs | | T8 | Pick same token both legs | Submit disabled + existing error copy | | T9 | Paste valid unlisted CW20 | Submit enabled; checksum OK | | T10 | Paste #382 bad checksum / bad format | Inline error; submit disabled (regression) | | T11 | Paste native-looking `uluna` into custom field | Rejected (not a Terra address); cannot submit native | | T12 | `excludeToken` | Other leg’s address omitted; search cannot select it | | T13 | Code-ID not whitelisted (listed or paste) | Amber warning; submit still enabled (today’s behavior) | | T14 | Disconnected wallet | CTA Connect; no tx | | T15 | #508 UST1 notice still rendered | Copy unchanged | | T16 | XSS-ish symbol/name in catalog fixture | Rendered as text; no HTML execution | | T17 | Search paste > 128 chars | Truncated | | T18 | Swap `TokenSearchSelect` tests still pass | Factory universe unchanged | ### Playwright / LocalTerra (when `make has-localterra`) | ID | Path | Expect | |----|------|--------| | P1 | Open `/create`, pick two LocalTerra listed/env CW20s that have **no** pair yet | Addresses match `.env.local`; Create Pair enabled | | P2 | Custom paste of a LocalTerra gem CW20 | Same as today’s paste path | | P3 | Pick two tokens that already have a pair | Wallet/tx error surfaces; no false success | | P4 | Keyboard: open list, Arrow/Enter, Escape | Combobox/#350 parity if search control used | | P5 | Phone width (~390px) | No CLS / clipped list (reuse #498 rules if combobox) | On-chain create (optional, one-per-block + fee): only if the pair does not exist and fee uluna is available. Do not spam `CreatePair` in CI on a shared LocalTerra. ## Test plan — attack, hack, and abuse | ID | Vector | Expect | |----|--------|--------| | A1 | **Look-alike ticker** in a future tokenlist MR (`CL8Y` vs `CL8Y ` / `U5T1`) | Catalog is review-gated via MR, not runtime user input. UI must not say “verified safe”. Code-ID check still runs. | | A2 | **Malicious `logoURI`** (`javascript:`, `http://`, non-allowlisted host) | `resolveAllowedTokenLogoUri` drops it; blockie fallback; no script load | | A3 | **XSS in `symbol` / `name`** (`<img onerror>`) | Text nodes only | | A4 | **Paste attacker CW20** (fee-on-transfer, fake `TokenInfo`) | Paste allowed (permissionless factory). Warning if code ID not whitelisted. Factory still rejects non-whitelisted code IDs. Do not add a client-only “block all unlisted” gate that pretends to be security. | | A5 | **Hijack Swap universe** (PR “reuse `getAllTokens`”) | Rejected by C8 / unit: Create Pair catalog test must not import router pair graph | | A6 | **Remote list swap** (fetch GitLab raw / indexer tokens, attacker MITM or poisoned CDN) | No runtime HTTP for the catalog. Bundle only. | | A7 | **Native smuggling** (select LUNC, or paste `uluna`) | Cannot encode `native_token` on this page | | A8 | **Same-token both legs** via picker + paste mix (listed CL8Y + pasted same addr different case) | Compare case-insensitive; submit disabled | | A9 | **Search injection / DoS** (huge paste, regex-ish query) | 128-char cap; client filter only; no indexer `ILIKE` | | A10 | **`onChange` with id not in options** (tampered handler / extra option) | Ignore; submit uses only validated addresses | | A11 | **Homograph / wrong-prefix address** (`terra1` checksum fail, `terravaloper`, other HRP) | #382 errors; no submit | | A12 | **Clickjack / prefill via query string** (`/create?a=&b=`) | Out of scope unless implemented. If deep-link is added, still require checksum + user-visible symbols before enable. Default: **no** query prefill in this issue (open-redirect / phishing aid). | | A13 | **Social-engineering copy** (“Official verified token — safe to create”) | Forbidden. Listed = in our published catalog only. | | A14 | **LocalTerra mainnet-address foot-gun** | Env overlay required (C5). Test that picker does not offer columbus-5 cLUNC when `VITE_LUNC_C_TOKEN_ADDRESS` is the local wrap token. | ## Verification criteria Close only when all of the following are true: 1. `make test-frontend` (or scoped Vitest for catalog + `CreatePairPage` + existing #382/#508 cases) passes. 2. `make verify-issue-<iid>` exists and covers T1–T18 (and P1–P5 when LocalTerra is up — provision with `make setup-cloud-localterra` / `make has-localterra`, do not SKIP). 3. Manual: `/create` on LocalTerra — pick two env CW20s; confirm filled addresses match `.env.local`; paste path still works; natives absent; Swap pay/receive list unchanged. 4. `docs/frontend.md` § Create pair updated; no factory/indexer behavior change in the diff. 5. QA sign-off on C1–C11 and A1–A14 (table in the MR).
PlasticDigits commented 2026-08-17 03:45:49 +00:00 (Migrated from gitlab.com)

marked as related to #382

marked as related to #382
PlasticDigits commented 2026-08-17 03:45:50 +00:00 (Migrated from gitlab.com)

marked as related to #376

marked as related to #376
PlasticDigits commented 2026-08-17 03:45:50 +00:00 (Migrated from gitlab.com)

marked as related to #378

marked as related to #378
PlasticDigits commented 2026-08-17 03:45:51 +00:00 (Migrated from gitlab.com)

marked as related to #481

marked as related to #481
PlasticDigits commented 2026-08-17 03:45:52 +00:00 (Migrated from gitlab.com)

marked as related to #508

marked as related to #508
PlasticDigits commented 2026-08-17 04:28:59 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1071

mentioned in merge request !1071
PlasticDigits commented 2026-08-17 04:47:06 +00:00 (Migrated from gitlab.com)

mentioned in commit d7aa1c07e1

mentioned in commit d7aa1c07e1f21416a60c226a09bceb15b9b88db4
PlasticDigits commented 2026-08-17 04:47:14 +00:00 (Migrated from gitlab.com)

mentioned in commit 7fdc3a44ad

mentioned in commit 7fdc3a44ad3318fb4cc6df68805ff520b508ef97
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-17 04:47:15 +00:00
PlasticDigits commented 2026-08-17 04:53:46 +00:00 (Migrated from gitlab.com)

Merged via !1071 onto main (7fdc3a44) after landing !1069 + !1070. Conflicts were Makefile .PHONY/help unions plus both #541 and #542 rows in AGENTS_FRONTEND_COPY_COGNITIVE_LOAD.md.

Sanity check on main: make verify-issue-542 — 9/9 pass (unit T1–T18, catalog isolation, C542-1–C542-11 docs/skills). Playwright P1–P5 on LocalTerra all passed (listed pick, custom paste, no false success on existing pair, keyboard, phone width, natives absent).

Remaining (not a merge blocker): no on-chain Create Pair e2e-tx for a novel pair (one-per-block + fee). P3 only asserts no false success, not a specific factory duplicate-pair error string. Tracked in the post-merge follow-up.

Merged via !1071 onto `main` (`7fdc3a44`) after landing !1069 + !1070. Conflicts were Makefile `.PHONY`/help unions plus both #541 and #542 rows in `AGENTS_FRONTEND_COPY_COGNITIVE_LOAD.md`. **Sanity check on `main`:** `make verify-issue-542` — 9/9 pass (unit T1–T18, catalog isolation, C542-1–C542-11 docs/skills). Playwright P1–P5 on LocalTerra all passed (listed pick, custom paste, no false success on existing pair, keyboard, phone width, natives absent). **Remaining (not a merge blocker):** no on-chain Create Pair `e2e-tx` for a *novel* pair (one-per-block + fee). P3 only asserts no false success, not a specific factory duplicate-pair error string. Tracked in the post-merge follow-up.
PlasticDigits commented 2026-08-17 04:53:57 +00:00 (Migrated from gitlab.com)

mentioned in issue #545

mentioned in issue #545
PlasticDigits commented 2026-08-17 04:53:59 +00:00 (Migrated from gitlab.com)

marked as related to #545

marked as related to #545
PlasticDigits commented 2026-08-17 09:47:31 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1073

mentioned in merge request !1073
PlasticDigits commented 2026-08-18 12:08:57 +00:00 (Migrated from gitlab.com)

mentioned in issue #562

mentioned in issue #562
PlasticDigits commented 2026-08-18 12:08:58 +00:00 (Migrated from gitlab.com)

marked as related to #562

marked as related to #562
PlasticDigits commented 2026-08-22 10:59:13 +00:00 (Migrated from gitlab.com)

mentioned in issue #593

mentioned in issue #593
PlasticDigits commented 2026-08-23 04:45:17 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1120

mentioned in merge request !1120
PlasticDigits commented 2026-08-23 04:56:22 +00:00 (Migrated from gitlab.com)

mentioned in issue #602

mentioned in issue #602
PlasticDigits commented 2026-08-25 01:55:13 +00:00 (Migrated from gitlab.com)

mentioned in issue #630

mentioned in issue #630
PlasticDigits commented 2026-08-25 06:01:21 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1141

mentioned in merge request !1141
PlasticDigits commented 2026-08-25 06:04:06 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1142

mentioned in merge request !1142
PlasticDigits commented 2026-08-31 04:56:53 +00:00 (Migrated from gitlab.com)

mentioned in issue #711

mentioned in issue #711
PlasticDigits commented 2026-08-31 05:36:04 +00:00 (Migrated from gitlab.com)

mentioned in issue #713

mentioned in issue #713
PlasticDigits commented 2026-08-31 16:08:49 +00:00 (Migrated from gitlab.com)

mentioned in issue #715

mentioned in issue #715
PlasticDigits commented 2026-08-31 16:08:53 +00:00 (Migrated from gitlab.com)

marked as related to #715

marked as related to #715
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#542
No description provided.