ux: Pool fee badge — surface register CTA / eligibility (reporter confirmed discount works after /tiers) #476

Closed
opened 2026-07-12 07:14:19 +00:00 by PlasticDigits · 10 comments
PlasticDigits commented 2026-07-12 07:14:19 +00:00 (Migrated from gitlab.com)

Summary

Mainnet soft-launch bug report (Pool page, CORAL/PEARL): “fee reduction from CL8Y-cb holding isn't applied” while the UI shows FEE: 1.80%.

Investigation verdict: On-chain fee discount is implemented and live on columbus-5. The reporter’s symptom is expected when the wallet is not registered on /tiers, holds a different token than the configured cl8y_token, or when the UI only has the base pair fee to show. Pool lacks Swap’s unregistered CTA and registry-outage warning, so users reasonably conclude the feature is missing.

Bundle related frontend/docs work here. Do not change the on-chain register+balance model unless product explicitly opens a separate contract RFC.


Current codebase

On-chain (working)

Piece Behavior
Fee-discount contract terra1wcczsdk7jwj99n3my6wx8wr4ee0hn6yaapgd792lgx5elrdtrn2scfnecz — tiers 0–9 + 255; cl8y_token = terra16wtml2q66g82fdkx66tap0qjkahqwp4lwq3ngtygacg5q0kzycgqvhpax3
Eligibility Hold configured CL8Y CW20 and Register { tier_id } on /tiers (EOA self-register for tiers 1–9). Holding alone → discount_bps: 0
Pair swap effective_fee_bps = fee_bps * (10000 - discount_bps) / 10000; fail-closed to full fee_bps if registry query fails (#365)
Router Forwards trader on hops when trusted
Default pair fee 180 bps (1.80%) on soft-launch pairs

Verified production: dex.cl8y.com bundle embeds VITE_FEE_DISCOUNT_ADDRESS; indexer GET /api/v1/health/fee-discount returns fee_discount_registry_ok: true.

Frontend gaps

Surface Today
Pool (PoolPage.tsx) Queries getTraderDiscount when wallet + env set; FeeDisplay shows strikethrough only if discount_bps > 0. No “Hold CL8Y… → /tiers” CTA. No registry-outage banner (#374).
Swap (SwapPage.tsx) Same discount query + CTA when unregistered + registry-outage warning
Tiers (TiersPage.tsx) Correct copy: “Hold CL8Y… then register…”
Token identity String “CL8Y-cb” does not exist in repo. Only the single CW20 above counts. Users holding a differently named/bridged asset see no discount.
Decimals tokenRegistry.ts lists CL8Y as 6 decimals; chain/docs use 18. /tiers “Hold X CL8Y” can display wrong amounts (lookupByCW20(…).decimals ?? 18).

FeeDisplay with discountBps === 0 → plain 1.80% — matches the screenshot for an unregistered (or wrong-token) connected wallet.

  • Canonical tiers: docs/reference/fee-discount-tiers.md (#198)
  • Registry outage Swap banner: #374 / #365
  • Quote trader parity: #238 / #245
  • Agent playbook: skills/AGENTS_FEE_DISCOUNT_TIERS.md
  • Deploy: deployments/mainnet-soft-launch/deploy-trace.md

Why a new implementation is needed

Users on Pool (and reporters saying “CL8Y-cb”) cannot tell whether:

  1. the feature exists,
  2. they must register,
  3. which exact CW20 balances count,
  4. or the registry LCD is down (Swap warns; Pool does not).

Without Pool parity + explicit eligibility copy, soft-launch support load and “feature broken” reports will continue even when on-chain discount works.


Constraints / guardrails

  • Do not make discounts passive (hold-only without Register) in this issue — registration is intentional (security-model / anti-gaming). That would be a separate contract ADR.
  • Do not add multi-token cl8y_token eligibility here — product/governance decision tracked in the companion ops/docs issue if needed.
  • Keep on-chain fail-closed full fee on registry errors (#365); UI warnings must stay non-blocking.
  • Reuse existing feeDiscountRegistryWarning helpers — no parallel status logic.
  • Preserve FeeDisplay math; only improve empty/unregistered/outage states and copy.
  • Fix CL8Y decimals carefully: LocalTerra TCL8Y is also 18 (#383); do not break tests that assume 6 in unrelated tokens.

Relevant files

File Role
frontend-dapp/src/pages/PoolPage.tsx Fee badge; missing CTA / outage warning
frontend-dapp/src/pages/SwapPage.tsx Reference CTA + banner behavior
frontend-dapp/src/pages/TiersPage.tsx Registration UX; decimals via registry
frontend-dapp/src/components/ui/FeeDisplay.tsx Base vs discounted display
frontend-dapp/src/utils/feeDiscountRegistryWarning.ts Shared outage resolution
frontend-dapp/src/services/terraclassic/feeDiscount.ts LCD get_discount / get_registration
frontend-dapp/src/utils/tokenRegistry.ts CL8Y decimals (bug: 6 vs 18)
frontend-dapp/src/utils/constants.ts VITE_FEE_DISCOUNT_ADDRESS, VITE_CL8Y_TOKEN_ADDRESS
docs/frontend.md Fee discount + outage docs
docs/reference/fee-discount-tiers.md Tier ladder / invariants
QA_TEMPLATE.md § 3.1.3 Pool fee display QA
frontend-dapp/src/pages/SwapPage.feeDiscountRegistryBanner.test.tsx Pattern to mirror for Pool

  1. Pool parity with Swap (unregistered): When wallet connected, fee-discount configured, and status unregistered, show the same “Hold CL8Y to reduce swap fees →” link to /tiers near the pool fee badge (or pool list header).
  2. Eligibility copy: Short tooltip/footnote on Pool fee badge and reinforce on /tiers: discount applies only after register, and only the configured CL8Y CW20 (VITE_CL8Y_TOKEN_ADDRESS / fee-discount config.cl8y_token) counts — not arbitrarily named “CL8Y-cb” holdings unless that address matches.
  3. Pool registry outage: Reuse shouldShowFeeDiscountRegistryWarning / banner text on Pool when registered + LCD/indexer unhealthy (same as Swap #374).
  4. Optional badge states: e.g. “1.80% · not registered” vs strikethrough discounted fee when discount_bps > 0 (keep FeeDisplay as source of truth for the %).
  5. Fix CL8Y decimals in tokenRegistry.ts to 18 (or address-keyed override) + unit test so /tiers Hold amounts match min_cl8y_balance wei.
  6. Docs: one paragraph in docs/frontend.md / FAQ pointing reporters to /tiers + explorer link for cl8y_token.

Acceptance criteria

  • Connected unregistered wallet on Pool sees a clear path to /tiers (CTA or equivalent), not only a bare 1.80%.
  • Copy states that users must hold the eligible CL8Y CW20 and register; does not imply passive holding alone.
  • Connected registered wallet with live discount sees strikethrough base + effective fee on Pool (existing FeeDisplay path), covered by automated test.
  • Registered + mocked registry unreachable → Pool shows the same non-blocking warning pattern as Swap; swap/pool actions stay enabled.
  • CL8Y display decimals are 18 for the mainnet/TCL8Y address; tier “Hold N CL8Y” matches canonical doc for tier 1+.
  • No contract changes; no change to fail-closed fee behavior.

Test plan (all paths)

Path Expectation
Wallet disconnected on Pool Base pair fee only; no false discount
Connected, env set, unregistered, healthy LCD Base fee + CTA to /tiers
Connected, registered, discount_bps > 0 Strikethrough + effective % on Pool card
Connected, registered, balance below tier (needs_deregister / 0 discount) Base fee; no fake discount (on-chain truth)
VITE_FEE_DISCOUNT_ADDRESS empty No discount queries; base fee; no broken CTA
Registry LCD fail + indexer fee_discount_registry_ok: false Pool warning visible for registered; submit not blocked
/tiers Hold labels after decimals fix Tier 1 shows “1” CL8Y (not 10^12-scaled wrong)
Existing Swap CTA / banner tests Still green
E2E: register tier → Pool fee badge discounted Extend or add coverage near fee-tier-tx.spec.ts / Pool QA 3.1.3

Unit: PoolPage tests mirroring SwapPage.feeDiscountRegistryBanner.test.tsx; tokenRegistry / Tiers formatting tests.


Test plan (attack / hack / abuse vectors)

Vector Expectation
Spoofed UI claiming discount without register UI only; on-chain still charges full fee until Register + balance OK
Holding unrelated “CL8Y-cb” / wrong CW20 get_discount stays 0; register fails or deregisters — UI must not invent a discount
Phishing link in CTA CTA must stay relative /tiers on same origin
Registry outage social-engineering (“pay less off-chain”) Banner states on-chain may charge full fee; no off-chain fee promises
Contract/account spoof in copy Always cite config.cl8y_token / env address, not a free-text symbol alone

Verification criteria

  • Manual mainnet (or LocalTerra): unregistered → Pool CTA; register tier 1 with TCL8Y/CL8Y → Pool shows discounted fee; deregister → base fee returns.
  • cd frontend-dapp && npm run test:run covers new Pool + decimals tests.
  • Optional: make verify-issue-365 still passes after sharing warning util on Pool.
  • Reporter checklist comment: query get_registration / get_discount / CW20 balance for their wallet against fee-discount cl8y_token before claiming on-chain failure.
## Summary Mainnet soft-launch bug report (Pool page, CORAL/PEARL): *“fee reduction from CL8Y-cb holding isn't applied”* while the UI shows **FEE: 1.80%**. **Investigation verdict:** On-chain fee discount **is implemented and live** on columbus-5. The reporter’s symptom is expected when the wallet is **not registered** on `/tiers`, holds a **different token** than the configured `cl8y_token`, or when the UI only has the base pair fee to show. Pool lacks Swap’s unregistered CTA and registry-outage warning, so users reasonably conclude the feature is missing. Bundle related frontend/docs work here. Do **not** change the on-chain register+balance model unless product explicitly opens a separate contract RFC. --- ## Current codebase ### On-chain (working) | Piece | Behavior | |-------|----------| | Fee-discount contract | `terra1wcczsdk7jwj99n3my6wx8wr4ee0hn6yaapgd792lgx5elrdtrn2scfnecz` — tiers 0–9 + 255; `cl8y_token` = `terra16wtml2q66g82fdkx66tap0qjkahqwp4lwq3ngtygacg5q0kzycgqvhpax3` | | Eligibility | **Hold** configured CL8Y CW20 **and** **`Register { tier_id }`** on `/tiers` (EOA self-register for tiers 1–9). Holding alone → `discount_bps: 0` | | Pair swap | `effective_fee_bps = fee_bps * (10000 - discount_bps) / 10000`; fail-closed to full `fee_bps` if registry query fails (#365) | | Router | Forwards `trader` on hops when trusted | | Default pair fee | **180 bps (1.80%)** on soft-launch pairs | Verified production: `dex.cl8y.com` bundle embeds `VITE_FEE_DISCOUNT_ADDRESS`; indexer `GET /api/v1/health/fee-discount` returns `fee_discount_registry_ok: true`. ### Frontend gaps | Surface | Today | |---------|--------| | **Pool** (`PoolPage.tsx`) | Queries `getTraderDiscount` when wallet + env set; `FeeDisplay` shows strikethrough only if `discount_bps > 0`. **No** “Hold CL8Y… → /tiers” CTA. **No** registry-outage banner (#374). | | **Swap** (`SwapPage.tsx`) | Same discount query + CTA when `unregistered` + registry-outage warning | | **Tiers** (`TiersPage.tsx`) | Correct copy: *“Hold CL8Y… then register…”* | | **Token identity** | String **“CL8Y-cb” does not exist** in repo. Only the single CW20 above counts. Users holding a differently named/bridged asset see no discount. | | **Decimals** | `tokenRegistry.ts` lists CL8Y as **6** decimals; chain/docs use **18**. `/tiers` “Hold X CL8Y” can display wrong amounts (`lookupByCW20(…).decimals ?? 18`). | `FeeDisplay` with `discountBps === 0` → plain `1.80%` — matches the screenshot for an unregistered (or wrong-token) connected wallet. ### Related issues / docs - Canonical tiers: `docs/reference/fee-discount-tiers.md` (#198) - Registry outage Swap banner: #374 / #365 - Quote `trader` parity: #238 / #245 - Agent playbook: `skills/AGENTS_FEE_DISCOUNT_TIERS.md` - Deploy: `deployments/mainnet-soft-launch/deploy-trace.md` --- ## Why a new implementation is needed Users on Pool (and reporters saying “CL8Y-cb”) cannot tell whether: 1. the feature exists, 2. they must **register**, 3. which **exact CW20** balances count, 4. or the registry LCD is down (Swap warns; Pool does not). Without Pool parity + explicit eligibility copy, soft-launch support load and “feature broken” reports will continue even when on-chain discount works. --- ## Constraints / guardrails - **Do not** make discounts passive (hold-only without `Register`) in this issue — registration is intentional (security-model / anti-gaming). That would be a separate contract ADR. - **Do not** add multi-token `cl8y_token` eligibility here — product/governance decision tracked in the companion ops/docs issue if needed. - Keep on-chain fail-closed full fee on registry errors (#365); UI warnings must stay **non-blocking**. - Reuse existing `feeDiscountRegistryWarning` helpers — no parallel status logic. - Preserve `FeeDisplay` math; only improve empty/unregistered/outage states and copy. - Fix CL8Y decimals carefully: LocalTerra TCL8Y is also 18 (#383); do not break tests that assume 6 in unrelated tokens. --- ## Relevant files | File | Role | |------|------| | `frontend-dapp/src/pages/PoolPage.tsx` | Fee badge; missing CTA / outage warning | | `frontend-dapp/src/pages/SwapPage.tsx` | Reference CTA + banner behavior | | `frontend-dapp/src/pages/TiersPage.tsx` | Registration UX; decimals via registry | | `frontend-dapp/src/components/ui/FeeDisplay.tsx` | Base vs discounted display | | `frontend-dapp/src/utils/feeDiscountRegistryWarning.ts` | Shared outage resolution | | `frontend-dapp/src/services/terraclassic/feeDiscount.ts` | LCD `get_discount` / `get_registration` | | `frontend-dapp/src/utils/tokenRegistry.ts` | CL8Y decimals (bug: 6 vs 18) | | `frontend-dapp/src/utils/constants.ts` | `VITE_FEE_DISCOUNT_ADDRESS`, `VITE_CL8Y_TOKEN_ADDRESS` | | `docs/frontend.md` | Fee discount + outage docs | | `docs/reference/fee-discount-tiers.md` | Tier ladder / invariants | | `QA_TEMPLATE.md` § 3.1.3 | Pool fee display QA | | `frontend-dapp/src/pages/SwapPage.feeDiscountRegistryBanner.test.tsx` | Pattern to mirror for Pool | --- ## Recommended direction 1. **Pool parity with Swap (unregistered):** When wallet connected, fee-discount configured, and status `unregistered`, show the same “Hold CL8Y to reduce swap fees →” link to `/tiers` near the pool fee badge (or pool list header). 2. **Eligibility copy:** Short tooltip/footnote on Pool fee badge and reinforce on `/tiers`: discount applies only after **register**, and only the **configured CL8Y CW20** (`VITE_CL8Y_TOKEN_ADDRESS` / fee-discount `config.cl8y_token`) counts — not arbitrarily named “CL8Y-cb” holdings unless that address matches. 3. **Pool registry outage:** Reuse `shouldShowFeeDiscountRegistryWarning` / banner text on Pool when registered + LCD/indexer unhealthy (same as Swap #374). 4. **Optional badge states:** e.g. “1.80% · not registered” vs strikethrough discounted fee when `discount_bps > 0` (keep `FeeDisplay` as source of truth for the %). 5. **Fix CL8Y decimals** in `tokenRegistry.ts` to **18** (or address-keyed override) + unit test so `/tiers` Hold amounts match `min_cl8y_balance` wei. 6. Docs: one paragraph in `docs/frontend.md` / FAQ pointing reporters to `/tiers` + explorer link for `cl8y_token`. --- ## Acceptance criteria - [ ] Connected **unregistered** wallet on Pool sees a clear path to `/tiers` (CTA or equivalent), not only a bare `1.80%`. - [ ] Copy states that users must **hold the eligible CL8Y CW20 and register**; does not imply passive holding alone. - [ ] Connected **registered** wallet with live discount sees strikethrough base + effective fee on Pool (existing `FeeDisplay` path), covered by automated test. - [ ] Registered + mocked registry unreachable → Pool shows the same non-blocking warning pattern as Swap; swap/pool actions stay enabled. - [ ] CL8Y display decimals are **18** for the mainnet/TCL8Y address; tier “Hold N CL8Y” matches canonical doc for tier 1+. - [ ] No contract changes; no change to fail-closed fee behavior. --- ## Test plan (all paths) | Path | Expectation | |------|-------------| | Wallet disconnected on Pool | Base pair fee only; no false discount | | Connected, env set, **unregistered**, healthy LCD | Base fee + CTA to `/tiers` | | Connected, **registered**, `discount_bps > 0` | Strikethrough + effective % on Pool card | | Connected, registered, balance below tier (needs_deregister / 0 discount) | Base fee; no fake discount (on-chain truth) | | `VITE_FEE_DISCOUNT_ADDRESS` empty | No discount queries; base fee; no broken CTA | | Registry LCD fail + indexer `fee_discount_registry_ok: false` | Pool warning visible for registered; submit not blocked | | `/tiers` Hold labels after decimals fix | Tier 1 shows “1” CL8Y (not 10^12-scaled wrong) | | Existing Swap CTA / banner tests | Still green | | E2E: register tier → Pool fee badge discounted | Extend or add coverage near `fee-tier-tx.spec.ts` / Pool QA 3.1.3 | Unit: `PoolPage` tests mirroring `SwapPage.feeDiscountRegistryBanner.test.tsx`; `tokenRegistry` / Tiers formatting tests. --- ## Test plan (attack / hack / abuse vectors) | Vector | Expectation | |--------|-------------| | Spoofed UI claiming discount without register | UI only; on-chain still charges full fee until `Register` + balance OK | | Holding unrelated “CL8Y-cb” / wrong CW20 | `get_discount` stays 0; register fails or deregisters — UI must not invent a discount | | Phishing link in CTA | CTA must stay relative `/tiers` on same origin | | Registry outage social-engineering (“pay less off-chain”) | Banner states on-chain may charge full fee; no off-chain fee promises | | Contract/account spoof in copy | Always cite `config.cl8y_token` / env address, not a free-text symbol alone | --- ## Verification criteria - Manual mainnet (or LocalTerra): unregistered → Pool CTA; register tier 1 with TCL8Y/CL8Y → Pool shows discounted fee; deregister → base fee returns. - `cd frontend-dapp && npm run test:run` covers new Pool + decimals tests. - Optional: `make verify-issue-365` still passes after sharing warning util on Pool. - Reporter checklist comment: query `get_registration` / `get_discount` / CW20 `balance` for their wallet against fee-discount `cl8y_token` before claiming on-chain failure.
PlasticDigits commented 2026-07-12 07:14:42 +00:00 (Migrated from gitlab.com)

mentioned in issue #477

mentioned in issue #477
PlasticDigits commented 2026-07-12 07:14:50 +00:00 (Migrated from gitlab.com)

Investigation notes (bug report triage)

Reporter symptom: Pool CORAL/PEARL shows FEE: 1.80%; claim that CL8Y-cb holding fee reduction is not applied.

Not a missing on-chain feature. Fee discount is live on soft-launch mainnet (registry + pair wiring + production VITE_FEE_DISCOUNT_ADDRESS in dex.cl8y.com bundle; indexer fee-discount health OK).

Most likely explanations for 1.80% badge:

  1. Wallet not registered on /tiers (holding alone does nothing)
  2. Holding a differently named asset (“CL8Y-cb”) that is not cl8y_token
  3. Registered but balance below tier / discount 0

Companion: #477 (token identity docs + indexer.env.example dryrun stubs).

## Investigation notes (bug report triage) **Reporter symptom:** Pool CORAL/PEARL shows `FEE: 1.80%`; claim that CL8Y-cb holding fee reduction is not applied. **Not a missing on-chain feature.** Fee discount is live on soft-launch mainnet (registry + pair wiring + production `VITE_FEE_DISCOUNT_ADDRESS` in `dex.cl8y.com` bundle; indexer fee-discount health OK). **Most likely explanations for 1.80% badge:** 1. Wallet not registered on `/tiers` (holding alone does nothing) 2. Holding a differently named asset (“CL8Y-cb”) that is not `cl8y_token` 3. Registered but balance below tier / discount 0 Companion: #477 (token identity docs + `indexer.env.example` dryrun stubs).
PlasticDigits commented 2026-07-12 07:32:13 +00:00 (Migrated from gitlab.com)

mentioned in commit 408940320d

mentioned in commit 408940320da3abb49fc841d8b7a34c06eea0390f
PlasticDigits commented 2026-07-12 07:32:25 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1013

mentioned in merge request !1013
PlasticDigits commented 2026-07-12 07:44:37 +00:00 (Migrated from gitlab.com)

mentioned in commit 6f8987d028

mentioned in commit 6f8987d02834adb7973925b4053c2c310dfba601
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-07-12 07:44:37 +00:00
PlasticDigits commented 2026-07-12 08:11:02 +00:00 (Migrated from gitlab.com)

Reporter follow-up

My apologies needed to register the wallet in the tier section this is working fine

Confirmed: on-chain + UI discount work after /tiers register. The Pool 1.80% badge was expected for an unregistered wallet, not a broken fee-discount path.

Keeping this issue open as UX / discoverability only (Pool CTA, eligibility copy, outage parity) so the next user does not hit the same confusion. Not a functional defect.

## Reporter follow-up > My apologies needed to register the wallet in the tier section this is working fine **Confirmed:** on-chain + UI discount work after `/tiers` register. The Pool `1.80%` badge was expected for an unregistered wallet, not a broken fee-discount path. Keeping this issue open as **UX / discoverability** only (Pool CTA, eligibility copy, outage parity) so the next user does not hit the same confusion. Not a functional defect.
PlasticDigits commented 2026-07-12 08:11:04 +00:00 (Migrated from gitlab.com)

changed title from fix/ux: Pool fee badge shows base 1.80% — clarify CL8Y fee discount requires register + eligible CW20 to ux: Pool fee badge — surface register CTA / eligibility (reporter confirmed discount works after /tiers)

<p>changed title from <code class="idiff"><span class="idiff left deletion">fix/</span>ux: Pool fee badge <span class="idiff right deletion">shows base 1.80% — clarify CL8Y fee discount requires register + eligible CW20</span></code> to <code class="idiff">ux: Pool fee badge <span class="idiff left right addition">— surface register CTA / eligibility (reporter confirmed discount works after /tiers)</span></code></p>
PlasticDigits commented 2026-08-17 03:45:50 +00:00 (Migrated from gitlab.com)

mentioned in issue #542

mentioned in issue #542
PlasticDigits commented 2026-08-17 10:26:07 +00:00 (Migrated from gitlab.com)

mentioned in issue #547

mentioned in issue #547
PlasticDigits commented 2026-08-26 01:06:40 +00:00 (Migrated from gitlab.com)

mentioned in issue #651

mentioned in issue #651
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#476
No description provided.