feat(#507): fee-aware wrap UX and cLUNC/cUSTC mainnet enablement #1044

Merged
PlasticDigits merged 2 commits from feat/507-mainnet-wrap-ux into main 2026-08-09 07:02:31 +00:00
PlasticDigits commented 2026-08-08 13:19:47 +00:00 (Migrated from gitlab.com)

Summary

  • Wire wrap-mapper fee_bps into Swap/Pool simulate + execute nets (netCw20AfterNativeWrap / netAfterWrapMapperFee) so previews and CW20 send amounts match on-chain mint/unwrap skims (no false 1:1).
  • Display product symbols cLUNC / cUSTC (env keys stay VITE_LUNC_C_*); prefer registry symbols over on-chain LUNC-C labels.
  • Document post-SL5 Coolify wrap enablement (wrap-enablement.env.example, AGENTS_MAINNET_WRAP_ENABLEMENT.md) without silently adding wrap to soft-launch defaults (SL5).

Closes #507 for in-repo work. Ops still must set Coolify env + rebuild + mainnet smoke txs.

Test plan

  • make lint-frontend (warnings only, pre-existing)
  • Targeted Vitest: wrapMapper fee math, simulateNativeSwap fee_bps, tokenRegistry cLUNC/cUSTC, pool counterpart fee, SwapPage SEC-A02
  • Restest fee_bps / SEC-A02 suite
  • make test-mainnet-soft-launch-defaults
  • Coolify: set four wrap VITE_* from deployments/mainnet-soft-launch/wrap-enablement.env.example and rebuild
  • Mainnet QA: wrap/unwrap both denoms; confirm fee skim at fee_bps=100; pause/rate-limit CTAs
  • LocalTerra regression (optional): e2e/wrap-swap.spec.ts after redeploy

Issue AC checkoff

  • Fee UX query-backed (UI note + sim/execute nets)
  • cLUNC/cUSTC symbols + tokenlist + registry override
  • Pause/rate-limit CTAs preserved
  • Pool auto-wrap nets fee after tax
  • SL5 docs + defaults script not rewritten as economic wrap deploy
  • Lint + targeted frontend tests green for changed code
  • Production Coolify env set (ops)
  • Production Swap wrap/unwrap smoke (ops)
  • Mainnet smoke tx hashes recorded (ops)

Closes #507

## Summary - Wire wrap-mapper `fee_bps` into Swap/Pool simulate + execute nets (`netCw20AfterNativeWrap` / `netAfterWrapMapperFee`) so previews and CW20 send amounts match on-chain mint/unwrap skims (no false 1:1). - Display product symbols **cLUNC** / **cUSTC** (env keys stay `VITE_LUNC_C_*`); prefer registry symbols over on-chain `LUNC-C` labels. - Document post-SL5 Coolify wrap enablement (`wrap-enablement.env.example`, `AGENTS_MAINNET_WRAP_ENABLEMENT.md`) without silently adding wrap to soft-launch defaults (**SL5**). Closes #507 for in-repo work. Ops still must set Coolify env + rebuild + mainnet smoke txs. ## Test plan - [x] `make lint-frontend` (warnings only, pre-existing) - [x] Targeted Vitest: wrapMapper fee math, `simulateNativeSwap` fee_bps, tokenRegistry cLUNC/cUSTC, pool counterpart fee, SwapPage SEC-A02 - [x] Restest fee_bps / SEC-A02 suite - [x] `make test-mainnet-soft-launch-defaults` - [ ] Coolify: set four wrap `VITE_*` from `deployments/mainnet-soft-launch/wrap-enablement.env.example` and rebuild - [ ] Mainnet QA: wrap/unwrap both denoms; confirm fee skim at `fee_bps=100`; pause/rate-limit CTAs - [ ] LocalTerra regression (optional): `e2e/wrap-swap.spec.ts` after redeploy ## Issue AC checkoff - [x] Fee UX query-backed (UI note + sim/execute nets) - [x] cLUNC/cUSTC symbols + tokenlist + registry override - [x] Pause/rate-limit CTAs preserved - [x] Pool auto-wrap nets fee after tax - [x] SL5 docs + defaults script not rewritten as economic wrap deploy - [x] Lint + targeted frontend tests green for changed code - [ ] Production Coolify env set (ops) - [ ] Production Swap wrap/unwrap smoke (ops) - [ ] Mainnet smoke tx hashes recorded (ops) Closes #507
PlasticDigits commented 2026-08-08 13:19:52 +00:00 (Migrated from gitlab.com)

marked this merge request as ready

marked this merge request as **ready**
PlasticDigits commented 2026-08-08 13:19:55 +00:00 (Migrated from gitlab.com)

mentioned in issue #507

mentioned in issue #507
PlasticDigits commented 2026-08-09 06:50:50 +00:00 (Migrated from gitlab.com)

RECOMMEND: FIX

Thorough review of !1044 (3681e8b, feat/507-mainnet-wrap-ux → main) against #507, including security/DeFi attack surface, gap analysis, and verification of fee math vs router invariants. Scope is client/docs/tokenlist only — no smartcontracts/ changes. No critical fund-drain, prompt-injection, or DB-leak findings; do not merge until Medium items below are addressed (or explicitly accepted as post-merge enablement gates with Coolify still dark).

Issue #507 acceptance criteria

AC Status
Coolify four wrap VITE_* on columbus-5 Ops-deferred (template + playbook added)
Prod Swap wrap/unwrap smoke Ops-deferred
Fee displayed query-backed (expect 100 bps / 1%) Partial — correct when LCD config loads; fail-open to 0 → false “1:1” when config missing (see M1)
Pause / rate-limit CTAs gate submit Partial — works when LCD works; fail-open on query error (M2)
Pool provide/withdraw auto-wrap Partial — provide nets fee after tax; withdraw unwrap fee not previewed
cLUNC / cUSTC symbols + metadata Met (registry override + tokenlist)
SL5 docs note post-enablement; defaults not silently enabling wrap Met — test-mainnet-soft-launch-defaults asserts commented-only wrap keys
Lint / frontend tests for changed code Met locally for targeted suites (see Testing)
Mainnet smoke tx hashes / LCD verify Ops-deferred (issue verification checklist)

In-repo #507 work is largely delivered; closing the issue still requires ops Coolify + smoke. Treat “Closes #507” as in-repo complete only if that matches project practice — otherwise keep issue open until ops ACs land.

Security / DeFi audit

Smart contracts / oracle / DB / indexer

  • Contracts: unchanged; fee skim / pause / rate-limit / minimum_receive remain chain-enforced.
  • Oracle: no new oracle paths; wrap fee is mapper Config.fee_bps, not an external price feed.
  • Indexer: route_slippage.rs only adds cLUNC/cUSTC symbol aliases — no auth, query, or PII surface change. No DB leak vector in this MR.
  • Rust server: no privilege/access-control change.

Attack table from #507

Vector Assessment
False fee-free 1:1 while fee_bps=100 Fixed when config loads; regresses on LCD failure (M1)
Pause while wrapping Gated when LCD works; fail-open on error (M2)
Rate-limit bypass in UI Same fail-open (M2)
Wrong treasury (governance multisig vs CMM) Docs/checklist only — no runtime env↔config.treasury check (M3)
LocalTerra addresses on prod Ops/docs; no chain-id runtime assert
minimum_receive on unwrap_output OK — sim nets unwrap fee before slippage floor (R3 aligned)
Malicious logo URL Unchanged allowlist
Double-wrap / wrong mapping Still env pairs + chain mapping authoritative
Sandwich/MEV from optimistic quotes Client overquote on fee=0 fallback can cause failed txs (native-input CW20 send too large) or pool provide imbalance — not a silent drain of third-party funds

Findings to fix

M1 — Fail-open fee_bps = 0 when wrap-mapper config is unavailable
queryWrapMapperFeeBps / wrapMapperFeeBps ?? 0 + wrapUnwrapFeeNote(..., 0) → “Wrap (1:1)” and optimistic sim.
frontend-dapp/src/services/terraclassic/wrapMapper.ts (queryWrapMapperConfig catch → null; fee helper → 0), SwapPage.tsx (~382), PoolPage fee default.
Fix: treat unknown fee as blocking for wrap/native-wrap paths (disable submit + “Wrap fee unavailable”); never show 1:1 unless config successfully returned fee_bps === 0. Include fee_bps / config success in sim query key.

M2 — Pause / rate-limit queries fail-open on LCD error
queryPausedState / checkRateLimitExceeded catch → false.
Fix: if wrap mapper is configured and safety queries fail, disable wrap submit (same gate as M1).

M3 — No runtime treasury cross-check
Build-time VITE_TREASURY_ADDRESS is trusted for wrap_deposit. Wrong Coolify value misroutes user funds. Docs warn; UI does not.
Fix: after config load, compare config.treasury to TREASURY_CONTRACT_ADDRESS; banner + block wrap on mismatch.

L1 — E2E E4 still weakly asserts /1:1/ (wrap-swap.spec.ts) with toBeGreaterThanOrEqual(0) — no regression guard for fee note.
L2 — Multihop native routes only say “wrap/unwrap your tokens” (no fee %).
L3 — amountForTargetNetAfterWrapMapperFee returns targetNet when bps >= 10000 (edge; unlikely on-chain).
L4 — Trade market panel still has no native wrap path (isWrapOrUnwrap: false) — document as intentional out-of-scope for #507.

Fee math correctness (when config loads)

Verified aligned with router net_after_wrap_mapper_unwrap_fee:

  • net = A − floor(A × fee_bps / 10_000)
  • Order: burn tax → mapper fee → CW20 send / sim offer (netCw20AfterNativeWrap)
  • Simulate ↔ execute CW20 amounts match for native-input paths
  • Native-output minimum_receive uses post-unwrap net

Gap analysis (non-security)

Area Gap Blocks merge?
Features In-repo wrap fee UX + naming + enablement docs solid After M1–M3
UI/UX Config-failure honesty; multihop fee copy; no tax+fee breakdown; Trade wrap N/A M1 yes
Unit tests Good math coverage; missing config-null / amount=1 / page-level fee note Prefer before merge
E2E Symbol rename only; fee assertion weak Prefer before merge
Packages frontend + tokenlist + scripts/docs; contracts unchanged (correct); docker/frontend example not updated (minor) No
Gas N/A No
DRY / readability Shared helpers good; dual sync/async fee patterns in Pool acceptable No
SL5 guardrail Confirmed not silently enabling wrap —
Retail expectations Fee breakdown, burn-tax note, portfolio wrap labels still missing No for #507

Testing performed in review

  • Targeted Vitest: wrapMapper, router, poolProvideCounterpart, tokenRegistry — 54 passed
  • make test-mainnet-soft-launch-defaults — ALL PASS
  • Full LocalTerra e2e / mainnet smoke — not run (ops / optional per MR)

CI

Pipeline 2743565178 failed on cargo-audit-indexer (event-listener, scc, spin advisories) — unrelated to this diff, but may block merge under green-pipeline policy. Frontend jobs were skipped after security stage failure.

  1. Fail-closed wrap submit + display when mapper config / pause / rate-limit queries fail (M1–M2).
  2. Runtime treasury address match against mapper config (M3).
  3. Strengthen unit + e2e: assert fee note for fee_bps=100; assert submit disabled on config null; drop useless /1:1/ soft assert.
  4. Clarify Trade native wrap out-of-scope on the issue.
  5. Resolve or waive cargo-audit-indexer for pipeline green.
  6. Keep Coolify wrap env off until M1–M3 land; then run docs/qa-templates/wrap-unwrap-test-pass.md and record smoke txs.

Bottom line: Strong in-repo progress on fee-honest wrap UX and SL5-safe enablement docs, but optimistic fee/safety defaults under LCD failure are inconsistent with #507’s attack table and should be fixed before production wrap enablement.

RECOMMEND: FIX Thorough review of !1044 (`3681e8b`, `feat/507-mainnet-wrap-ux` → `main`) against [#507](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/507), including security/DeFi attack surface, gap analysis, and verification of fee math vs router invariants. Scope is client/docs/tokenlist only — **no `smartcontracts/` changes**. No critical fund-drain, prompt-injection, or DB-leak findings; **do not merge until Medium items below are addressed** (or explicitly accepted as post-merge enablement gates with Coolify still dark). ## Issue #507 acceptance criteria | AC | Status | |----|--------| | Coolify four wrap `VITE_*` on columbus-5 | **Ops-deferred** (template + playbook added) | | Prod Swap wrap/unwrap smoke | **Ops-deferred** | | Fee displayed query-backed (expect 100 bps / 1%) | **Partial** — correct when LCD config loads; **fail-open to 0 → false “1:1”** when config missing (see M1) | | Pause / rate-limit CTAs gate submit | **Partial** — works when LCD works; **fail-open** on query error (M2) | | Pool provide/withdraw auto-wrap | **Partial** — provide nets fee after tax; withdraw unwrap fee not previewed | | cLUNC / cUSTC symbols + metadata | **Met** (registry override + tokenlist) | | SL5 docs note post-enablement; defaults **not** silently enabling wrap | **Met** — `test-mainnet-soft-launch-defaults` asserts commented-only wrap keys | | Lint / frontend tests for changed code | **Met** locally for targeted suites (see Testing) | | Mainnet smoke tx hashes / LCD verify | **Ops-deferred** (issue verification checklist) | In-repo #507 work is largely delivered; closing the issue still requires ops Coolify + smoke. Treat “Closes #507” as **in-repo complete only** if that matches project practice — otherwise keep issue open until ops ACs land. ## Security / DeFi audit ### Smart contracts / oracle / DB / indexer - **Contracts:** unchanged; fee skim / pause / rate-limit / `minimum_receive` remain chain-enforced. - **Oracle:** no new oracle paths; wrap fee is mapper `Config.fee_bps`, not an external price feed. - **Indexer:** `route_slippage.rs` only adds cLUNC/cUSTC symbol aliases — no auth, query, or PII surface change. **No DB leak vector** in this MR. - **Rust server:** no privilege/access-control change. ### Attack table from #507 | Vector | Assessment | |--------|------------| | False fee-free 1:1 while `fee_bps=100` | **Fixed when config loads**; **regresses on LCD failure** (M1) | | Pause while wrapping | Gated when LCD works; fail-open on error (M2) | | Rate-limit bypass in UI | Same fail-open (M2) | | Wrong treasury (governance multisig vs CMM) | Docs/checklist only — **no runtime env↔config.treasury check** (M3) | | LocalTerra addresses on prod | Ops/docs; no chain-id runtime assert | | `minimum_receive` on `unwrap_output` | **OK** — sim nets unwrap fee before slippage floor (R3 aligned) | | Malicious logo URL | Unchanged allowlist | | Double-wrap / wrong mapping | Still env pairs + chain mapping authoritative | | Sandwich/MEV from optimistic quotes | Client overquote on fee=0 fallback can cause **failed txs** (native-input CW20 send too large) or **pool provide imbalance** — not a silent drain of third-party funds | ### Findings to fix **M1 — Fail-open `fee_bps = 0` when wrap-mapper config is unavailable** `queryWrapMapperFeeBps` / `wrapMapperFeeBps ?? 0` + `wrapUnwrapFeeNote(..., 0)` → “Wrap (1:1)” and optimistic sim. `frontend-dapp/src/services/terraclassic/wrapMapper.ts` (`queryWrapMapperConfig` catch → null; fee helper → 0), `SwapPage.tsx` (~382), `PoolPage` fee default. **Fix:** treat unknown fee as blocking for wrap/native-wrap paths (disable submit + “Wrap fee unavailable”); never show 1:1 unless config successfully returned `fee_bps === 0`. Include `fee_bps` / config success in sim query key. **M2 — Pause / rate-limit queries fail-open on LCD error** `queryPausedState` / `checkRateLimitExceeded` catch → `false`. **Fix:** if wrap mapper is configured and safety queries fail, disable wrap submit (same gate as M1). **M3 — No runtime treasury cross-check** Build-time `VITE_TREASURY_ADDRESS` is trusted for `wrap_deposit`. Wrong Coolify value misroutes user funds. Docs warn; UI does not. **Fix:** after config load, compare `config.treasury` to `TREASURY_CONTRACT_ADDRESS`; banner + block wrap on mismatch. **L1 — E2E E4 still weakly asserts `/1:1/`** (`wrap-swap.spec.ts`) with `toBeGreaterThanOrEqual(0)` — no regression guard for fee note. **L2 — Multihop native routes** only say “wrap/unwrap your tokens” (no fee %). **L3 — `amountForTargetNetAfterWrapMapperFee` returns `targetNet` when `bps >= 10000`** (edge; unlikely on-chain). **L4 — Trade market panel** still has no native wrap path (`isWrapOrUnwrap: false`) — document as intentional out-of-scope for #507. ## Fee math correctness (when config loads) Verified aligned with router `net_after_wrap_mapper_unwrap_fee`: - `net = A − floor(A × fee_bps / 10_000)` - Order: burn tax → mapper fee → CW20 send / sim offer (`netCw20AfterNativeWrap`) - Simulate ↔ execute CW20 amounts match for native-input paths - Native-output `minimum_receive` uses post-unwrap net ## Gap analysis (non-security) | Area | Gap | Blocks merge? | |------|-----|---------------| | Features | In-repo wrap fee UX + naming + enablement docs solid | After M1–M3 | | UI/UX | Config-failure honesty; multihop fee copy; no tax+fee breakdown; Trade wrap N/A | M1 yes | | Unit tests | Good math coverage; missing config-null / amount=1 / page-level fee note | Prefer before merge | | E2E | Symbol rename only; fee assertion weak | Prefer before merge | | Packages | frontend + tokenlist + scripts/docs; contracts unchanged (correct); docker/frontend example not updated (minor) | No | | Gas | N/A | No | | DRY / readability | Shared helpers good; dual sync/async fee patterns in Pool acceptable | No | | SL5 guardrail | Confirmed not silently enabling wrap | — | | Retail expectations | Fee breakdown, burn-tax note, portfolio wrap labels still missing | No for #507 | ## Testing performed in review - Targeted Vitest: `wrapMapper`, `router`, `poolProvideCounterpart`, `tokenRegistry` — **54 passed** - `make test-mainnet-soft-launch-defaults` — **ALL PASS** - Full LocalTerra e2e / mainnet smoke — **not run** (ops / optional per MR) ## CI Pipeline [2743565178](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/pipelines/2743565178) **failed** on `cargo-audit-indexer` (`event-listener`, `scc`, `spin` advisories) — **unrelated to this diff**, but may block merge under green-pipeline policy. Frontend jobs were skipped after security stage failure. ## Recommended before merge 1. Fail-closed wrap submit + display when mapper config / pause / rate-limit queries fail (M1–M2). 2. Runtime treasury address match against mapper config (M3). 3. Strengthen unit + e2e: assert fee note for `fee_bps=100`; assert submit disabled on config null; drop useless `/1:1/` soft assert. 4. Clarify Trade native wrap out-of-scope on the issue. 5. Resolve or waive `cargo-audit-indexer` for pipeline green. 6. Keep Coolify wrap env **off** until M1–M3 land; then run `docs/qa-templates/wrap-unwrap-test-pass.md` and record smoke txs. **Bottom line:** Strong in-repo progress on fee-honest wrap UX and SL5-safe enablement docs, but **optimistic fee/safety defaults under LCD failure are inconsistent with #507’s attack table** and should be fixed before production wrap enablement.
PlasticDigits commented 2026-08-09 06:54:24 +00:00 (Migrated from gitlab.com)

added 1 commit

  • a2f57478 - fix(#507): fail-closed wrap config, pause, and treasury gates

Compare with previous version

added 1 commit <ul><li>a2f57478 - fix(#507): fail-closed wrap config, pause, and treasury gates</li></ul> [Compare with previous version](/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/326/diffs?diff_id=1950753994&start_sha=3681e8b5484ff0f394522ff207801438a6a4f1bb)
PlasticDigits commented 2026-08-09 07:02:31 +00:00 (Migrated from gitlab.com)

mentioned in commit 8fa03d6f06

mentioned in commit 8fa03d6f06aa2517682a8865fdfc9b0c9135b2c0
PlasticDigits (Migrated from gitlab.com) merged commit 8fa03d6f06 into main 2026-08-09 07:02:31 +00:00
PlasticDigits commented 2026-08-09 07:05:00 +00:00 (Migrated from gitlab.com)

mentioned in issue #506

mentioned in issue #506
PlasticDigits commented 2026-08-09 07:05:01 +00:00 (Migrated from gitlab.com)

mentioned in issue #508

mentioned in issue #508
Sign in to join this conversation.
No reviewers
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!1044
No description provided.