Unwrap costs 3.47% not the documented 2%: burn tax is deducted from the payout on top of the fee #512

Closed
opened 2026-08-09 14:32:03 +00:00 by leonardocolucci · 24 comments
leonardocolucci commented 2026-08-09 14:32:03 +00:00 (Migrated from gitlab.com)

Summary

Unwrapping LUNC-C to native LUNC effectively costs 3.47%, not the documented 2% unwrap fee: the chain's burn tax (currently 1.5%) is deducted from the treasury's payout transfer, so the recipient receives withdraw_amount − tax — the tax stacks on top of the fee and is silently paid by the user.

Reported by community member Spaceship Racer in the Telegram group (they couldn't reach GitLab); verified independently on-chain before filing.

Evidence (mainnet tx)

Tx C282C337B3F3E4AC7ECC95B92E82DCD1484C2E6FA766A07DBFAFAC49F6B280A0 (block 29871998, 2026-08-09 13:36 UTC):

Event attr Value
action unwrap (wrap-mapper terra1xuuuhpmyd5t29ry7mydg7ra2q2phrwhx7j28nx7x9sjw6zznkumsz0nmd2)
cw20_contract terra1437qslye72t7qmmahn4t5chz50r8a62g45phwkquwpyu2l62u6ksqssgdg (LUNC-C)
gross_amount 10,000.000000 LUNC
fee 200.000000 LUNC (2% ✓)
withdraw_amount 9,800.000000 LUNC
actually received 9,653.000000 LUNC

Transfer events show the treasury (terra16j5u6ey…sem3lr2) paying the recipient 9,653 LUNC while 147 LUNC (= exactly 1.5% of 9,800) goes to the tax module and is split onward (14.7 + 14.7 + 117.6). So: 10,000 → −2% fee → −1.5% burn tax → 9,653, i.e. 3.47% total.

Why

The burn tax on the treasury's native InstantWithdraw bank send is deducted from the transferred amount (receiver pays), and the unwrap flow computes withdraw_amount before tax. NATIVE_TOKEN_WRAPPING.md's "1 tax event for unwrapping output" design anticipated the tax event itself, but the incidence falls on the user on top of the 2% fee, which neither the fee math nor the UI accounts for.

Suggested fixes (either)

  1. Gross-up the withdraw: treasury sends withdraw_amount / (1 − tax_rate) (query /terra/treasury/v1beta1/tax_rate or compute from params) so the recipient nets exactly gross − fee. Tax is then effectively paid out of the 2% fee margin.
  2. At minimum, disclose: have the unwrap quote/UI show "you will receive X after the 2% fee and Y% chain burn tax" so users aren't surprised.

(A governance burn-tax whitelist for the treasury was considered and discarded — per community feedback, LUNC governance would not whitelist CL8Y DEX contracts under the current validator set, so it is not a realistic path.)

Addendum: exchange-deposit recipient trap (same flow, second hazard)

Because Unwrap { recipient } accepts any address, users may set an exchange deposit address as the recipient to "save a hop" (and one transfer's burn tax). This silently loses the funds: the payout arrives as a contract-triggered transfer from the treasury, which at least MEXC's deposit crediting ignores even with the correct memo — the LUNC lands at the exchange's address but is never credited. Confirmed the hard way by community member Spaceship Racer (reported in Telegram 2026-08-09 16:39).

Suggested mitigation: the unwrap UI should warn "withdraw to your own wallet, then transfer to an exchange normally — exchange deposit systems do not credit contract-initiated transfers."

Happy to retest once a fix lands — we run automated tooling against the DEX and can verify quickly.

## Summary Unwrapping LUNC-C to native LUNC effectively costs **3.47%**, not the documented **2%** unwrap fee: the chain's burn tax (currently 1.5%) is deducted from the treasury's payout transfer, so the recipient receives `withdraw_amount − tax` — the tax stacks on top of the fee and is silently paid by the user. Reported by community member **Spaceship Racer** in the Telegram group (they couldn't reach GitLab); verified independently on-chain before filing. ## Evidence (mainnet tx) Tx `C282C337B3F3E4AC7ECC95B92E82DCD1484C2E6FA766A07DBFAFAC49F6B280A0` (block 29871998, 2026-08-09 13:36 UTC): | Event attr | Value | |---|---| | action | `unwrap` (wrap-mapper `terra1xuuuhpmyd5t29ry7mydg7ra2q2phrwhx7j28nx7x9sjw6zznkumsz0nmd2`) | | cw20_contract | `terra1437qslye72t7qmmahn4t5chz50r8a62g45phwkquwpyu2l62u6ksqssgdg` (LUNC-C) | | gross_amount | 10,000.000000 LUNC | | fee | 200.000000 LUNC (2% ✓) | | withdraw_amount | 9,800.000000 LUNC | | **actually received** | **9,653.000000 LUNC** | Transfer events show the treasury (`terra16j5u6ey…sem3lr2`) paying the recipient 9,653 LUNC while **147 LUNC** (= exactly 1.5% of 9,800) goes to the tax module and is split onward (14.7 + 14.7 + 117.6). So: `10,000 → −2% fee → −1.5% burn tax → 9,653`, i.e. **3.47% total**. ## Why The burn tax on the treasury's native `InstantWithdraw` bank send is deducted from the transferred amount (receiver pays), and the unwrap flow computes `withdraw_amount` before tax. NATIVE_TOKEN_WRAPPING.md's "1 tax event for unwrapping output" design anticipated the tax event itself, but the *incidence* falls on the user on top of the 2% fee, which neither the fee math nor the UI accounts for. ## Suggested fixes (either) 1. **Gross-up the withdraw**: treasury sends `withdraw_amount / (1 − tax_rate)` (query `/terra/treasury/v1beta1/tax_rate` or compute from params) so the recipient nets exactly `gross − fee`. Tax is then effectively paid out of the 2% fee margin. 2. At minimum, **disclose**: have the unwrap quote/UI show "you will receive X after the 2% fee and Y% chain burn tax" so users aren't surprised. (A governance burn-tax whitelist for the treasury was considered and discarded — per community feedback, LUNC governance would not whitelist CL8Y DEX contracts under the current validator set, so it is not a realistic path.) ## Addendum: exchange-deposit recipient trap (same flow, second hazard) Because `Unwrap { recipient }` accepts any address, users may set an **exchange deposit address** as the recipient to "save a hop" (and one transfer's burn tax). This silently loses the funds: the payout arrives as a contract-triggered transfer from the treasury, which at least MEXC's deposit crediting ignores **even with the correct memo** — the LUNC lands at the exchange's address but is never credited. Confirmed the hard way by community member Spaceship Racer (reported in Telegram 2026-08-09 16:39). Suggested mitigation: the unwrap UI should warn "withdraw to your own wallet, then transfer to an exchange normally — exchange deposit systems do not credit contract-initiated transfers." Happy to retest once a fix lands — we run automated tooling against the DEX and can verify quickly.
leonardocolucci commented 2026-08-09 14:47:43 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
leonardocolucci commented 2026-08-09 15:42:22 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
leonardocolucci commented 2026-08-10 15:25:58 +00:00 (Migrated from gitlab.com)

mentioned in issue #513

mentioned in issue #513
PlasticDigits commented 2026-08-11 00:26:29 +00:00 (Migrated from gitlab.com)

Approved for investigation

Approved for investigation
PlasticDigits commented 2026-08-11 00:37:12 +00:00 (Migrated from gitlab.com)

Further note: "the displayed amount for wraping is incorrect, the recieve amount is right tho, it displays 9751 but user correctly recieve 9800"

Further note: "the displayed amount for wraping is incorrect, the recieve amount is right tho, it displays 9751 but user correctly recieve 9800"
PlasticDigits commented 2026-08-11 00:37:30 +00:00 (Migrated from gitlab.com)

Fix approved

Fix approved
PlasticDigits commented 2026-08-11 00:49:18 +00:00 (Migrated from gitlab.com)

mentioned in commit df885e0f62

mentioned in commit df885e0f62b9af11c345685266539d50214eb45a
PlasticDigits commented 2026-08-11 00:49:22 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1049

mentioned in merge request !1049
PlasticDigits commented 2026-08-11 00:49:33 +00:00 (Migrated from gitlab.com)

Implemented in !1049 (branch fix/issue-512-unwrap-burn-tax).

Done in this MR (dApp):

  • Unwrap You Receive nets InstantWithdraw burn tax after mapper fee (evidence math: 10k → 9,653)
  • Disclose unwrap fee + burn tax on fee line (W10)
  • Exchange-deposit recipient warning on Swap + /wrap (W11)
  • Wrap display fix: mint quote = fee only (10k @ 200 bps → 9,800, not ~9,751) (W8)
  • Invariants W8–W11 + skills/docs crosslinks; make verify-issue-512

Not in this MR:

  • On-chain InstantWithdraw gross-up in ustr-cmm (economic fix so tax is paid from fee residual) — requires wrap-mapper migrate on columbus-5

Verification: make verify-issue-512 passed twice locally.

Implemented in !1049 (branch `fix/issue-512-unwrap-burn-tax`). **Done in this MR (dApp):** - [x] Unwrap You Receive nets InstantWithdraw burn tax after mapper fee (evidence math: 10k → 9,653) - [x] Disclose unwrap fee + burn tax on fee line (W10) - [x] Exchange-deposit recipient warning on Swap + `/wrap` (W11) - [x] Wrap display fix: mint quote = fee only (10k @ 200 bps → 9,800, not ~9,751) (W8) - [x] Invariants W8–W11 + skills/docs crosslinks; `make verify-issue-512` **Not in this MR:** - [ ] On-chain InstantWithdraw **gross-up** in ustr-cmm (economic fix so tax is paid from fee residual) — requires wrap-mapper migrate on columbus-5 Verification: `make verify-issue-512` passed twice locally.
PlasticDigits commented 2026-08-11 01:06:11 +00:00 (Migrated from gitlab.com)

mentioned in commit 3d950c52d5

mentioned in commit 3d950c52d597d259d92399e52f8293251cb91a2e
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-11 01:06:11 +00:00
PlasticDigits commented 2026-08-11 01:07:20 +00:00 (Migrated from gitlab.com)

Merge sanity check (!1049 → main)

Merged via glab mr merge 331 (no automerge; CI was ci_quota_exceeded, not waited on). Local make verify-issue-512 passed (11/11) on the MR branch before merge.

Acceptance vs #512

Criterion Result
Unwrap You Receive nets burn tax after fee (10k → 9 653) Met
Disclose unwrap fee + burn tax (W10) Met on direct unwrap (/wrap + Swap wrap/unwrap)
Exchange-deposit recipient warning (W11) Met on direct unwrap
Wrap mint quote fee-only (10k → 9 800) Met
Router minimum_receive post-fee pre-tax (R3) Met
Docs/invariants W8–W11 + verify target Met

Remaining gaps / follow-ups (not merge blockers)

  1. Swap unwrap_output routes — When Swap uses nativeRouteInfo.needsUnwrapOutput (CW20 → LUNC via pool + unwrap), You Receive / routerMinReceiveBase are correct, but the W10 fee-line disclosure and W11 exchange-deposit warning only render for direct unwrap (wrapUnwrapType === 'unwrap'). Either extend disclosure to needsUnwrapOutput or explicitly scope W10/W11 to direct unwrap in docs.
  2. Test gaps — No SwapPage component coverage for the exchange warning / burn-tax fee note; no WrapPage UI assertion for unwrap ~9.653 on 10k input (unit math is covered in router.test.ts / nativeTransferTax.test.ts).
  3. Still out of scope — On-chain InstantWithdraw gross-up in ustr-cmm (economic fix so tax is paid from fee residual) remains a separate migrate follow-up.

Manual mainnet retest from the MR test plan is still useful once the dApp ships this build.

## Merge sanity check (!1049 → main) Merged via `glab mr merge 331` (no automerge; CI was `ci_quota_exceeded`, not waited on). Local `make verify-issue-512` passed (11/11) on the MR branch before merge. ### Acceptance vs #512 | Criterion | Result | |---|---| | Unwrap You Receive nets burn tax after fee (10k → 9 653) | Met | | Disclose unwrap fee + burn tax (W10) | Met on direct unwrap (`/wrap` + Swap wrap/unwrap) | | Exchange-deposit recipient warning (W11) | Met on direct unwrap | | Wrap mint quote fee-only (10k → 9 800) | Met | | Router `minimum_receive` post-fee pre-tax (R3) | Met | | Docs/invariants W8–W11 + verify target | Met | ### Remaining gaps / follow-ups (not merge blockers) 1. **Swap `unwrap_output` routes** — When Swap uses `nativeRouteInfo.needsUnwrapOutput` (CW20 → LUNC via pool + unwrap), You Receive / `routerMinReceiveBase` are correct, but the W10 fee-line disclosure and W11 exchange-deposit warning only render for **direct** unwrap (`wrapUnwrapType === 'unwrap'`). Either extend disclosure to `needsUnwrapOutput` or explicitly scope W10/W11 to direct unwrap in docs. 2. **Test gaps** — No `SwapPage` component coverage for the exchange warning / burn-tax fee note; no WrapPage UI assertion for unwrap ~9.653 on 10k input (unit math is covered in `router.test.ts` / `nativeTransferTax.test.ts`). 3. **Still out of scope** — On-chain InstantWithdraw **gross-up** in ustr-cmm (economic fix so tax is paid from fee residual) remains a separate migrate follow-up. Manual mainnet retest from the MR test plan is still useful once the dApp ships this build.
PlasticDigits commented 2026-08-11 01:32:01 +00:00 (Migrated from gitlab.com)

mentioned in issue PlasticDigits2/ustr-cmm#9

mentioned in issue PlasticDigits2/ustr-cmm#9
PlasticDigits commented 2026-08-11 01:32:52 +00:00 (Migrated from gitlab.com)

mentioned in issue #516

mentioned in issue #516
leonardocolucci commented 2026-08-11 11:23:37 +00:00 (Migrated from gitlab.com)

Confirming this also affects cUSTC → USTC unwraps at the same 3.47% — reported independently by CryptoGeezer in Telegram today. Same mechanism (USTC/uusd is burn-taxed identically to LUNC/uluna), so one fix covers both wrapped tokens.

Confirming this also affects **cUSTC → USTC** unwraps at the same 3.47% — reported independently by CryptoGeezer in Telegram today. Same mechanism (USTC/uusd is burn-taxed identically to LUNC/uluna), so one fix covers both wrapped tokens.
PlasticDigits commented 2026-08-15 09:57:08 +00:00 (Migrated from gitlab.com)

mentioned in issue PlasticDigits2/ustr-cmm#13

mentioned in issue PlasticDigits2/ustr-cmm#13
PlasticDigits commented 2026-08-15 10:19:47 +00:00 (Migrated from gitlab.com)

mentioned in issue #521

mentioned in issue #521
PlasticDigits commented 2026-08-15 12:02:23 +00:00 (Migrated from gitlab.com)

On-chain 3.47% stack is retuned (no gross-up)

ustr-cmm#9/#13 shipped asymmetric fees on columbus-5: wrap-mapper fee_wrap_bps=200 / fee_unwrap_bps=51 (code 11574). Product fix is fee split, not InstantWithdraw gross-up. W8–W11 disclosure from this issue stays.

Empirical unwrap 10 000 → ≈9 800 (not 9 653) still needs the #13 live probes + Coolify rebuild of #516. Router unwrap_output follow-up: #523.

This issue stays closed.

## On-chain 3.47% stack is retuned (no gross-up) ustr-cmm#9/#13 shipped asymmetric fees on columbus-5: wrap-mapper **`fee_wrap_bps=200` / `fee_unwrap_bps=51`** (code `11574`). Product fix is fee split, not InstantWithdraw gross-up. W8–W11 disclosure from this issue stays. Empirical unwrap 10 000 → ≈9 800 (not 9 653) still needs the #13 live probes + Coolify rebuild of #516. Router `unwrap_output` follow-up: [#523](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/523). This issue stays **closed**.
PlasticDigits commented 2026-08-16 07:14:04 +00:00 (Migrated from gitlab.com)

mentioned in issue #531

mentioned in issue #531
PlasticDigits commented 2026-08-16 09:55:49 +00:00 (Migrated from gitlab.com)

mentioned in issue #533

mentioned in issue #533
leonardocolucci commented 2026-08-17 21:14:18 +00:00 (Migrated from gitlab.com)

Thanks for the fast fix on the unwrap quote (!1049) and for the follow-through on the wrap-mapper fee retune — much cleaner outcome than a gross-up.

Thanks for the fast fix on the unwrap quote (!1049) and for the follow-through on the wrap-mapper fee retune — much cleaner outcome than a gross-up.
PlasticDigits commented 2026-08-21 11:29:50 +00:00 (Migrated from gitlab.com)

mentioned in issue #587

mentioned in issue #587
PlasticDigits commented 2026-08-23 03:05:42 +00:00 (Migrated from gitlab.com)

mentioned in issue #599

mentioned in issue #599
PlasticDigits commented 2026-08-24 00:30:13 +00:00 (Migrated from gitlab.com)

mentioned in issue #613

mentioned in issue #613
PlasticDigits commented 2026-08-26 04:12:48 +00:00 (Migrated from gitlab.com)

mentioned in issue #661

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