Community tax CW20 template: DEX-safe buy/sell/transfer tax, paid SKUs, CMM-only migrate #592

Closed
opened 2026-08-22 10:59:03 +00:00 by PlasticDigits · 53 comments
PlasticDigits commented 2026-08-22 10:59:03 +00:00 (Migrated from gitlab.com)

Parent

Community tax / trading-tax CW20 for sovereign communities. Sibling issues: dApp create/manage and indexer catalog (link after open).

Payment (amendment)

Two on-chain invoices, both 50 UST1 (6 dp → 50000000) per unit, paid to CMM treasury terra16j5u6… in UST1. Users may pay in any routable token via #595; the token/launcher still only accepts UST1 Send.

Invoice When Amount
Unlock SKU Instantiate or later EnableFeature (except MintControl = instantiate-only) 50 UST1 × SKU count
Change settings (batch) One post-instantiate manager save / execute that mutates config 50 UST1 flat — covers the whole batch

Batch (not per-field): one paid tx may update any number of already activated settings together (e.g. buy + sell + treasury + exempt list in one Send). Invoice is always 50 UST1 for that tx, not 50 × N keys.

Already activated only: the batch may include:

  • Free-profile settings (activated at instantiate): buy_bps, sell_bps, single treasury
  • Settings whose SKU is already unlocked: transfer_bps (TransferTax), sinks/ratios (SplitRouter), exemptions (ExemptionDirectory), UpdateTaxBps beyond instantiate-fixed if VariableRates is on, AutoLP pair/threshold/LP recipient (AutoV2Lp), launch guards, UpdateMinter / RevokeMint (MintControl)

Including a field for a SKU that is not unlocked → fail the whole batch (no partial apply, fee not kept). Do not combine EnableFeature (SKU unlock) into a settings batch; that is a separate 50 UST1 invoice. Unlock first, then change in a later batch (or two msgs / two invoices in one Cosmos tx if the dApp sequences unlock then batch — still two 50 UST1 charges).

Not a settings invoice: initial instantiate params; EnableFeature itself; permissionless SkimToLp; Mint (issuing supply); RegisterListedPair if permissionless + factory-verified.

No-op (every submitted value identical to on-chain) must fail closed and not take UST1. Invalid update reverts the fee. No refunds on a successful batch.

Free create profile remains 0 UST1 SKU cost (gas only) until a SKU is bought or a settings batch is submitted.

Problem statement

Communities on Terra Classic cannot launch a first-party buy / sell / transfer tax token that is safe to list on this DEX. Today the only listed CW20 templates are 1:1 mintable/standard (e.g. columbus-5 10184, 6036). Fee-on-transfer (FoT) templates are explicitly forbidden from the factory whitelist because the pair credits declared CW20 amounts, not balance deltas (invariants H-01, P2). There is no launcher, no paid-feature SKU, no manager-vs-upgrade split, and no Auto V2 LP helper.

This issue is the on-chain design and implementation: token template, optional sister contracts (tax splitter / AutoLP / hook), paid feature SKUs (50 UST1 each), CMM-only wasm upgrade, and listing compatibility without upgrading pair/router swap math.

Current codebase

