fix(frontend): extra-debit Sell submits when TaxPreview.debit exceeds balance #1267

Closed
opened 2026-09-15 14:43:43 +00:00 by PlasticDigits · 4 comments

Summary

Retail listed-pair Sell of a community-tax CW20 still broadcasts when the wallet holds the declared offer amount but not TaxPreview.debit (declared + sell tax). The token wasm fail-closes with Insufficient balance for extra-debit sell tax (InsufficientForSellTax). The user pays gas; balances do not move.

This is T592-2 working as designed on-chain. The defect is the dApp execute path: Swap/Trade treat amount ≤ CW20 balance as fundable, while extra-debit Sell requires debit ≤ balance. Max already has applyExtraDebitSellCap (#593 / taxPreviewMaxSpend.ts). Typed amounts, reverse-quote offer sizing, percentage fill, and the Swap CTA do not use that cap. Wallet/toast copy still shows the raw wasm string (no humanizeTerraTxError map).

Not #1228 (closed: SendFrom allowance vs TaxPreview.debit; crate + ALPHA instance rotate already landed). Not #9 (generic Overflow: Cannot Sub). Not #678 (UST1 acquire theater). Not #593 (Create Token + Max helper — Max exists; submit gate does not). Do not reopen those.

Parent invariants: #592 T592-1/T592-2/T592-13, #607 R607-7 (Max must leave extra-debit room on pair-direct and router hops), #609 manager-directory skip.

Repro (Given / When / Then)

Given a connected wallet holding B of a factory-listed community-tax CW20 (sell_bps > 0, pair registered, wallet not manager-exempt), and Swap / (or Trade market Sell) with that token as You Pay.

When the user sets You Pay to the full displayed balance B (type/paste, not Max) and confirms Swap.

Then the dApp must not broadcast. CTA is Insufficient Balance (or equivalent one-liner that the sell needs extra tokens for sell tax). No signing modal. TaxPreview { from: wallet, to: listed pair, amount: B } has debit > B.

Given the same wallet and token.

When the user taps Max.

Then You Pay is maxDeclaredForExtraDebitSell(B, sell_bps) (LCD TaxPreview.debit ≤ B), Swap is enabled, and execute credits the pair the declared amount (T592-1) while owner debit matches preview.

Given the same setup except sell_bps = 0 or the wallet is manager-directory / manager-role skip (#609 / #633).

When they sell B.

Then extra-debit does not apply; amount ≤ balance remains the fundable test (Honest / skip). Do not cap 100% on exempt wallets after skip is known. Unknown skip stays fail-closed (keep sell_bps, never unlock 100% early — existing effectiveExtraDebitSellBps).

Expected vs actual

Surface Expected Actual
Swap CTA Block when TaxPreview.debit > balance even if declared ≤ balance insufficientBalance is rawInputAmount > balanceQuery.data only (SwapPage.tsx)
Swap Max Cap declared so debit fits (computeMaxSpendableHumanAmount + extraDebitSellBps) Max can cap; typed 100% / reverse-offer / 100% chip still submit
Tax detection Extra-debit Max + gate run for live listed tax wasm on the pay token useCommunityTaxSellBps sets isTax only when LCD code_id === COMMUNITY_TAX_CODE_ID (single env pin). Listed instances on a newer listed id (e.g. 11666 vs a 11630 pin) skip GetConfig / sell_bps; Max and gate behave as 0-tax
Router hop Sell (T592-13) User Sends declared 1:1 to router; extra-debit tax still comes from leftover. Declared must leave tax room (R607-7) Same amount ≤ balance gate; 100% Send leaves 0 leftover → InsufficientForSellTax
Error copy One short sentence; no wasm ContractError / message-index dump (#489 / #371 class) Wallet/raw: failed to execute message; … Insufficient balance for extra-debit sell tax: execute wasm contract failed
Trade market Sell Same debit gate as Swap canSubmit does not compare debit vs escrow/wallet balance

Why this is needed

  1. Extra-debit is the product (T592-2). Pair inbound stays 1:1. Selling 100% of a taxed CW20 is a guaranteed revert. Retail still can submit that size because the CTA only knows CW20 balance.
  2. Live tax wasm is not always the frontend catalog pin. Catalog remains single-id for Create Token / attestation (#594). Extra-debit execute cannot wait for that pin: LCD TaxPreview / GetConfig on the pay contract is the source of truth for this wallet and this pair.
  3. Router hops extra-debit the trader from leftover after a 1:1 Send (T592-13). 100% declared is the same revert class as pair-direct, with a worse mental model (balance looked sufficient).
  4. Raw wasm errors train users to retry Max = 100% and burn gas. Map InsufficientForSellTax and keep the CTA from reaching the wallet.

Constraints / guardrails

ID Rule
H-01 No pair/router FoT / balance-delta swap math. Pair credit on Sell stays declared amount.
T592-1 Inbound listed pair / router / protocol-exempt credit = amount.
T592-2 Do not “fix” by skipping sell tax, shrinking pair credit, or treating Send as 1:1. Extra-debit stays.
T592-7 Provide TransferFrom and limit PlaceLimitOrder* Send stay 1:1. Do not extra-debit those here.
T592-13 / R607-7 Pair-direct and official-router sells need extra-debit room. extraDebitSellBpsForExecute already ignores usesRouter on purpose — keep that.
Preview LCD TaxPreview.debit / credit / tax is the execute source of truth. Local maxDeclaredForExtraDebitSell is an offline helper; it must not disagree with preview on the success path. Do not invent a second bps formula.
Detect Do not require code_id === VITE_COMMUNITY_TAX_CODE_ID to arm extra-debit. That equality is for Create Token catalog / manage chrome. Sell sizing should use LCD preview/config (or a listed tax code set that includes every currently listed tax wasm). Unknown / in-flight: fail closed (do not treat as 0-tax).
Exempt Manager-directory / manager-role skip → 0 extra-debit only when skip is known true. null skip keeps bps.
#678 Keep 5% confirm-again / 30% Expert / 99% / blacklist / pause / freeze / acquire guidance. Do not skip them because “tax token.”
#1255 If pay decimals are unresolved, still no execute (existing hole). Extra-debit gate uses the same raw units as execute.
Copy #489: one short sentence + optional Docs. Hint Sell tax extra already exists (SELL_TAX_EXTRA_HINT). No lecture banner.
Secrets No live spend keys, no drain recipe, no full mainnet instance dump in tests. Truncate addrs in docs.
Ops No host/VM/deploy runbook in this ticket. Frontend env pin updates for Create Token catalog stay a one-line leftover if needed — not the fix.
Wasm Do not change community-tax, pair, or router wasm unless a crate test proves InsufficientForSellTax fires when TaxPreview.debit ≤ balance (that would be a new contract bug; do not assume it).

Relevant files

Path Why
frontend-dapp/src/pages/SwapPage.tsx insufficientBalance vs rawInputAmount / balanceQuery; Max already passes extraDebitSellBpsForExecute
frontend-dapp/src/components/trade/TradeMarketOrderPanel.tsx Market Sell canSubmit / Max; same extra-debit import
frontend-dapp/src/hooks/useCommunityTaxSellBps.ts isTax = code_id === COMMUNITY_TAX_CODE_ID — misses rotated listed wasm
frontend-dapp/src/utils/taxPreviewMaxSpend.ts maxDeclaredForExtraDebitSell / effectiveExtraDebitSellBps / route hint
frontend-dapp/src/utils/maxSpendableAmount.ts Max cap; human round-trip must stay ≤ declared max
frontend-dapp/src/utils/humanizeTerraTxError.ts (or tx humanize sibling) Map Insufficient balance for extra-debit sell tax
frontend-dapp/src/services/terraclassic/communityTaxToken.ts LCD TaxPreview / GetConfig / IsExempt
smartcontracts/contracts/community-tax-token/src/error.rs InsufficientForSellTax string — do not change unless preview/execute diverge
frontend-dapp/src/utils/taxPreviewMaxSpend.test.ts Extend: submit-gate + code_id mismatch + round-trip
docs/frontend.md / skills/AGENTS_COMMUNITY_TAX_CW20.md Extra-debit submit invariant; catalog pin ≠ sell detection
scripts/qa/verify-issue-593.sh Keep extra-debit Max greps green; add this issue’s verify
  1. Pure helper (Vitest-first): sellDebitExceedsBalance({ declaredRaw, balanceRaw, debitRaw }) and declaredRawForMax({ balanceRaw, debitPreview }). Prefer LCD TaxPreview for the live (from=wallet, to=pair or router, amount=declared) once the route is known. Fallback to maxDeclaredForExtraDebitSell(balance, sell_bps) only when preview is unavailable and sell_bps is known; if both unknown, disable Swap (fail closed), do not assume 0-tax.
  2. Swap CTA: insufficientBalance or extra-debit shortfall → same disabled Insufficient Balance (optionally reuse SELL_TAX_EXTRA_HINT as the existing one-liner, not a second banner). Do not broadcast.
  3. Detection: query GetConfig / TaxPreview when the pay asset is a CW20 (factory-listed). Stop using catalog pin equality as the only isTax switch for sell sizing. Create Token / Manage may keep the pin.
  4. Human amount: after Max, toRawAmount(human, decimals) must be ≤ maxDeclared. If format round-trip would exceed, step declared down 1 raw unit until preview debit fits (table test).
  5. Reverse quote / typeable receive (#713 / #1256): derived You Pay still runs the debit gate before execute (offer-in broadcast unchanged).
  6. Trade market Sell: same helper; do not fork a third formula.
  7. Humanize the wasm string to a short retail sentence. Signing modal / toast must not dump message index.

Acceptance criteria

  • AC1. Given listed-pair Sell, sell_bps > 0, non-exempt wallet, declared = full CW20 balance. When they click Swap/Trade. Then no tx is built or signed; CTA disabled.
  • AC2. Max on that wallet produces declared such that LCD TaxPreview.debit ≤ balance and execute succeeds in LocalTerra tax-on (or crate+RTL with mocked preview). Pair credit = declared.
  • AC3. Typing Max’s human string (or 100% of displayed balance) cannot round-trip to a declared whose debit exceeds balance.
  • AC4. Pay token LCD code_id ≠ VITE_COMMUNITY_TAX_CODE_ID but instance still extra-debits (listed tax wasm). Max + CTA still apply extra-debit. No 0-tax 100% sell.
  • AC5. Manager-exempt known true: Max may be 100%; extra-debit gate off. Unknown exempt: still capped.
  • AC6. Router ops.length ≥ 2 listed-tax pay: 100% declared still blocked; Max leaves tax room (R607-7).
  • AC7. Honest / unregistered pair / sell_bps = 0: behavior unchanged vs today (amount ≤ balance only).
  • AC8. On-chain revert InsufficientForSellTax (if a race still hits it) humanizes to one short sentence; no GitLab/Forgejo ids, no CLI.
  • AC9. Provide / limit place / buy-side receive chrome unchanged (T592-7). No pair wasm change.
  • AC10. make verify-issue-593 and extra-debit unit tests stay green. New make verify-issue-<iid>.

Test plan (functional paths)

# Path Expect
T1 Listed tax CW20, sell_bps = 500, balance 1_050_000, type declared 1_050_000 CTA disabled; no simulate execute / no broadcast
T2 Same, Max declared 1_000_000; preview debit 1_050_000
T3 Same, type Max human after format still debit ≤ balance
T4 Direct pair vs router 2-hop both cap extra-debit
T5 code_id 11666 (or any listed tax id) while env pin is another listed id still T1/T2
T6 Manager skip true Max = balance; T1 allowed
T7 Exempt query in flight do not unlock 100%
T8 sell_bps = 0 T1 allowed if amount ≤ balance
T9 Trade market Sell T1/T2 same as Swap
T10 Reverse exactField=output implying offer = 100% balance gate before execute
T11 Native / non-tax CW20 pay extra-debit helper inert
T12 Insufficient CW20 amount > balance still Insufficient Balance (#9 class)
T13 Preview LCD error fail closed; no 0-tax submit

cargo test -p cl8y-community-tax-token only if touching wasm (default: do not). Frontend: Vitest helpers + RTL Swap/Trade CTA. Optional Playwright tax-on LocalTerra Max vs 100% (make verify-issue-622 stay green).

Test plan — attack, hack, and abuse

# Attempt Expect
A1 Skip extra-debit on Swap so 100% sells “work” Forbidden (tax evasion / T592-2)
A2 Credit pair amount - tax (inbound FoT) Forbidden (H-01 / T592-1)
A3 Detect tax only via env pin; rotated listed wasm sells 100% Forbidden (this ticket)
A4 Treat unknown exempt as skip Forbidden (unlock 100% early)
A5 Spoof Swap.trader to extra-debit another wallet Unchanged T592-13; not a frontend bypass
A6 Extra-debit TransferFrom provide Forbidden (T592-7)
A7 Show raw wasm + message index in toast Forbidden (#371 / #489)
A8 Disable #678 slippage / blacklist because tax token Forbidden
A9 Use tokenlist/symbol to decide tax Identity is contract; LCD preview/config only
A10 Race: sell_bps 0 then preview tax Cancel in-flight 0-tax quote; no broadcast at stale 0-tax
A11 Hostile TaxPreview non-numeric debit Unresolved; no execute; no BigInt throw in UI
A12 Retry spam after revert CTA must have blocked; if revert still happens, humanize + no auto-rebroadcast

Do not publish a columbus-5 copy-paste that drains a victim wallet via SendFrom (that is #1228’s closed surface).

Verification criteria

make verify-issue-<iid>
# expected:
# - vitest: maxDeclared / debit-vs-balance gate; env pin ≠ live code_id still caps;
#   human round-trip ≤ maxDeclared; exempt true vs unknown
# - RTL: Swap (and Trade market) 100% typed balance disables CTA; Max enables
# - grep: SwapPage insufficientBalance is not only rawInputAmount > balance
# - grep: InsufficientForSellTax / extra-debit sell tax mapped in humanize
# - make verify-issue-593 / verify-issue-607 extra-debit Max greps still pass

Manual LocalTerra tax-on pair (#623 / #622): Max sell succeeds; typing full balance does not open the wallet. Listed columbus-5 tax instance: same CTA without requiring the catalog pin to match that instance’s code_id.

Out of scope

  • Reopening #1228 / #593 / #1250.
  • Pair/router wasm, factory whitelist, or instance migrate.
  • Buy-side outbound split quote chrome (audit note on #593; not this revert).
  • Charts 24h/7d/30d volume display.
  • Changing VITE_COMMUNITY_TAX_CODE_ID as a substitute for LCD extra-debit detection (pin may still need a docs leftover; it is not the gate).

First-pass model recommendation

Recommendation: grok-high

Rationale: Extra-debit Sell is founder-required wasm/tax (T592-2 / T592-13), not a local three-file UI tweak. A safe first pass must keep pair inbound 1:1, align Swap + Trade submit with LCD TaxPreview (not a second bps), stop using catalog pin equality as the only tax detector, preserve manager skip fail-closed, and avoid tax-evasion “fixes.” Wrong direction bricks sells or skips tax. Verification is table-driven debit math plus RTL CTA, not one snapshot. Composer’s low/medium local-edit bar does not hold (contracts/wasm + cross-surface execute gate).

## Summary Retail listed-pair **Sell** of a community-tax CW20 still broadcasts when the wallet holds the **declared** offer amount but not `TaxPreview.debit` (`declared + sell tax`). The token wasm fail-closes with `Insufficient balance for extra-debit sell tax` (`InsufficientForSellTax`). The user pays gas; balances do not move. This is T592-2 working as designed on-chain. The defect is the dApp execute path: Swap/Trade treat `amount ≤ CW20 balance` as fundable, while extra-debit Sell requires `debit ≤ balance`. Max already has `applyExtraDebitSellCap` (#593 / `taxPreviewMaxSpend.ts`). Typed amounts, reverse-quote offer sizing, percentage fill, and the Swap CTA do not use that cap. Wallet/toast copy still shows the raw wasm string (no `humanizeTerraTxError` map). Not #1228 (closed: `SendFrom` **allowance** vs `TaxPreview.debit`; crate + ALPHA instance rotate already landed). Not #9 (generic `Overflow: Cannot Sub`). Not #678 (UST1 acquire theater). Not #593 (Create Token + Max helper — Max exists; submit gate does not). Do not reopen those. Parent invariants: #592 T592-1/T592-2/T592-13, #607 R607-7 (Max must leave extra-debit room on pair-direct **and** router hops), #609 manager-directory skip. ## Repro (Given / When / Then) Given a connected wallet holding `B` of a factory-listed community-tax CW20 (`sell_bps > 0`, pair registered, wallet not manager-exempt), and Swap `/` (or Trade market Sell) with that token as You Pay. When the user sets You Pay to the **full displayed balance** `B` (type/paste, not Max) and confirms Swap. Then the dApp must not broadcast. CTA is Insufficient Balance (or equivalent one-liner that the sell needs extra tokens for sell tax). No signing modal. `TaxPreview { from: wallet, to: listed pair, amount: B }` has `debit > B`. Given the same wallet and token. When the user taps Max. Then You Pay is `maxDeclaredForExtraDebitSell(B, sell_bps)` (LCD `TaxPreview.debit` ≤ `B`), Swap is enabled, and execute credits the pair the declared amount (T592-1) while owner debit matches preview. Given the same setup except `sell_bps = 0` or the wallet is manager-directory / manager-role skip (#609 / #633). When they sell `B`. Then extra-debit does not apply; `amount ≤ balance` remains the fundable test (Honest / skip). Do not cap 100% on exempt wallets after skip is **known**. Unknown skip stays fail-closed (keep `sell_bps`, never unlock 100% early — existing `effectiveExtraDebitSellBps`). ## Expected vs actual | Surface | Expected | Actual | | --- | --- | --- | | Swap CTA | Block when `TaxPreview.debit > balance` even if `declared ≤ balance` | `insufficientBalance` is `rawInputAmount > balanceQuery.data` only (`SwapPage.tsx`) | | Swap Max | Cap declared so debit fits (`computeMaxSpendableHumanAmount` + `extraDebitSellBps`) | Max can cap; typed 100% / reverse-offer / 100% chip still submit | | Tax detection | Extra-debit Max + gate run for **live** listed tax wasm on the pay token | `useCommunityTaxSellBps` sets `isTax` only when LCD `code_id === COMMUNITY_TAX_CODE_ID` (single env pin). Listed instances on a newer listed id (e.g. 11666 vs a 11630 pin) skip `GetConfig` / `sell_bps`; Max and gate behave as 0-tax | | Router hop Sell (T592-13) | User `Send`s declared 1:1 to router; extra-debit tax still comes from leftover. Declared must leave tax room (R607-7) | Same `amount ≤ balance` gate; 100% Send leaves 0 leftover → `InsufficientForSellTax` | | Error copy | One short sentence; no wasm `ContractError` / message-index dump (#489 / #371 class) | Wallet/raw: `failed to execute message; … Insufficient balance for extra-debit sell tax: execute wasm contract failed` | | Trade market Sell | Same debit gate as Swap | `canSubmit` does not compare debit vs escrow/wallet balance | ## Why this is needed 1. Extra-debit is the product (T592-2). Pair inbound stays 1:1. Selling 100% of a taxed CW20 is a guaranteed revert. Retail still can submit that size because the CTA only knows CW20 `balance`. 2. Live tax wasm is not always the frontend catalog pin. Catalog remains single-id for Create Token / attestation (#594). Extra-debit **execute** cannot wait for that pin: LCD `TaxPreview` / `GetConfig` on the pay contract is the source of truth for this wallet and this pair. 3. Router hops extra-debit the trader from leftover after a 1:1 Send (T592-13). 100% declared is the same revert class as pair-direct, with a worse mental model (balance looked sufficient). 4. Raw wasm errors train users to retry Max = 100% and burn gas. Map `InsufficientForSellTax` and keep the CTA from reaching the wallet. ## Constraints / guardrails | ID | Rule | | --- | --- | | H-01 | No pair/router FoT / balance-delta swap math. Pair credit on Sell stays declared `amount`. | | T592-1 | Inbound listed pair / router / protocol-exempt credit = `amount`. | | T592-2 | Do not “fix” by skipping sell tax, shrinking pair credit, or treating `Send` as 1:1. Extra-debit stays. | | T592-7 | Provide `TransferFrom` and limit `PlaceLimitOrder*` `Send` stay 1:1. Do not extra-debit those here. | | T592-13 / R607-7 | Pair-direct **and** official-router sells need extra-debit room. `extraDebitSellBpsForExecute` already ignores `usesRouter` on purpose — keep that. | | Preview | LCD `TaxPreview.debit` / `credit` / `tax` is the execute source of truth. Local `maxDeclaredForExtraDebitSell` is an offline helper; it must not disagree with preview on the success path. Do not invent a second bps formula. | | Detect | Do not require `code_id === VITE_COMMUNITY_TAX_CODE_ID` to **arm extra-debit**. That equality is for Create Token catalog / manage chrome. Sell sizing should use LCD preview/config (or a listed tax code **set** that includes every currently listed tax wasm). Unknown / in-flight: fail closed (do not treat as 0-tax). | | Exempt | Manager-directory / manager-role skip → 0 extra-debit only when skip is known true. `null` skip keeps bps. | | #678 | Keep 5% confirm-again / 30% Expert / 99% / blacklist / pause / freeze / acquire guidance. Do not skip them because “tax token.” | | #1255 | If pay decimals are unresolved, still no execute (existing hole). Extra-debit gate uses the same raw units as execute. | | Copy | #489: one short sentence + optional Docs. Hint `Sell tax extra` already exists (`SELL_TAX_EXTRA_HINT`). No lecture banner. | | Secrets | No live spend keys, no drain recipe, no full mainnet instance dump in tests. Truncate addrs in docs. | | Ops | No host/VM/deploy runbook in this ticket. Frontend env pin updates for Create Token catalog stay a one-line leftover if needed — not the fix. | | Wasm | Do not change community-tax, pair, or router wasm unless a crate test proves `InsufficientForSellTax` fires when `TaxPreview.debit ≤ balance` (that would be a new contract bug; do not assume it). | ## Relevant files | Path | Why | | --- | --- | | `frontend-dapp/src/pages/SwapPage.tsx` | `insufficientBalance` vs `rawInputAmount` / `balanceQuery`; Max already passes `extraDebitSellBpsForExecute` | | `frontend-dapp/src/components/trade/TradeMarketOrderPanel.tsx` | Market Sell `canSubmit` / Max; same extra-debit import | | `frontend-dapp/src/hooks/useCommunityTaxSellBps.ts` | `isTax = code_id === COMMUNITY_TAX_CODE_ID` — misses rotated listed wasm | | `frontend-dapp/src/utils/taxPreviewMaxSpend.ts` | `maxDeclaredForExtraDebitSell` / `effectiveExtraDebitSellBps` / route hint | | `frontend-dapp/src/utils/maxSpendableAmount.ts` | Max cap; human round-trip must stay ≤ declared max | | `frontend-dapp/src/utils/humanizeTerraTxError.ts` (or tx humanize sibling) | Map `Insufficient balance for extra-debit sell tax` | | `frontend-dapp/src/services/terraclassic/communityTaxToken.ts` | LCD `TaxPreview` / `GetConfig` / `IsExempt` | | `smartcontracts/contracts/community-tax-token/src/error.rs` | `InsufficientForSellTax` string — do not change unless preview/execute diverge | | `frontend-dapp/src/utils/taxPreviewMaxSpend.test.ts` | Extend: submit-gate + code_id mismatch + round-trip | | `docs/frontend.md` / `skills/AGENTS_COMMUNITY_TAX_CW20.md` | Extra-debit submit invariant; catalog pin ≠ sell detection | | `scripts/qa/verify-issue-593.sh` | Keep extra-debit Max greps green; add this issue’s verify | ## Recommended direction 1. Pure helper (Vitest-first): `sellDebitExceedsBalance({ declaredRaw, balanceRaw, debitRaw })` and `declaredRawForMax({ balanceRaw, debitPreview })`. Prefer LCD `TaxPreview` for the live `(from=wallet, to=pair or router, amount=declared)` once the route is known. Fallback to `maxDeclaredForExtraDebitSell(balance, sell_bps)` only when preview is unavailable **and** `sell_bps` is known; if both unknown, disable Swap (fail closed), do not assume 0-tax. 2. Swap CTA: `insufficientBalance` **or** extra-debit shortfall → same disabled Insufficient Balance (optionally reuse `SELL_TAX_EXTRA_HINT` as the existing one-liner, not a second banner). Do not broadcast. 3. Detection: query `GetConfig` / `TaxPreview` when the pay asset is a CW20 (factory-listed). Stop using catalog pin equality as the only `isTax` switch for **sell sizing**. Create Token / Manage may keep the pin. 4. Human amount: after Max, `toRawAmount(human, decimals)` must be `≤ maxDeclared`. If format round-trip would exceed, step declared down 1 raw unit until preview debit fits (table test). 5. Reverse quote / typeable receive (#713 / #1256): derived You Pay still runs the debit gate before execute (offer-in broadcast unchanged). 6. Trade market Sell: same helper; do not fork a third formula. 7. Humanize the wasm string to a short retail sentence. Signing modal / toast must not dump `message index`. ## Acceptance criteria - AC1. Given listed-pair Sell, `sell_bps > 0`, non-exempt wallet, declared = full CW20 balance. When they click Swap/Trade. Then no tx is built or signed; CTA disabled. - AC2. Max on that wallet produces declared such that LCD `TaxPreview.debit ≤ balance` and execute succeeds in LocalTerra tax-on (or crate+RTL with mocked preview). Pair credit = declared. - AC3. Typing Max’s human string (or 100% of displayed balance) cannot round-trip to a declared whose debit exceeds balance. - AC4. Pay token LCD `code_id` ≠ `VITE_COMMUNITY_TAX_CODE_ID` but instance still extra-debits (listed tax wasm). Max + CTA still apply extra-debit. No 0-tax 100% sell. - AC5. Manager-exempt known true: Max may be 100%; extra-debit gate off. Unknown exempt: still capped. - AC6. Router `ops.length ≥ 2` listed-tax pay: 100% declared still blocked; Max leaves tax room (R607-7). - AC7. Honest / unregistered pair / `sell_bps = 0`: behavior unchanged vs today (`amount ≤ balance` only). - AC8. On-chain revert `InsufficientForSellTax` (if a race still hits it) humanizes to one short sentence; no GitLab/Forgejo ids, no CLI. - AC9. Provide / limit place / buy-side receive chrome unchanged (T592-7). No pair wasm change. - AC10. `make verify-issue-593` and extra-debit unit tests stay green. New `make verify-issue-<iid>`. ## Test plan (functional paths) | # | Path | Expect | | --- | --- | --- | | T1 | Listed tax CW20, `sell_bps = 500`, balance `1_050_000`, type declared `1_050_000` | CTA disabled; no `simulate` execute / no broadcast | | T2 | Same, Max | declared `1_000_000`; preview debit `1_050_000` | | T3 | Same, type Max human after format | still debit ≤ balance | | T4 | Direct pair vs router 2-hop | both cap extra-debit | | T5 | `code_id` 11666 (or any listed tax id) while env pin is another listed id | still T1/T2 | | T6 | Manager skip true | Max = balance; T1 allowed | | T7 | Exempt query in flight | do not unlock 100% | | T8 | `sell_bps = 0` | T1 allowed if amount ≤ balance | | T9 | Trade market Sell T1/T2 | same as Swap | | T10 | Reverse `exactField=output` implying offer = 100% balance | gate before execute | | T11 | Native / non-tax CW20 pay | extra-debit helper inert | | T12 | Insufficient CW20 `amount > balance` | still Insufficient Balance (#9 class) | | T13 | Preview LCD error | fail closed; no 0-tax submit | `cargo test -p cl8y-community-tax-token` only if touching wasm (default: do not). Frontend: Vitest helpers + RTL Swap/Trade CTA. Optional Playwright tax-on LocalTerra Max vs 100% (`make verify-issue-622` stay green). ## Test plan — attack, hack, and abuse | # | Attempt | Expect | | --- | --- | --- | | A1 | Skip extra-debit on Swap so 100% sells “work” | Forbidden (tax evasion / T592-2) | | A2 | Credit pair `amount - tax` (inbound FoT) | Forbidden (H-01 / T592-1) | | A3 | Detect tax only via env pin; rotated listed wasm sells 100% | Forbidden (this ticket) | | A4 | Treat unknown exempt as skip | Forbidden (unlock 100% early) | | A5 | Spoof `Swap.trader` to extra-debit another wallet | Unchanged T592-13; not a frontend bypass | | A6 | Extra-debit `TransferFrom` provide | Forbidden (T592-7) | | A7 | Show raw wasm + message index in toast | Forbidden (#371 / #489) | | A8 | Disable #678 slippage / blacklist because tax token | Forbidden | | A9 | Use tokenlist/symbol to decide tax | Identity is contract; LCD preview/config only | | A10 | Race: sell_bps 0 then preview tax | Cancel in-flight 0-tax quote; no broadcast at stale 0-tax | | A11 | Hostile `TaxPreview` non-numeric debit | Unresolved; no execute; no `BigInt` throw in UI | | A12 | Retry spam after revert | CTA must have blocked; if revert still happens, humanize + no auto-rebroadcast | Do not publish a columbus-5 copy-paste that drains a victim wallet via `SendFrom` (that is #1228’s closed surface). ## Verification criteria ```text make verify-issue-<iid> # expected: # - vitest: maxDeclared / debit-vs-balance gate; env pin ≠ live code_id still caps; # human round-trip ≤ maxDeclared; exempt true vs unknown # - RTL: Swap (and Trade market) 100% typed balance disables CTA; Max enables # - grep: SwapPage insufficientBalance is not only rawInputAmount > balance # - grep: InsufficientForSellTax / extra-debit sell tax mapped in humanize # - make verify-issue-593 / verify-issue-607 extra-debit Max greps still pass ``` Manual LocalTerra tax-on pair (#623 / #622): Max sell succeeds; typing full balance does not open the wallet. Listed columbus-5 tax instance: same CTA without requiring the catalog pin to match that instance’s `code_id`. ## Out of scope - Reopening #1228 / #593 / #1250. - Pair/router wasm, factory whitelist, or instance migrate. - Buy-side outbound split quote chrome (audit note on #593; not this revert). - Charts 24h/7d/30d volume display. - Changing `VITE_COMMUNITY_TAX_CODE_ID` as a substitute for LCD extra-debit detection (pin may still need a docs leftover; it is not the gate). ## First-pass model recommendation Recommendation: grok-high Rationale: Extra-debit Sell is founder-required wasm/tax (T592-2 / T592-13), not a local three-file UI tweak. A safe first pass must keep pair inbound 1:1, align Swap + Trade submit with LCD `TaxPreview` (not a second bps), stop using catalog pin equality as the only tax detector, preserve manager skip fail-closed, and avoid tax-evasion “fixes.” Wrong direction bricks sells or skips tax. Verification is table-driven debit math plus RTL CTA, not one snapshot. Composer’s low/medium local-edit bar does not hold (contracts/wasm + cross-surface execute gate).
Author
Owner

/agent implement

/agent implement
Author
Owner

cl8y-agent-control: queued implement job 96528e6e-8429-4767-8943-6054dfcfcd6a (not executed; no Hetzner VM).

cl8y-agent-control: queued `implement` job `96528e6e-8429-4767-8943-6054dfcfcd6a` (not executed; no Hetzner VM).
Author
Owner

Merge complete: PR #1270 landed on main (ce5f9804).

Met: AC1 typed 100% extra-debit Sell is blocked. extraDebitSubmitGate now uses max(LCD debit, local extra-debit from sell_bps) when sell_bps > 0, so Honest TaxPreview (debit === declared) cannot enable a full-balance tax sell. make verify-issue-1267 passed (76 Vitest), including that Honest shape. Woodpecker on the PR was green.

Leftover (not this close bar): pair-direct TaxPreview still omits Swap send_msg; router still ignores hop_trader_debit and relies on the local floor. Inline TxResultAlert can still show raw error.message (toast humanize is mapped). Tracking in a follow-up issue.

Merge complete: PR #1270 landed on main (`ce5f9804`). **Met:** AC1 typed 100% extra-debit Sell is blocked. `extraDebitSubmitGate` now uses `max(LCD debit, local extra-debit from sell_bps)` when `sell_bps > 0`, so Honest TaxPreview (`debit === declared`) cannot enable a full-balance tax sell. `make verify-issue-1267` passed (76 Vitest), including that Honest shape. Woodpecker on the PR was green. **Leftover (not this close bar):** pair-direct TaxPreview still omits Swap `send_msg`; router still ignores `hop_trader_debit` and relies on the local floor. Inline `TxResultAlert` can still show raw `error.message` (toast humanize is mapped). Tracking in a follow-up issue.
Author
Owner

Follow-up leftover: #1285 (send_msg / hop_trader_debit / inline humanize).

Follow-up leftover: #1285 (send_msg / hop_trader_debit / inline humanize).
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#1267
No description provided.