feat(wrap-mapper): split fee_wrap_bps / fee_unwrap_bps for ≈2% unwrap all-in (no gross-up) #9
Labels
No labels
bug
docs
documentation
duplicate
enhancement
frontend
good first issue
help wanted
invalid
missing-implementation
priority
medium
product
QA
question
testing
UX
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/ustr-cmm#9
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Split wrap-mapper
fee_bpsintofee_wrap_bpsandfee_unwrap_bps, migrate mainnet, and setfee_wrap_bps=200/fee_unwrap_bps=51so user unwrap all-in ≈ 2% under today’s 1.5% Classic burn tax without InstantWithdraw gross-up. Document the retune rule for future tax changes.Product decision (approved): prefer asymmetric fees + existing tax incidence over on-chain gross-up.
Companion (DEX consumer): track UI/docs/ops in
PlasticDigits/cl8y-dex-terraclassic(linked after create).Related: cl8y-dex #512 (UI disclosure of stacked tax; closed mitigation), mainnet wrap-mapper
terra1xuuuhpmyd5t29ry7mydg7ra2q2phrwhx7j28nx7x9sjw6zznkumsz0nmd2.Current codebase
On-chain (columbus-5, verified)
config.fee_bpsburn_tax_rateterra1xsecn4snv94ezcez0z3vq8an9j4h4kxxcydp8lBehavior today
amount × fee_bps / 10_000;MsgExecuteContractwrap_deposit is untaxed → user nets post-fee CW20 only.fee_bps; treasuryInstantWithdraw→BankMsg::Sendof post-fee amount; receiver pays burn tax.fee_bps=200+ 1.5% tax: 10 000 → fee 200 → withdraw 9 800 → user 9 653 (~3.47% all-in), not 2%.Fee was raised 100→200 after Prop #12223 so a future gross-up could fund tax from fee residual (
docs/DEPLOYMENT.md“Wrap fee vs burn tax”). Gross-up was not implemented; users still eat tax on top of the full 2% fee.Relevant files (ustr-cmm)
contracts/contracts/wrap-mapper/src/state.rsConfig.fee_bps,MIN_FEE_BPS,MAX_FEE_BPScontracts/contracts/wrap-mapper/src/msg.rsInstantiateMsg.fee_bps,ExecuteMsg::SetFeeBps,ConfigResponse.fee_bpscontracts/contracts/wrap-mapper/src/contract.rscalculate_fee(..., config.fee_bps); migrate/querycontracts/contracts/wrap-mapper/src/error.rsdocs/DEPLOYMENT.mdset_fee_bpsgov exampledocs/CONTRACTS.mdplans/NATIVE_TOKEN_WRAPPING.mdAudit note:
MIN_FEE_BPS/ “fee must cover tax” assumptions (audits/INTERNAL_KIMIK3_*.mdM-4) assume a single fee intended to subsidize unwrap tax under gross-up — must be revised for asymmetric fees.Why this is needed
C282C337…).fee_unwrap_bps=51(no gross-up) match gross-up+200 for treasury surplus (~+51 bps per unwrap) while avoiding tax-oracle / gross-up contract complexity.fee_bpscannot express “expensive wrap, tax-tuned unwrap”.Target math (burn_tax = 0.015)
Want user receive ≈
0.98 × Awith no gross-up:fee_bps=200wrap=200,unwrap=51Prefer 51 if goal is ≤ 2% all-in.
Constraints / guardrails
fee_wrap_bps=200.ACW20 and withdrawsA − fee_unwrap; treasury surplus Δ ≈+fee_unwrap(user-paid tax does not erodenative ≥ supply). Document explicitly — old “fee ≥ tax” floor does not apply to unwrap under this policy.MIN_FEE_BPS/ bounds: allowfee_unwrap_bps=51(and potentially lower after retune). Do not force unwrap fee ≥ burn tax. Keep a saneMAX_FEE_BPSfor both. Zero fee: follow existing product policy (reject or allow only if explicitly decided — default keep reject-zero if that is current wrap-mapper behavior).fee_bps=200→ migrate tofee_wrap_bps=200andfee_unwrap_bps=200or directly to200/51in the same gov window; never leave unwrap at 200 after advertising the fix without setting 51.terra1xsecn…); not DEX factory multisig.ConfigResponseshape is OK if versioned migrate + coordinated DEX release; prefer additive fields + deprecatefee_bpsonly if a short dual-read window is required — document chosen compat strategy in the MR.Recommended direction
fee_bpswithfee_wrap_bps+fee_unwrap_bpsonConfig.SetFeeWrapBps/SetFeeUnwrapBpsand/orSetFees { fee_wrap_bps, fee_unwrap_bps }(governance-gated).Config: return both fields; if temporary compat needed, mirrorfee_bps→ unwrap or omit with clear changelog.fee_wrap_bps; unwrap path →fee_unwrap_bpsonly.fee_bps→ both fields; then gov set 200 / 51 on columbus-5.Retune rule (must document)
When chain
burn_tax_ratechanges, recompute:aimed at 2% user all-in (
receive/A = 0.98). Worked example:burn_tax_rate=0.015→ 51. Prefer rounding so all-in is ≤ 2% when ambiguous.Also document: if tax rises above ~2%, “2% all-in with no gross-up” becomes impossible without paying users a subsidy — escalate to product (gross-up or higher all-in target).
Acceptance criteria
fee_wrap_bpsandfee_unwrap_bps(no reliance on a single fee for both paths).fee_wrap_bps; unwrap uses onlyfee_unwrap_bps.fee_bpssucceeds on upgrade tests; mainnet migrate plan written.fee_wrap_bps=200,fee_unwrap_bps=51(or documented equivalent from retune rule at ship time).docs/DEPLOYMENT.md+docs/CONTRACTS.md(+ wrapping plan if needed) document asymmetric fees, solvency argument, and retune rule.wasm executeJSON forSetFees/ setters (no secrets).Test plan (all paths)
Unit / contract
fee_wrap_bpsonly (unwrap fee ignored).fee_unwrap_bpsonly (wrap fee ignored).SetFeeWrapBps/SetFeeUnwrapBps/SetFees: auth (gov only), bounds, events/attrs.fee_bps=200→ expected new fields; Cw2 version.Integration / LocalTerra (or multi-test)
Mainnet verify (read-only then tiny live)
fee_unwrap× (1 − tax); record tx hash.fee_wrap; record tx hash.uluna/uusd≥ cLUNC/cUSTC supply after probes.Test plan (attack / hack / abuse)
SetFees*rejected.MAX_FEE_BPS; at minimum allowed; reject out-of-bounds / zero if policy rejects zero.fee_unwrap << tax, provenative ≥ supplydoes not regress across unwraps (surplus += unwrap fee); add regression test/comment.Verification criteria
fee_wrap_bps=200,fee_unwrap_bps=51on columbus-5Retune verification: given a hypothetical
burn_tax_rate, docs alone suffice for an operator to compute the nextfee_unwrap_bpswithout reading this issue.mentioned in issue PlasticDigits/cl8y-dex-terraclassic#516
marked as related to PlasticDigits/cl8y-dex-terraclassic#516
Companion DEX consumer issue: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/516
Bundled rollout: ship ustr-cmm fee split + mainnet 200/51 first, then DEX #516 + Coolify frontend redeploy.
mentioned in commit
b3fc79cbfcmentioned in merge request !31
Implementation MR: https://gitlab.com/PlasticDigits2/ustr-cmm/-/merge_requests/9
Done in MR (code/docs/tests):
fee_wrap_bps+fee_unwrap_bpsfee_bps+ mainnet migrate plan in DEPLOYMENT.mdset_fees/ setter JSONskills/wrap-mapper-asymmetric-fees+ skills README crosslinkStill for human ops / companion (out of this MR):
SetFees200/51mentioned in commit
1939449360mentioned in issue #13
marked as related to #13
Remaining mainnet ops + DEX consumer rollout tracker: https://gitlab.com/PlasticDigits2/ustr-cmm/-/work_items/13
Live columbus-5 still code
11565/{ fee_bps: 200 }as of 2026-08-15. #13 covers store/migrate,SetFees200/51, probes, and Coolify coordination with DEX #516.mentioned in issue PlasticDigits/cl8y-dex-terraclassic#521
mentioned in commit
ed05356800mentioned in issue PlasticDigits/cl8y-dex-terraclassic#523
Follow-up: mainnet fees now live (ops were on #13)
columbus-5 wrap-mapper
terra1xuuuh…nmd2is no longer{ fee_bps: 200 }/ code11565.11574(cw20.3.0)fee_wrap_bps=200,fee_unwrap_bps=519B305800…DA408F05225E…D107SetFees740CB152…BDF3This issue stays closed (code shipped in !31). Remaining human AC (tiny wrap/unwrap probes, Coolify, DEX router wasm) remains on #13 + DEX #523.
mentioned in issue #15