DEX will not (and must not) grow FoT math

  • Pair / limit escrow credit declared Transfer / Send amounts, not balanceOf deltas. Inbound FoT (recipient credit < declared amount on transfers to the pair) desyncs RESERVES / PENDING_ESCROW (P2). Documented by adversarial_token::fee_on_transfer_creates_reserve_imbalance and Layer B layer_b_b1_fot_desyncs_reserves.
  • Ops policy: docs/runbooks/cw20-whitelist-policy.md — never whitelist templates whose recipient credit can differ from the declared amount. Playbook skills/AGENTS_HOOK_CW20_OPS.md H-01: do not add pair balance-delta / FoT swap math.
  • Post-listing migrate onto FoT/rebase wasm is already fail-closed by F6 (code_id pin + IsCodeIdWhitelisted re-check, #582 / #584). Listing-time FoT remains an ops/harness gate (#589).

What exists today (reuse, do not fork the pair)

Piece Role Gap
cw20-mintable (git dep) Honest mintable CW20 (10184 analogue) No tax, no feature SKUs, minter is not a paid community product
Factory whitelisted_code_ids / AddWhitelistedCodeId Listing gate Governance-only; no community-tax code_id
Factory SetPairHooks Register AfterSwap hooks Governance-only — a community cannot self-register a tax hook on its pair
cl8y-dex-tax-hook / burn-hook Ask-side settlement (I-02): pair forwards % of ask to recipient / burn Single recipient, single bps, admin-not-manager, one tax_token; no sell-as-offer, no ratios, no AutoLP
dex-common::hook_settlement Pair queries hook GetConfig, splits ask New hook shapes need a compatible GetConfig or stay off the AfterSwap path
#589 harness + cw20_mutants.rs FoT / rebase / honeypot detectors A new template must go green on pair-touching 1:1 (inbound) and must not look like mutant 8654
Faucet /mint + CreatePair /create Soft-launch drip; permissionless pair create (100 LUNC) No create token flow

Upgrade / admin split today

  • DEX stack wasm admin / factory governance: terra1zlmv2xydxcusurtr6rl78wsvytdc6mfex6hep7 (docs/reference/governance-multisig.md).
  • Wrap-stack CMM governance (UST1 / wrap-mapper / treasury pause): terra1xsecn4snv94ezcez0z3vq8an9j4h4kxxcydp8l (deployments/mainnet-ust1-wrap/REGISTRY.md).
  • CMM treasury (fee sink, wrap custody): terra16j5u6ey7a84g40sr3gd94nzg5w5fm45046k9s2347qhfpwm5fr6sem3lr2 — not a wasm admin.

Anyone can instantiate a stored code_id with an arbitrary --admin. F6 freezes listed pairs if the instance migrates off the pinned template; it does not stop an unlisted rug.

Why needed

Sovereign communities want their own token with a trusted leader (manager) who can tune treasury routing, exemptions, and (optionally) mint — without the DEX becoming a general FoT AMM and without that leader being able to MsgMigrateContract to a hidden-mint / honeypot wasm.

This is a product (PinkSale / Pancake “liquidity generator” style) adapted to this DEX’s accounting: taxes that would desync reserves are implemented so every transfer that credits the pair, router, or limit escrow remains 1:1.

Research: features successful tax tokens actually used

Use this as a catalog to steal from or reject, not as a mandate to clone SafeMoon.

Feature Who used it Why it “worked” Verdict for this DEX
Buy tax / sell tax (separate bps) SafeMoon-class, PinkSale LGT, many BSC “baby” tokens Marketing + LP + burn from volume Yes — see architecture (buy = outbound split; sell = extra-debit or ask-hook)
Wallet-to-wallet transfer tax Same family; also some “anti-dump” coins Taxes OTC / CEX deposit paths Paid SKU — never on protocol addresses
Auto liquidity (AutoV2LP) SafeMoon, BabyDoge, PinkSale LGT Sells half of tax into the quote and provide_liquidity Paid SKU — deferred keeper, never re-enter the pair inside Transfer/AfterSwap
Treasury / marketing wallet FLOKI (early), almost every tax token Funds community ops Free (single sink). Multi-sink ratios = paid
Holder reflection / rebase SafeMoon, Reflect.finance, Olympus-style “Earn by holding” Forbidden — idle balance mutation breaks P2 / Layer A
Burn slice of tax Many Narrative deflation Paid SKU (part of split-router)
Exempt / whitelist wallets Universal (owner, LP, CEX, routers) Avoid taxing CEX deposits; avoid breaking DEX Protocol list always-on; manager directory = paid
Mint + revoke mint Honest CW20s (10184); rugs hide mint Genesis + “fair” later Paid SKU; revoke is one-way
Owner can change tax bps Almost all; rugs set 99% Tune after launch; CEX listing often drops tax to 0 (FLOKI) Paid SKU with hard caps; decreases always allowed
Max wallet / max tx / cooldown Anti-snipe at launch Bot friction Optional paid; easy to become a honeypot — cap + documentation
Trading enabled flag / sniper block Launch tools Block buys until LP is live Optional paid; must not block exits once trading is on
Blacklist / pause trading AML and honeypots Pause-to-rug is a top rug vector Do not ship as manager methods. Pause/blacklist stay DEX factory/pair if needed
Hidden mint / tax map after listing 8654 ALPHA-class, mutants A4/A12 Rug Harness must fail these
0% tax (PEPE, SHIB, later FLOKI) Memes that listed on CEX Compatibility Manager must be able to set taxes to 0
LP lock / burn LP Trust wallets, PinkSale lockers “Rug-proof LP” Out of scope for v1 (no locker in-repo). AutoLP can mint LP to a burn address or CMM as a config
Creator rewards from curve (pump.fun) Solana memes Not a transfer tax Out of scope
Native LUNC burn tax Terra Classic chain Protocol-level Orthogonal; do not emulate inside CW20

Extra ideas worth considering (not all v1)

  • Tax-increase timelock (e.g. 24h) so holders can exit before a raise.
  • Instantiate-time max bps that manager can never exceed (honeypot brake).
  • Manager handover / 2-step accept (community leader succession).
  • Primary-pair auto-register: permissionless RegisterListedPair { pair } that queries factory pair and only then treats that address as buy/sell.
  • Sell tax in quote asset via a sister AfterSwap hook (economically a sell tax in UST1/cLUNC) — only if hook wiring is solved without pair math changes.
  • No reflection, no elastic supply, no “rewards in another token” inside Transfer.

Constraints / guardrails

  1. Do not upgrade pair/router swap math. No balance-delta reconciliation. No exception to H-01 for generic FoT.
  2. Inbound 1:1 (hard): Transfer / Send / TransferFrom / SendFrom to pair, router, limit-related escrow, AutoLP, tax splitter, and this token itself must credit the recipient exactly amount. Layer A/B and fee_on_transfer_creates_reserve_imbalance analogue must stay green for those paths.
  3. Outbound buy tax is allowed: when from is a registered listed pair, debit amount from pair, credit amount - buy_tax to the trader, credit buy_tax to sinks. Pair reserves still decrement by amount → P2 holds.
  4. Sell tax without inbound FoT (pick one, document):
    • Preferred (no DEX upgrade): extra-debit — sender is debited amount + sell_tax (or ceil equivalent), pair is credited exactly amount. dApp must size max-sell. Fails closed if balance insufficient.
    • Alternative (sister hook, no pair math): AfterSwap skim of ask when offer is the tax token (quote-denominated sell tax). Requires hook registration (see options below).
    • Forbidden: credit pair amount * (1 - sell_bps) while pair accounts amount.
  5. No rebase / reflection / idle balance growth.
  6. Management execute msgs = manager only. Wasm --admin / MsgMigrateContract / UpdateAdmin = CMM governance only terra1xsecn4snv94ezcez0z3vq8an9j4h4kxxcydp8l (not DEX terra1zlmv2…, not CMM treasury terra16j5u6…, not the token manager). Hardcode expected admin in launcher docs; dApp/indexer refuse to promote instances whose LCD ContractInfo.admin ≠ CMM.
  7. Anyone can still instantiate the code_id with a rogue --admin. Mitigate: launcher path + catalog filter (admin == CMM and created_by_launcher). F6 still freezes listed pairs on migrate-off-template.
  8. Do not give manager Pause, Blacklist, or SetTax(100%) unbounded. Instantiate max_buy_bps / max_sell_bps / max_transfer_bps (recommend combined cap ≤ 2500 bps unless product sets otherwise). Raising above current bps may require the variable-rates SKU; lowering to 0 is always allowed if that SKU is on, or only at instantiate if not.
  9. Mint SKU is instantiate-only. Cannot buy “mint” later for a token marketed as fixed supply.
  10. AutoLP must not re-enter the pair in the same tx as the taxed transfer/swap. Threshold + permissionless SkimToLp. AutoLP contract is protocol-exempt.
  11. Factory whitelist of this code_id happens only after #589 REPORT GO (Layer A 1:1 on protocol paths + Layer B P2 green). Update whitelist runbook with a narrow exception: this named template, not “tax tokens in general”.
  12. SetPairHooks stays governance-only unless a later issue explicitly adds a minimal factory method. Prefer extra-debit sell tax so communities do not wait on governance to tax sells.
  13. Paid SKUs: 50 UST1 per SKU as the on-chain invoice. User may pay in any routable token via #595; launcher/token still receives UST1. Forwarded to CMM treasury. No refunds. Idempotent enable.
  14. Manager settings batch fee: one post-instantiate config Send costs 50 UST1 flat and may mutate all already-activated settings in that batch (see Payment amendment). Implement as UST1 Send + hook (or launcher UpdateTokenSettings) so fee and mutations are atomic. Reject any key whose SKU is not unlocked. Manager-only; unpaid / underpaid / no-op → no state change. Do not charge this fee on Mint or permissionless SkimToLp.
  15. Gas: no unbounded exemption maps in Transfer (pagination for queries; Map lookups in execute are O(1) per address).

Sister-hook options (do not pick silently)

Option DEX upgrade? Sell tax AutoLP Recommendation
A. Token-only (protocol-exempt + extra-debit sell + outbound buy) No In tax token Sister splitter contract, not AfterSwap Default v1
B. Token + existing tax-hook shape No math; still needs SetPairHooks Ask skim (buy if tax token is ask; sell if we extend config) No Only if CMM will register hooks per pair (ops-heavy)
C. Minimal factory SetPairHooks for token manager + canonical hook code_id Small factory execute Clean ask-side sell tax Hook still must not re-enter Follow-up only; out of v1 unless product insists
D. Pair FoT math Yes Classic inbound FoT — Rejected

Relevant files

Area Paths
Pair accounting smartcontracts/contracts/pair/src/contract.rs, asset_code_id_guard.rs, lib.rs (P2 / F6 comments)
Factory whitelist / hooks smartcontracts/packages/dex-common/src/factory.rs, smartcontracts/contracts/factory/src/contract.rs
Hook settlement smartcontracts/packages/dex-common/src/hook_settlement.rs, smartcontracts/contracts/hooks/tax-hook/, hooks/README.md
Adversarial / harness smartcontracts/tests/src/adversarial_token.rs, cw20_mutants.rs, cw20_codeid_harness.rs, cw20-codeid-audits/
Policy docs/runbooks/cw20-whitelist-policy.md, docs/contracts-security-audit.md (H-01, P2, F6, I-02), skills/AGENTS_HOOK_CW20_OPS.md, skills/AGENTS_CW20_CODE_ID_PIN.md, skills/AGENTS_CW20_CODE_ID_AUDIT.md
CMM ids docs/reference/governance-multisig.md, deployments/mainnet-ust1-wrap/REGISTRY.md
New (this issue) smartcontracts/contracts/community-tax-token/ (name TBD), optional community-tax-splitter/ / community-tax-autolp/, optional community-token-launcher/; workspace smartcontracts/Cargo.toml

1. community-tax-token CW20 (new crate)

Honest CW20-base surface (transfer, send, increase_allowance, queries) plus:

  • Roles: manager (execute config); no in-contract migrate. Wasm admin = CMM at instantiate (launcher sets it).
  • Free profile: name, symbol, decimals (cap 18, factory bootstrap), initial_supply minted to genesis_recipient, fixed buy_bps / sell_bps to one treasury addr, protocol exemption set, manager = chosen leader.
  • Protocol exemption: always includes registered listed pairs (via RegisterListedPair validating factory), DEX router, this contract, splitter/AutoLP if instantiated. Manager cannot remove protocol entries.
  • Buy: if from ∈ pairs and to not exempt → outbound split (guardrail 3).
  • Sell: extra-debit when to ∈ pairs (guardrail 4).
  • Transfer tax: if paid SKU and neither side protocol-exempt.
  • Queries: GetConfig, GetFeatures, GetExemptions, IsProtocolExempt, Minter, TaxPreview { from, to, amount } (dApp max-spend).

community-token-launcher (or factory-adjacent crate):

  • Holds token_code_id, optional splitter/AutoLP code ids, ust1 addr, cmm_treasury, cmm_governance (wasm admin to stamp).
  • CreateToken { …, features: Vec<Sku> } with funds / UST1 Send = 50 * len(paid_skus) UST1.
  • Instantiates token with --admin CMM (via submessage instantiate-with-admin if available; otherwise CosmWasm instantiate admin = launcher then update-admin to CMM in the same tx — must not leave launcher as admin).
  • Emits wasm events: community_token, sku, manager, code_id for the indexer.
  • EnableFeature { token, sku } later (except mint SKU).

3. Paid SKU map (50 UST1 each; group methods)

SKU Methods / behavior (one price for the group)
MintControl Mint, UpdateMinter, one-way RevokeMint. Instantiate-only purchase. Optional cap.
TransferTax Distinct transfer_bps vs buy/sell.
SplitRouter Ratios summing to 10000 toward treasury / burn / autolp / extra wallets (max N sinks, e.g. 4).
AutoV2Lp Instantiates AutoLP; SetPrimaryPair; permissionless SkimToLp; LP to burn, manager, or CMM — config at enable.
ExemptionDirectory Manager AddExempt / RemoveExempt (cannot remove protocol).
VariableRates Manager UpdateTaxBps within instantiate max; 0 always allowed. Optional timelock on increases.
LaunchGuards (optional v1) max_wallet, cooldown_blocks, trading_enabled (default on unless set). No seller blacklist.

Free remains usable with zero UST1 besides gas at create: fixed buy/sell to one wallet + initial supply. One later save of those free-profile settings costs 50 UST1 for the batch (even if several fields change). Unlocking VariableRates/ExemptionDirectory/etc. is a separate 50 UST1 SKU; after that SKU is on, those fields may be included in a settings batch (still 50 UST1 for the whole batch, not another per-field charge).

4. AutoV2LP sister

Not pair AfterSwap. Receives tax 1:1 (exempt). When token_balance ≥ threshold, SkimToLp: swap half via router or pair (new tx / later tx), provide_liquidity, mint LP to configured sink. Tests: reentrancy during taxed Send must not call pair; skim after swap succeeds.

5. Listing

After harness GO: governance AddWhitelistedCodeId. Runbook exception paragraph. Do not whitelist 8654 or mutants. #589 catalogue rows for the new id.

Acceptance criteria

  • New CW20 crate + launcher (or documented instantiate path) with free profile and paid SKUs at 50 UST1 each, paid to CMM treasury.
  • Post-instantiate manager settings batch charges 50 UST1 flat for one atomic save of already-activated settings; unpaid/no-op/non-manager/unactivated-SKU keys fail without taking fee.
  • Manager-only config/mint; non-manager execute → Unauthorized.
  • LCD ContractInfo.admin on launcher-created tokens = CMM governance; manager cannot migrate or update-admin.
  • Inbound transfers to pair/router/escrow/splitter/AutoLP are 1:1; Layer A/B P2 green; classic FoT test still fails a naïve inbound-tax mutant.
  • Buy tax outbound and sell extra-debit (or documented hook alternative) match TaxPreview.
  • Mint SKU cannot be enabled post-instantiate; RevokeMint is irreversible.
  • AutoLP does not re-enter pair inside Transfer/Send/AfterSwap.
  • No reflection/rebase/pause/blacklist manager APIs.
  • #589 REPORT template filled; factory whitelist is a separate ops step gated on GO.
  • Pair/router wasm unchanged for swap math (factory whitelist add is ops, not this MR unless Option C is explicitly chosen).
  • Docs: runbook + security-audit row + playbook; make verify-issue-NNN.

Test plan (all paths)

Unit / multi-test (smartcontracts):

  • Instantiate free profile: supply, bps, treasury, manager, CMM admin.
  • Buy: pair → user outbound split; pair balance − amount; user + treasury + sinks = amount.
  • Sell: user → pair extra-debit; pair + amount exactly; seller debit = amount + tax.
  • Sell with insufficient balance for extra-debit → fail closed.
  • Transfer tax SKU off: wallet↔wallet 1:1; on: tax to sinks; exempt wallets skip.
  • Protocol exempt cannot be removed by manager.
  • RegisterListedPair: factory-listed pair OK; random addr rejected; spoof pair rejected.
  • Each SKU: unpaid → execute rejected; pay 50 UST1 → enabled; second pay idempotent (no double-charge or reject cleanly).
  • MintControl: mint as minter; non-minter fail; revoke; mint after revoke fail; SKU absent at instantiate → no mint forever.
  • VariableRates: within cap OK; above instantiate max fail; to 0 OK; without 50 UST1 batch invoice → reject; buy+sell in one batch → still 50 UST1 (not 100).
  • Settings no-op (all values unchanged) with 50 UST1 attached → revert, UST1 not kept.
  • Free-profile batch after instantiate requires 50 UST1 once; SKU unlock is a separate invoice.
  • Batch that includes transfer_bps while TransferTax SKU is off → revert entire batch.
  • Mint succeeds without settings invoice (SKU only); RevokeMint / UpdateMinter require an activated MintControl SKU and a 50 UST1 settings batch.
  • SkimToLp remains permissionless and free.
  • SplitRouter: ratios ≠ 10000 fail; burn sink reduces supply; dust rounding (last sink gets remainder).
  • AutoLP: accumulate below threshold no-op; SkimToLp adds LP; during Send no nested swap.
  • Limit place/fill/cancel/claim with tax token as asset: escrow 1:1 (L1).
  • Provide/withdraw 1:1 inbound/outbound-from-pair (withdraw is pair → LP holder: decide if buy tax applies to withdraw; recommended: exempt withdraw / LP paths — treat pair→user as buy tax only for swap Send with Swap msg, not all pair sends). Must specify: prefer swap-direction detection via Send msg / skip tax if to is LP provider path. If detection is fragile, exempt all pair↔user transfers except those originating from swap receive — document chosen rule and test provide/withdraw/limit untaxed.
  • Hybrid swap: book + pool legs; no reserve desync.
  • Router multi-hop: intermediate pair hops 1:1.
  • F6: migrate instance off template → pair write paths fail; Refresh refuses FoT replacement.

Harness: new cw20-codeid-audits/codeids/<id>/ after store; LAYER_B_LT=1 make verify-issue-589.

LocalTerra: launcher create, CreatePair, swap buy/sell, extra-debit max button, AutoLP skim.

Attack / abuse / hack test plan

  • Inbound FoT sneak: mutant that taxes to == pair — must be rejected by harness (P2 red). Our template must not match.
  • Honeypot 99% tax: instantiate max cap blocks; VariableRates cannot exceed cap.
  • Hidden mint: no mint without SKU; no second minter backdoor (mutant A12 analogue).
  • Manager migrate: manager MsgMigrateContract rejected (not wasm admin); CMM migrate to same family wasm only in ops (F6 refresh). Test manager cannot UpdateAdmin.
  • Rogue instantiate with attacker admin: catalog must not list; CreatePair still possible if code_id whitelisted — F6 freeze on their later migrate. Consider launcher-attestation query GetLauncherOrigin so dApp warns “unverified admin”.
  • Exemption grief: manager exempts a bot from buy tax — product-accepted; cannot un-exempt pair to break DEX.
  • Permissionless RegisterListedPair: cannot register a non-factory contract to trigger fake “buy” taxes on random sends.
  • Reentrancy: Send to contract that calls back Transfer / SkimToLp / pair swap in the same tx — fail or no double-tax / no reserve desync.
  • Extra-debit drain: sell tax computed so pair never receives less than amount; rounding cannot create extra tokens.
  • UST1 payment spoof: enabling SKU or changing a setting with wrong token / wrong amount / 0 → reject; do not trust memo-only.
  • Settings fee bypass: manager UpdateTaxBps via plain execute without UST1 Send → Unauthorized / FeeRequired; cannot pay in the tax token unless routed to UST1 via #595 (launcher/token invoice token is UST1 only).
  • Wrong batch invoice: two keys with 0 or 25 UST1 → revert both; 100 UST1 for one batch → reject excess (prefer exact 50; do not silently keep a tip unless documented). Unactivated SKU field in the batch → revert, fee not kept.
  • Feature downgrade rug: disabling SKUs (if allowed) must not trap funds in AutoLP; prefer enable-only.
  • Max-wallet + tax: user cannot be bricked such that they cannot sell (sell to pair should bypass max-wallet).
  • Cooldown / trading_enabled: once on, manager cannot selectively block sells (no blacklist). If trading_enabled=false, both buy and sell blocked, or document; do not ship buy-only-on.
  • Event lying: Transfer attributes must match actual credits (mutant D16).
  • Router / zap / one-sided LP (#533): all protocol contracts that hold the token must be exempt or 1:1. Audit wrap-mapper, router, pair, AutLP.
  • Limit book: maker place inbound 1:1; take outbound: decide tax (prefer no buy tax on limit fill to maker/taker beyond swap hook — test both sides).
  • DoS: huge exemption Map; query pagination only.
  • Phishing dApp: not on-chain; frontend issue covers copy.

Verification criteria

  • cd smartcontracts && cargo test -p <token-crate> -p <launcher-crate> and DEX suite adversarial + cw20_codeid_harness still pass for mintable and fail known-bad FoT.
  • make test-contracts; new make verify-issue-NNN.
  • Docs: whitelist runbook names this template as the only tax exception and restates inbound 1:1.
  • No pair/router FoT math diff in the implementing MR.
  • Manual LocalTerra: create free token → pair → buy/sell; pay SKU → mint → revoke; CMM-only migrate probe.

Labels / owner / priority

contracts architecture enhancement · Owner: contracts · Priority: P2

## Parent Community tax / trading-tax CW20 for sovereign communities. Sibling issues: **dApp create/manage** and **indexer catalog** (link after open). ## Payment (amendment) Two on-chain invoices, both **50 UST1** (6 dp → `50000000`) per unit, paid to CMM treasury `terra16j5u6…` in UST1. Users may pay in **any routable token** via [**#595**](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/595); the token/launcher still **only accepts UST1** `Send`. | Invoice | When | Amount | |---------|------|--------| | **Unlock SKU** | Instantiate or later `EnableFeature` (except MintControl = instantiate-only) | **50 UST1 × SKU count** | | **Change settings (batch)** | One post-instantiate manager **save / execute** that mutates config | **50 UST1 flat** — covers **the whole batch** | **Batch (not per-field):** one paid tx may update any number of **already activated** settings together (e.g. buy + sell + treasury + exempt list in one `Send`). Invoice is **always 50 UST1** for that tx, not `50 × N keys`. **Already activated only:** the batch may include: - **Free-profile** settings (activated at instantiate): `buy_bps`, `sell_bps`, single `treasury` - Settings whose **SKU is already unlocked**: `transfer_bps` (TransferTax), sinks/ratios (SplitRouter), exemptions (ExemptionDirectory), `UpdateTaxBps` beyond instantiate-fixed if VariableRates is on, AutoLP pair/threshold/LP recipient (AutoV2Lp), launch guards, `UpdateMinter` / `RevokeMint` (MintControl) Including a field for a SKU that is **not** unlocked → **fail the whole batch** (no partial apply, fee not kept). Do **not** combine `EnableFeature` (SKU unlock) into a settings batch; that is a separate 50 UST1 invoice. Unlock first, then change in a later batch (or two msgs / two invoices in one Cosmos tx if the dApp sequences unlock then batch — still two 50 UST1 charges). **Not a settings invoice:** initial instantiate params; `EnableFeature` itself; permissionless `SkimToLp`; `Mint` (issuing supply); `RegisterListedPair` if permissionless + factory-verified. **No-op** (every submitted value identical to on-chain) must **fail closed** and **not** take UST1. Invalid update reverts the fee. No refunds on a successful batch. Free create profile remains **0 UST1** SKU cost (gas only) until a SKU is bought or a settings **batch** is submitted. ## Problem statement Communities on Terra Classic cannot launch a first-party **buy / sell / transfer tax** token that is safe to list on this DEX. Today the only listed CW20 templates are 1:1 mintable/standard (e.g. columbus-5 **10184**, **6036**). Fee-on-transfer (FoT) templates are **explicitly forbidden** from the factory whitelist because the pair credits **declared** CW20 amounts, not balance deltas (invariants **H-01**, **P2**). There is no launcher, no paid-feature SKU, no manager-vs-upgrade split, and no Auto V2 LP helper. This issue is the **on-chain** design and implementation: token template, optional sister contracts (tax splitter / AutoLP / hook), paid feature SKUs (50 UST1 each), CMM-only wasm upgrade, and listing compatibility **without upgrading pair/router swap math**. ## Current codebase ### DEX will not (and must not) grow FoT math - Pair / limit escrow credit **declared** `Transfer` / `Send` amounts, not `balanceOf` deltas. Inbound FoT (recipient credit &lt; declared amount on transfers **to** the pair) desyncs `RESERVES` / `PENDING_ESCROW` (**P2**). Documented by `adversarial_token::fee_on_transfer_creates_reserve_imbalance` and Layer B `layer_b_b1_fot_desyncs_reserves`. - Ops policy: [`docs/runbooks/cw20-whitelist-policy.md`](docs/runbooks/cw20-whitelist-policy.md) — **never whitelist** templates whose recipient credit can differ from the declared amount. Playbook [`skills/AGENTS_HOOK_CW20_OPS.md`](skills/AGENTS_HOOK_CW20_OPS.md) **H-01**: do **not** add pair balance-delta / FoT swap math. - Post-listing migrate onto FoT/rebase wasm is already fail-closed by **F6** (code_id pin + `IsCodeIdWhitelisted` re-check, [#582](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/582) / [#584](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/584)). Listing-time FoT remains an **ops/harness** gate ([#589](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/589)). ### What exists today (reuse, do not fork the pair) | Piece | Role | Gap | |-------|------|-----| | `cw20-mintable` (git dep) | Honest mintable CW20 (10184 analogue) | No tax, no feature SKUs, minter is not a paid community product | | Factory `whitelisted_code_ids` / `AddWhitelistedCodeId` | Listing gate | Governance-only; no community-tax code_id | | Factory `SetPairHooks` | Register `AfterSwap` hooks | **Governance-only** — a community cannot self-register a tax hook on its pair | | `cl8y-dex-tax-hook` / `burn-hook` | Ask-side settlement (**I-02**): pair forwards `%` of **ask** to recipient / burn | Single recipient, single bps, admin-not-manager, one `tax_token`; **no sell-as-offer**, no ratios, no AutoLP | | `dex-common::hook_settlement` | Pair queries hook `GetConfig`, splits ask | New hook shapes need a **compatible** `GetConfig` **or** stay off the AfterSwap path | | `#589` harness + `cw20_mutants.rs` | FoT / rebase / honeypot detectors | A new template must go **green** on pair-touching 1:1 (inbound) and **must not** look like mutant 8654 | | Faucet `/mint` + `CreatePair` `/create` | Soft-launch drip; permissionless pair create (100 LUNC) | No **create token** flow | ### Upgrade / admin split today - DEX stack wasm admin / factory governance: `terra1zlmv2xydxcusurtr6rl78wsvytdc6mfex6hep7` ([`docs/reference/governance-multisig.md`](docs/reference/governance-multisig.md)). - Wrap-stack **CMM governance** (UST1 / wrap-mapper / treasury pause): `terra1xsecn4snv94ezcez0z3vq8an9j4h4kxxcydp8l` ([`deployments/mainnet-ust1-wrap/REGISTRY.md`](deployments/mainnet-ust1-wrap/REGISTRY.md)). - CMM **treasury** (fee sink, wrap custody): `terra16j5u6ey7a84g40sr3gd94nzg5w5fm45046k9s2347qhfpwm5fr6sem3lr2` — **not** a wasm admin. Anyone can `instantiate` a stored code_id with an arbitrary `--admin`. F6 freezes listed pairs if the instance **migrates off** the pinned template; it does not stop an unlisted rug. ## Why needed Sovereign communities want **their own** token with a **trusted leader** (manager) who can tune treasury routing, exemptions, and (optionally) mint — without the DEX becoming a general FoT AMM and without that leader being able to `MsgMigrateContract` to a hidden-mint / honeypot wasm. This is a **product** (PinkSale / Pancake “liquidity generator” style) adapted to **this** DEX’s accounting: taxes that would desync reserves are implemented so **every transfer that credits the pair, router, or limit escrow remains 1:1**. ## Research: features successful tax tokens actually used Use this as a **catalog to steal from or reject**, not as a mandate to clone SafeMoon. | Feature | Who used it | Why it “worked” | Verdict for this DEX | |---------|-------------|-----------------|----------------------| | **Buy tax / sell tax (separate bps)** | SafeMoon-class, PinkSale LGT, many BSC “baby” tokens | Marketing + LP + burn from volume | **Yes** — see architecture (buy = outbound split; sell = extra-debit or ask-hook) | | **Wallet-to-wallet transfer tax** | Same family; also some “anti-dump” coins | Taxes OTC / CEX deposit paths | **Paid SKU** — never on protocol addresses | | **Auto liquidity (AutoV2LP)** | SafeMoon, BabyDoge, PinkSale LGT | Sells half of tax into the quote and `provide_liquidity` | **Paid SKU** — **deferred** keeper, never re-enter the pair inside `Transfer`/`AfterSwap` | | **Treasury / marketing wallet** | FLOKI (early), almost every tax token | Funds community ops | **Free** (single sink). Multi-sink ratios = paid | | **Holder reflection / rebase** | SafeMoon, Reflect.finance, Olympus-style | “Earn by holding” | **Forbidden** — idle balance mutation breaks **P2** / Layer A | | **Burn slice of tax** | Many | Narrative deflation | **Paid SKU** (part of split-router) | | **Exempt / whitelist wallets** | Universal (owner, LP, CEX, routers) | Avoid taxing CEX deposits; avoid breaking DEX | **Protocol list always-on**; manager directory = paid | | **Mint + revoke mint** | Honest CW20s (10184); rugs hide mint | Genesis + “fair” later | **Paid SKU**; revoke is one-way | | **Owner can change tax bps** | Almost all; rugs set 99% | Tune after launch; CEX listing often **drops tax to 0** (FLOKI) | **Paid SKU** with **hard caps**; decreases always allowed | | **Max wallet / max tx / cooldown** | Anti-snipe at launch | Bot friction | **Optional paid**; easy to become a honeypot — cap + documentation | | **Trading enabled flag / sniper block** | Launch tools | Block buys until LP is live | **Optional paid**; must not block **exits** once trading is on | | **Blacklist / pause trading** | AML **and** honeypots | Pause-to-rug is a top rug vector | **Do not ship** as manager methods. Pause/blacklist stay DEX factory/pair if needed | | **Hidden mint / tax map after listing** | 8654 ALPHA-class, mutants A4/A12 | Rug | **Harness must fail** these | | **0% tax (PEPE, SHIB, later FLOKI)** | Memes that listed on CEX | Compatibility | Manager must be able to set taxes to **0** | | **LP lock / burn LP** | Trust wallets, PinkSale lockers | “Rug-proof LP” | Out of scope for v1 (no locker in-repo). AutoLP can mint LP **to a burn address or CMM** as a config | | **Creator rewards from curve (pump.fun)** | Solana memes | Not a transfer tax | Out of scope | | **Native LUNC burn tax** | Terra Classic chain | Protocol-level | Orthogonal; do not emulate inside CW20 | ### Extra ideas worth considering (not all v1) - **Tax-increase timelock** (e.g. 24h) so holders can exit before a raise. - **Instantiate-time max bps** that manager can never exceed (honeypot brake). - **Manager handover** / 2-step accept (community leader succession). - **Primary-pair auto-register**: permissionless `RegisterListedPair { pair }` that queries factory `pair` and only then treats that address as buy/sell. - **Sell tax in quote asset** via a sister `AfterSwap` hook (economically a sell tax in UST1/cLUNC) — only if hook wiring is solved without pair math changes. - **No reflection, no elastic supply, no “rewards in another token” inside `Transfer`.** ## Constraints / guardrails 1. **Do not upgrade pair/router swap math.** No balance-delta reconciliation. No exception to **H-01** for generic FoT. 2. **Inbound 1:1 (hard):** `Transfer` / `Send` / `TransferFrom` / `SendFrom` **to** pair, router, limit-related escrow, AutoLP, tax splitter, and this token itself must credit the recipient **exactly** `amount`. Layer A/B and `fee_on_transfer_creates_reserve_imbalance` analogue must stay **green** for those paths. 3. **Outbound buy tax is allowed:** when `from` is a registered listed pair, debit `amount` from pair, credit `amount - buy_tax` to the trader, credit `buy_tax` to sinks. Pair reserves still decrement by `amount` → **P2 holds**. 4. **Sell tax without inbound FoT (pick one, document):** - **Preferred (no DEX upgrade):** **extra-debit** — sender is debited `amount + sell_tax` (or `ceil` equivalent), pair is credited **exactly** `amount`. dApp must size max-sell. Fails closed if balance insufficient. - **Alternative (sister hook, no pair math):** AfterSwap skim of **ask** when offer is the tax token (quote-denominated sell tax). Requires hook **registration** (see options below). - **Forbidden:** credit pair `amount * (1 - sell_bps)` while pair accounts `amount`. 5. **No rebase / reflection / idle balance growth.** 6. **Management execute msgs = manager only.** Wasm `--admin` / `MsgMigrateContract` / `UpdateAdmin` = **CMM governance only** `terra1xsecn4snv94ezcez0z3vq8an9j4h4kxxcydp8l` (not DEX `terra1zlmv2…`, not CMM treasury `terra16j5u6…`, not the token manager). Hardcode expected admin in launcher docs; dApp/indexer **refuse to promote** instances whose LCD `ContractInfo.admin` ≠ CMM. 7. **Anyone can still instantiate the code_id** with a rogue `--admin`. Mitigate: launcher path + catalog filter (`admin == CMM` and `created_by_launcher`). F6 still freezes listed pairs on migrate-off-template. 8. **Do not** give manager `Pause`, `Blacklist`, or `SetTax(100%)` unbounded. Instantiate **max_buy_bps / max_sell_bps / max_transfer_bps** (recommend combined cap ≤ **2500** bps unless product sets otherwise). Raising above current bps may require the **variable-rates** SKU; lowering to 0 is always allowed if that SKU is on, or only at instantiate if not. 9. **Mint SKU is instantiate-only.** Cannot buy “mint” later for a token marketed as fixed supply. 10. **AutoLP must not re-enter the pair in the same tx as the taxed transfer/swap.** Threshold + permissionless `SkimToLp`. AutoLP contract is protocol-exempt. 11. **Factory whitelist** of this code_id happens only after `#589` REPORT **GO** (Layer A 1:1 on protocol paths + Layer B P2 green). Update whitelist runbook with a **narrow exception**: this **named** template, not “tax tokens in general”. 12. **SetPairHooks stays governance-only unless a later issue explicitly adds a minimal factory method.** Prefer extra-debit sell tax so communities do not wait on governance to tax sells. 13. Paid SKUs: **50 UST1** per SKU **as the on-chain invoice**. User may pay in any routable token via [#595](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/595); launcher/token still receives UST1. Forwarded to CMM treasury. No refunds. Idempotent enable. 14. **Manager settings batch fee:** one post-instantiate config `Send` costs **50 UST1 flat** and may mutate **all already-activated** settings in that batch (see Payment amendment). Implement as UST1 `Send` + hook (or launcher `UpdateTokenSettings`) so fee and mutations are **atomic**. Reject any key whose SKU is not unlocked. Manager-only; unpaid / underpaid / no-op → no state change. Do **not** charge this fee on `Mint` or permissionless `SkimToLp`. 15. Gas: no unbounded exemption maps in `Transfer` (pagination for queries; `Map` lookups in execute are O(1) per address). ### Sister-hook options (do not pick silently) | Option | DEX upgrade? | Sell tax | AutoLP | Recommendation | |--------|--------------|----------|--------|----------------| | **A. Token-only** (protocol-exempt + extra-debit sell + outbound buy) | No | In tax token | Sister **splitter** contract, not AfterSwap | **Default v1** | | **B. Token + existing tax-hook shape** | No math; still needs `SetPairHooks` | Ask skim (buy if tax token is ask; sell if we extend config) | No | Only if CMM will register hooks per pair (ops-heavy) | | **C. Minimal factory `SetPairHooks` for token manager + canonical hook code_id** | Small factory execute | Clean ask-side sell tax | Hook still must not re-enter | Follow-up only; **out of v1 unless product insists** | | **D. Pair FoT math** | Yes | Classic inbound FoT | — | **Rejected** | ## Relevant files | Area | Paths | |------|-------| | Pair accounting | `smartcontracts/contracts/pair/src/contract.rs`, `asset_code_id_guard.rs`, `lib.rs` (P2 / F6 comments) | | Factory whitelist / hooks | `smartcontracts/packages/dex-common/src/factory.rs`, `smartcontracts/contracts/factory/src/contract.rs` | | Hook settlement | `smartcontracts/packages/dex-common/src/hook_settlement.rs`, `smartcontracts/contracts/hooks/tax-hook/`, `hooks/README.md` | | Adversarial / harness | `smartcontracts/tests/src/adversarial_token.rs`, `cw20_mutants.rs`, `cw20_codeid_harness.rs`, `cw20-codeid-audits/` | | Policy | `docs/runbooks/cw20-whitelist-policy.md`, `docs/contracts-security-audit.md` (H-01, P2, F6, I-02), `skills/AGENTS_HOOK_CW20_OPS.md`, `skills/AGENTS_CW20_CODE_ID_PIN.md`, `skills/AGENTS_CW20_CODE_ID_AUDIT.md` | | CMM ids | `docs/reference/governance-multisig.md`, `deployments/mainnet-ust1-wrap/REGISTRY.md` | | New (this issue) | `smartcontracts/contracts/community-tax-token/` (name TBD), optional `community-tax-splitter/` / `community-tax-autolp/`, optional `community-token-launcher/`; workspace `smartcontracts/Cargo.toml` | ## Recommended direction ### 1. `community-tax-token` CW20 (new crate) Honest CW20-base surface (`transfer`, `send`, `increase_allowance`, queries) plus: - **Roles:** `manager` (execute config); **no** in-contract migrate. Wasm admin = CMM at instantiate (launcher sets it). - **Free profile:** `name`, `symbol`, `decimals` (cap **18**, factory bootstrap), `initial_supply` minted to `genesis_recipient`, fixed `buy_bps` / `sell_bps` to **one** `treasury` addr, protocol exemption set, manager = chosen leader. - **Protocol exemption:** always includes registered listed pairs (via `RegisterListedPair` validating factory), DEX router, this contract, splitter/AutoLP if instantiated. Manager **cannot** remove protocol entries. - **Buy:** if `from ∈ pairs` and `to` not exempt → outbound split (guardrail 3). - **Sell:** extra-debit when `to ∈ pairs` (guardrail 4). - **Transfer tax:** if paid SKU and neither side protocol-exempt. - **Queries:** `GetConfig`, `GetFeatures`, `GetExemptions`, `IsProtocolExempt`, `Minter`, `TaxPreview { from, to, amount }` (dApp max-spend). ### 2. Launcher (recommended over raw instantiate) `community-token-launcher` (or factory-adjacent crate): - Holds `token_code_id`, optional splitter/AutoLP code ids, `ust1` addr, `cmm_treasury`, `cmm_governance` (wasm admin to stamp). - `CreateToken { …, features: Vec<Sku> }` with `funds` / UST1 `Send` = `50 * len(paid_skus)` UST1. - Instantiates token with `--admin` CMM (via submessage instantiate-with-admin if available; otherwise CosmWasm instantiate admin = launcher then `update-admin` to CMM in the same tx — **must not** leave launcher as admin). - Emits wasm events: `community_token`, `sku`, `manager`, `code_id` for the indexer. - `EnableFeature { token, sku }` later (except mint SKU). ### 3. Paid SKU map (50 UST1 each; group methods) | SKU | Methods / behavior (one price for the group) | |-----|-----------------------------------------------| | **MintControl** | `Mint`, `UpdateMinter`, **one-way** `RevokeMint`. Instantiate-only purchase. Optional `cap`. | | **TransferTax** | Distinct `transfer_bps` vs buy/sell. | | **SplitRouter** | Ratios summing to 10000 toward treasury / burn / autolp / extra wallets (max N sinks, e.g. 4). | | **AutoV2Lp** | Instantiates AutoLP; `SetPrimaryPair`; permissionless `SkimToLp`; LP to burn, manager, or CMM — config at enable. | | **ExemptionDirectory** | Manager `AddExempt` / `RemoveExempt` (cannot remove protocol). | | **VariableRates** | Manager `UpdateTaxBps` within instantiate max; 0 always allowed. Optional timelock on increases. | | **LaunchGuards** (optional v1) | `max_wallet`, `cooldown_blocks`, `trading_enabled` (default on unless set). No seller blacklist. | Free remains usable with **zero** UST1 besides gas at create: fixed buy/sell to one wallet + initial supply. **One later save** of those free-profile settings costs **50 UST1 for the batch** (even if several fields change). Unlocking VariableRates/ExemptionDirectory/etc. is a **separate** 50 UST1 SKU; after that SKU is on, those fields may be included in a settings batch (still 50 UST1 for the whole batch, not another per-field charge). ### 4. AutoV2LP sister Not pair AfterSwap. Receives tax 1:1 (exempt). When `token_balance ≥ threshold`, `SkimToLp`: swap half via **router** or pair (new tx / later tx), `provide_liquidity`, mint LP to configured sink. Tests: reentrancy during taxed `Send` must **not** call pair; skim after swap succeeds. ### 5. Listing After harness GO: governance `AddWhitelistedCodeId`. Runbook exception paragraph. Do **not** whitelist 8654 or mutants. `#589` catalogue rows for the new id. ## Acceptance criteria - [ ] New CW20 crate + launcher (or documented instantiate path) with free profile and paid SKUs at **50 UST1** each, paid to CMM treasury. - [ ] Post-instantiate manager **settings batch** charges **50 UST1 flat** for one atomic save of already-activated settings; unpaid/no-op/non-manager/unactivated-SKU keys fail without taking fee. - [ ] Manager-only config/mint; non-manager execute → `Unauthorized`. - [ ] LCD `ContractInfo.admin` on launcher-created tokens = CMM governance; manager cannot migrate or `update-admin`. - [ ] Inbound transfers to pair/router/escrow/splitter/AutoLP are **1:1**; Layer A/B P2 green; classic FoT test still **fails** a naïve inbound-tax mutant. - [ ] Buy tax outbound and sell extra-debit (or documented hook alternative) match `TaxPreview`. - [ ] Mint SKU cannot be enabled post-instantiate; `RevokeMint` is irreversible. - [ ] AutoLP does not re-enter pair inside `Transfer`/`Send`/`AfterSwap`. - [ ] No reflection/rebase/pause/blacklist manager APIs. - [ ] `#589` REPORT template filled; factory whitelist is a **separate ops step** gated on GO. - [ ] Pair/router **wasm unchanged** for swap math (factory whitelist add is ops, not this MR unless Option C is explicitly chosen). - [ ] Docs: runbook + security-audit row + playbook; `make verify-issue-NNN`. ## Test plan (all paths) **Unit / multi-test (`smartcontracts`):** - Instantiate free profile: supply, bps, treasury, manager, CMM admin. - Buy: pair → user outbound split; pair balance − `amount`; user + treasury + sinks = `amount`. - Sell: user → pair extra-debit; pair + `amount` exactly; seller debit = `amount + tax`. - Sell with insufficient balance for extra-debit → fail closed. - Transfer tax SKU off: wallet↔wallet 1:1; on: tax to sinks; exempt wallets skip. - Protocol exempt cannot be removed by manager. - `RegisterListedPair`: factory-listed pair OK; random addr rejected; spoof pair rejected. - Each SKU: unpaid → execute rejected; pay 50 UST1 → enabled; second pay idempotent (no double-charge or reject cleanly). - MintControl: mint as minter; non-minter fail; revoke; mint after revoke fail; SKU absent at instantiate → no mint forever. - VariableRates: within cap OK; above instantiate max fail; to 0 OK; **without 50 UST1 batch invoice → reject**; buy+sell in **one** batch → still **50 UST1** (not 100). - Settings no-op (all values unchanged) with 50 UST1 attached → revert, UST1 not kept. - Free-profile batch after instantiate requires 50 UST1 once; SKU unlock is a separate invoice. - Batch that includes `transfer_bps` while TransferTax SKU is off → revert entire batch. - `Mint` succeeds without settings invoice (SKU only); `RevokeMint` / `UpdateMinter` require an activated MintControl SKU **and** a 50 UST1 settings batch. - `SkimToLp` remains permissionless and free. - SplitRouter: ratios ≠ 10000 fail; burn sink reduces supply; dust rounding (last sink gets remainder). - AutoLP: accumulate below threshold no-op; `SkimToLp` adds LP; during `Send` no nested swap. - Limit place/fill/cancel/claim with tax token as asset: escrow 1:1 (L1). - Provide/withdraw 1:1 inbound/outbound-from-pair (withdraw is pair → LP holder: decide if buy tax applies to **withdraw**; **recommended: exempt withdraw / LP paths** — treat pair→user as buy tax **only** for swap `Send` with `Swap` msg, not all pair sends). **Must specify:** prefer **swap-direction detection** via `Send` msg / skip tax if `to` is LP provider path. If detection is fragile, **exempt all pair↔user transfers except** those originating from swap receive — document chosen rule and test provide/withdraw/limit **untaxed**. - Hybrid swap: book + pool legs; no reserve desync. - Router multi-hop: intermediate pair hops 1:1. - F6: migrate instance off template → pair write paths fail; Refresh refuses FoT replacement. **Harness:** new `cw20-codeid-audits/codeids/<id>/` after store; `LAYER_B_LT=1 make verify-issue-589`. **LocalTerra:** launcher create, CreatePair, swap buy/sell, extra-debit max button, AutoLP skim. ## Attack / abuse / hack test plan - **Inbound FoT sneak:** mutant that taxes `to == pair` — must be rejected by harness (P2 red). Our template must not match. - **Honeypot 99% tax:** instantiate max cap blocks; VariableRates cannot exceed cap. - **Hidden mint:** no mint without SKU; no second minter backdoor (mutant A12 analogue). - **Manager migrate:** manager `MsgMigrateContract` rejected (not wasm admin); CMM migrate to **same family** wasm only in ops (F6 refresh). Test manager cannot `UpdateAdmin`. - **Rogue instantiate** with attacker admin: catalog must not list; CreatePair still possible if code_id whitelisted — **F6** freeze on their later migrate. Consider launcher-attestation query `GetLauncherOrigin` so dApp warns “unverified admin”. - **Exemption grief:** manager exempts a bot from buy tax — product-accepted; cannot un-exempt pair to break DEX. - **Permissionless `RegisterListedPair`:** cannot register a non-factory contract to trigger fake “buy” taxes on random sends. - **Reentrancy:** `Send` to contract that calls back `Transfer` / `SkimToLp` / pair `swap` in the same tx — fail or no double-tax / no reserve desync. - **Extra-debit drain:** sell tax computed so pair never receives less than `amount`; rounding cannot create extra tokens. - **UST1 payment spoof:** enabling SKU **or** changing a setting with wrong token / wrong amount / 0 → reject; do not trust memo-only. - **Settings fee bypass:** manager `UpdateTaxBps` via plain execute without UST1 `Send` → `Unauthorized` / `FeeRequired`; cannot pay in the tax token unless routed to UST1 via #595 (launcher/token invoice token is UST1 only). - **Wrong batch invoice:** two keys with 0 or 25 UST1 → revert both; **100 UST1 for one batch** → reject excess (prefer exact **50**; do not silently keep a tip unless documented). Unactivated SKU field in the batch → revert, fee not kept. - **Feature downgrade rug:** disabling SKUs (if allowed) must not trap funds in AutoLP; prefer **enable-only**. - **Max-wallet + tax:** user cannot be bricked such that they cannot sell (sell to pair should bypass max-wallet). - **Cooldown / trading_enabled:** once on, manager cannot selectively block sells (no blacklist). If `trading_enabled=false`, **both** buy and sell blocked, or document; do not ship buy-only-on. - **Event lying:** Transfer attributes must match actual credits (mutant D16). - **Router / zap / one-sided LP (#533):** all protocol contracts that hold the token must be exempt or 1:1. Audit wrap-mapper, router, pair, AutLP. - **Limit book:** maker place inbound 1:1; take outbound: decide tax (prefer no buy tax on limit fill to maker/taker beyond swap hook — **test both sides**). - **DoS:** huge exemption Map; query pagination only. - **Phishing dApp:** not on-chain; frontend issue covers copy. ## Verification criteria - `cd smartcontracts && cargo test -p <token-crate> -p <launcher-crate>` and DEX suite `adversarial` + `cw20_codeid_harness` still pass for mintable and fail known-bad FoT. - `make test-contracts`; new `make verify-issue-NNN`. - Docs: whitelist runbook names this template as the **only** tax exception and restates inbound 1:1. - No pair/router FoT math diff in the implementing MR. - Manual LocalTerra: create free token → pair → buy/sell; pay SKU → mint → revoke; CMM-only migrate probe. ## Labels / owner / priority `contracts` `architecture` `enhancement` · Owner: contracts · Priority: **P2**
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-22 10:59:13 +00:00 (Migrated from gitlab.com)

marked as related to #593

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

mentioned in issue #594

mentioned in issue #594
PlasticDigits commented 2026-08-22 10:59:15 +00:00 (Migrated from gitlab.com)

marked as related to #594

marked as related to #594
PlasticDigits commented 2026-08-22 10:59:22 +00:00 (Migrated from gitlab.com)
Siblings (product surface): - dApp create/manage: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/593 - Indexer catalog/API: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/594
PlasticDigits commented 2026-08-22 11:02:33 +00:00 (Migrated from gitlab.com)

mentioned in issue #595

mentioned in issue #595
PlasticDigits commented 2026-08-22 11:02:34 +00:00 (Migrated from gitlab.com)

marked as related to #595

marked as related to #595
PlasticDigits commented 2026-08-22 11:03:00 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-08-22 11:03:03 +00:00 (Migrated from gitlab.com)

Payment UX is not UST1-only. On-chain invoice remains 50 UST1/SKU; users pay in any routable token via https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/595 (launcher still only accepts UST1).

Payment UX is not UST1-only. On-chain invoice remains 50 UST1/SKU; users pay in any routable token via https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/595 (launcher still only accepts UST1).
PlasticDigits commented 2026-08-22 11:05:02 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-08-22 11:05:05 +00:00 (Migrated from gitlab.com)

Manager settings fee: after instantiate, changing any setting costs 50 UST1 per setting that actually changes (atomic UST1 invoice + mutation). Same 50 UST1 unit as SKU unlocks; pay in any token via #595. Instantiate defaults stay free; Mint and permissionless SkimToLp are not settings invoices. No-op updates must not take the fee.

**Manager settings fee:** after instantiate, changing **any** setting costs **50 UST1 per setting that actually changes** (atomic UST1 invoice + mutation). Same 50 UST1 unit as SKU unlocks; pay in any token via #595. Instantiate defaults stay free; `Mint` and permissionless `SkimToLp` are not settings invoices. No-op updates must not take the fee.
PlasticDigits commented 2026-08-22 11:08:58 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-08-22 11:09:04 +00:00 (Migrated from gitlab.com)

Settings fee correction: 50 UST1 covers one batch of changes to already activated settings (not 50 per field). Unlocked SKUs only; free-profile buy/sell/treasury count as activated at instantiate. EnableFeature stays a separate SKU invoice.

Settings fee correction: 50 UST1 covers **one batch** of changes to **already activated** settings (not 50 per field). Unlocked SKUs only; free-profile buy/sell/treasury count as activated at instantiate. EnableFeature stays a separate SKU invoice.
PlasticDigits commented 2026-08-22 12:26:36 +00:00 (Migrated from gitlab.com)

mentioned in issue #597

mentioned in issue #597
PlasticDigits commented 2026-08-22 12:26:36 +00:00 (Migrated from gitlab.com)

marked as related to #597

marked as related to #597
PlasticDigits commented 2026-08-22 12:26:49 +00:00 (Migrated from gitlab.com)

Follow-up (book liquidity, not AutoLP): team maker+taker limit MM with Tier 0 fee waiver + monthly subscription — design/investigation: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/597

AutoV2Lp stays the on-chain pool skim SKU. #597 is off-chain book MM billed via #595.

Follow-up (book liquidity, not AutoLP): team **maker+taker** limit MM with Tier 0 fee waiver + monthly subscription — design/investigation: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/597 AutoV2Lp stays the on-chain pool skim SKU. #597 is off-chain book MM billed via #595.
PlasticDigits commented 2026-08-23 03:10:03 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1117

mentioned in merge request !1117
PlasticDigits commented 2026-08-23 03:50:07 +00:00 (Migrated from gitlab.com)

mentioned in commit 093b78e43b

mentioned in commit 093b78e43ba1b43bcc448b81eea49621bd0aee6f
PlasticDigits commented 2026-08-23 03:50:24 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1119

mentioned in merge request !1119
PlasticDigits commented 2026-08-23 03:53:27 +00:00 (Migrated from gitlab.com)

mentioned in commit ffe04d143e

mentioned in commit ffe04d143e688f19232f6895afa4bf4d67759e32
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-23 03:53:28 +00:00
PlasticDigits commented 2026-08-23 03:53:49 +00:00 (Migrated from gitlab.com)

mentioned in issue #601

mentioned in issue #601
PlasticDigits commented 2026-08-23 03:53:50 +00:00 (Migrated from gitlab.com)

marked as related to #601

marked as related to #601
PlasticDigits commented 2026-08-23 03:53:59 +00:00 (Migrated from gitlab.com)

!1119 merge review (no automerge, did not wait for CI)

Merged !1119 onto main. Source issue-592-community-tax-cw20 was already based on current main (4f245fc8); git merge-tree reported no conflicts. Pair/router/factory swap-math files have zero diff vs main (only new crates + docs).

Acceptance vs this issue

  • make verify-issue-592 on the MR SHA 093b78e4 — 27 crate tests × 2 + docs/REPORT NO-GO / whitelist exception (PASS).
  • Invoices 50 UST1 exact; unpaid / no-op / non-manager / unactivated SKU fail closed (settings_*, batch_unactivated_sku_reverts, excess_invoice_rejected, non_manager_invoice_unauthorized).
  • Inbound pair Transfer 1:1; sell extra-debit; buy outbound split; TaxPreview match.
  • Launcher stamps CMM admin; MintControl instantiate-only + RevokeMint; AutoLP never called from token Transfer/Send.
  • T592-7 documented: pair→EOA Transfer (withdraw / limit refund) takes buy tax. Issue recommended exempting withdraw/LP; MR chose same CosmWasm primitive — not a merge blocker, but it is a product residual.
  • make test-contracts full workspace — not run this pass (MR test plan also left it open).
  • DEX suite adversarial + cw20_codeid_harness — not re-run this pass; pair wasm unchanged so FoT adversarial still lives on main.
  • Manual LocalTerra create → CreatePair → buy/sell — not run (out of !1119).

CI

Pipeline 2782630926 jobs test-contracts, cargo-audit-smartcontracts, gitleaks failed with ci_quota_exceeded, not a red suite. Project does not require a green pipeline to merge.

Post-merge tracking

Store + #589 REPORT GO + factory whitelist + LocalTerra smoke + leftover DEX integration: new issue #601. Siblings #593 / #594 / #597 already exist.

**!1119 merge review (no automerge, did not wait for CI)** Merged [!1119](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/401) onto `main`. Source `issue-592-community-tax-cw20` was **already based on current `main`** (`4f245fc8`); `git merge-tree` reported no conflicts. Pair/router/factory swap-math files have **zero** diff vs `main` (only new crates + docs). **Acceptance vs this issue** - [x] `make verify-issue-592` on the MR SHA `093b78e4` — 27 crate tests × 2 + docs/REPORT **NO-GO** / whitelist exception (PASS). - [x] Invoices 50 UST1 exact; unpaid / no-op / non-manager / unactivated SKU fail closed (`settings_*`, `batch_unactivated_sku_reverts`, `excess_invoice_rejected`, `non_manager_invoice_unauthorized`). - [x] Inbound pair Transfer 1:1; sell extra-debit; buy outbound split; `TaxPreview` match. - [x] Launcher stamps CMM admin; MintControl instantiate-only + `RevokeMint`; AutoLP never called from token Transfer/Send. - [x] **T592-7** documented: pair→EOA `Transfer` (withdraw / limit refund) takes **buy tax**. Issue recommended exempting withdraw/LP; MR chose same CosmWasm primitive — not a merge blocker, but it is a product residual. - [ ] `make test-contracts` full workspace — **not run** this pass (MR test plan also left it open). - [ ] DEX suite `adversarial` + `cw20_codeid_harness` — **not re-run** this pass; pair wasm unchanged so FoT adversarial still lives on `main`. - [ ] Manual LocalTerra create → `CreatePair` → buy/sell — **not run** (out of !1119). **CI** Pipeline [2782630926](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/pipelines/2782630926) jobs `test-contracts`, `cargo-audit-smartcontracts`, `gitleaks` failed with **`ci_quota_exceeded`**, not a red suite. Project does not require a green pipeline to merge. **Post-merge tracking** Store + `#589` REPORT **GO** + factory whitelist + LocalTerra smoke + leftover DEX integration: new issue [#601](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/601). Siblings [#593](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/593) / [#594](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/594) / [#597](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/597) already exist.
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-23 06:30:41 +00:00 (Migrated from gitlab.com)

mentioned in issue #603

mentioned in issue #603
PlasticDigits commented 2026-08-23 06:30:42 +00:00 (Migrated from gitlab.com)

marked as related to #603

marked as related to #603
PlasticDigits commented 2026-08-23 06:48:17 +00:00 (Migrated from gitlab.com)

mentioned in issue #604

mentioned in issue #604
PlasticDigits commented 2026-08-23 06:48:18 +00:00 (Migrated from gitlab.com)

marked as related to #604

marked as related to #604
PlasticDigits commented 2026-08-23 06:48:28 +00:00 (Migrated from gitlab.com)

mentioned in issue #605

mentioned in issue #605
PlasticDigits commented 2026-08-23 06:48:28 +00:00 (Migrated from gitlab.com)

marked as related to #605

marked as related to #605
PlasticDigits commented 2026-08-23 11:49:45 +00:00 (Migrated from gitlab.com)

mentioned in issue #606

mentioned in issue #606
PlasticDigits commented 2026-08-23 11:49:45 +00:00 (Migrated from gitlab.com)

marked as related to #606

marked as related to #606
PlasticDigits commented 2026-08-23 11:49:49 +00:00 (Migrated from gitlab.com)

mentioned in issue #607

mentioned in issue #607
PlasticDigits commented 2026-08-23 11:49:50 +00:00 (Migrated from gitlab.com)

marked as related to #607

marked as related to #607
PlasticDigits commented 2026-08-23 11:49:53 +00:00 (Migrated from gitlab.com)

mentioned in issue #608

mentioned in issue #608
PlasticDigits commented 2026-08-23 11:49:53 +00:00 (Migrated from gitlab.com)

marked as related to #608

marked as related to #608
PlasticDigits commented 2026-08-23 11:49:55 +00:00 (Migrated from gitlab.com)

mentioned in issue #609

mentioned in issue #609
PlasticDigits commented 2026-08-23 11:49:55 +00:00 (Migrated from gitlab.com)

marked as related to #609

marked as related to #609
PlasticDigits commented 2026-08-23 11:49:59 +00:00 (Migrated from gitlab.com)

marked as related to #610

marked as related to #610
PlasticDigits commented 2026-08-23 11:50:00 +00:00 (Migrated from gitlab.com)

mentioned in issue #610

mentioned in issue #610
PlasticDigits commented 2026-08-24 05:53:59 +00:00 (Migrated from gitlab.com)

mentioned in issue #620

mentioned in issue #620
PlasticDigits commented 2026-08-24 05:54:00 +00:00 (Migrated from gitlab.com)

marked as related to #620

marked as related to #620
PlasticDigits commented 2026-08-24 05:54:09 +00:00 (Migrated from gitlab.com)

mentioned in issue #621

mentioned in issue #621
PlasticDigits commented 2026-08-24 05:54:11 +00:00 (Migrated from gitlab.com)

marked as related to #621

marked as related to #621
PlasticDigits commented 2026-08-24 05:54:14 +00:00 (Migrated from gitlab.com)

mentioned in issue #622

mentioned in issue #622
PlasticDigits commented 2026-08-24 05:54:16 +00:00 (Migrated from gitlab.com)

marked as related to #622

marked as related to #622
PlasticDigits commented 2026-08-24 05:54:23 +00:00 (Migrated from gitlab.com)

marked as related to #623

marked as related to #623
PlasticDigits commented 2026-08-24 06:19:37 +00:00 (Migrated from gitlab.com)

mentioned in issue #624

mentioned in issue #624
PlasticDigits commented 2026-08-24 11:49:43 +00:00 (Migrated from gitlab.com)

mentioned in issue #626

mentioned in issue #626
PlasticDigits commented 2026-08-24 11:49:45 +00:00 (Migrated from gitlab.com)

marked as related to #626

marked as related to #626
PlasticDigits commented 2026-08-25 02:04:15 +00:00 (Migrated from gitlab.com)

mentioned in issue #633

mentioned in issue #633
PlasticDigits commented 2026-08-25 02:08:33 +00:00 (Migrated from gitlab.com)

mentioned in issue #634

mentioned in issue #634
PlasticDigits commented 2026-08-26 04:20:45 +00:00 (Migrated from gitlab.com)

mentioned in issue #670

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