PP2- Gas: Swap of CW20-to-native swaps underestimate gas; unwrap sub-message overhead not included in gas limit calculation #343

Closed
opened 2026-06-08 14:09:54 +00:00 by totdking · 20 comments
totdking commented 2026-06-08 14:09:54 +00:00 (Migrated from gitlab.com)
No description provided.
totdking commented 2026-06-08 14:11:19 +00:00 (Migrated from gitlab.com)

Summary

Swapping a CW20 token to LUNC (native output) fails after Keplr approval with:

Transaction needed more gas than estimated. Try again -- gas usage can vary slightly between blocks.

The gas limit computed by the frontend is insufficient for the full execution path. For CW20-to-native swaps, the router appends an extra unwrap sub-message after the final hop -- a CW20 send to the wrap-mapper contract, which then issues a bank send of native coins to the recipient. This extra sub-message is not counted in the gas estimate.

Observed for: EMBER (CW20) to LUNC (native).


Reproduction steps

  1. Navigate to / (Swap page) on LocalTerra
  2. Connect Keplr wallet (funded test account)
  3. Select EMBER as token in, LUNC as token out
  4. Enter an amount
  5. Submit the swap and approve in Keplr
  6. Observe: transaction broadcast succeeds, but execution fails with the out-of-gas message

Expected behavior

The gas estimate should cover all sub-messages in the execution path, including the unwrap sub-message appended by the router when the output token is native. The swap should execute and confirm successfully.


Actual behavior

The gas limit is computed from hop count only. The unwrap sub-message adds overhead that exceeds the safety margin, causing the transaction to run out of gas after broadcast.


Verified root cause (code read and confirmed)

frontend-dapp/src/services/terraclassic/terraGas.ts:156-177 -- getGasLimitForTx handles the CW20 send message:

} else if ('send' in executeMsg) {
  const sendMsg = executeMsg.send as { msg?: string } | undefined
  if (sendMsg?.msg) {
    try {
      const inner = JSON.parse(atob(sendMsg.msg)) as Record<string, unknown>
      // ...
      if ('execute_swap_operations' in inner) return gasLimitForSwapOperationsMsg(inner)
    }
  }
}

gasLimitForSwapOperationsMsg (terraGas.ts:93-100) calls gasLimitForExecuteSwapOperations(hops), where hops = operations.length. This formula is:

scaled  = round(SWAP_GAS_PER_HOP * hops * SWAP_GAS_BUFFER)   // 600000 * n * 1.3
padded  = scaled + hops * SWAP_MULTIHOP_GAS_PADDING_PER_HOP  // + 50000 per hop
floor   = hops * EXECUTE_SWAP_OPS_MIN_GAS_PER_HOP            // 661000 per hop
result  = max(padded, floor) + SWAP_GAS_SAFETY_MARGIN         // + 10000

For a 1-hop swap this yields 840,000. For a 2-hop swap: 1,670,000.

Neither gasLimitForSwapOperationsMsg nor getGasLimitForTx inspects the unwrap_output flag in the hook message. When unwrap_output: true is set (CW20-to-native output swap), the router's final reply_swap_hop (router/src/contract.rs:365-382) appends a CW20 send to the wrap-mapper, which then executes a bank send of native LUNC. This extra sub-message execution is not covered by the hop-count formula and pushes the total gas above the limit.

The UNWRAP_GAS_LIMIT = 400000 constant exists in constants.ts:27 and is used for direct unwrap transactions, but is never added to the swap gas estimate when unwrap_output is true.


Impact assessment

  • Blocks: all CW20-to-native swap directions (any token out is LUNC or USTC)
  • Combined with ISSUE-017 and ISSUE-018, no swap path of any type can successfully execute in the current QA environment
  • PP-2 and all downstream swap-dependent checklist items remain fully blocked

Environment

  • Chain: localterra
  • LCD: http://localhost:1317
  • Wallet: Keplr (Terra Classic)
  • Browser: Chromium
  • Page: / (Swap)
  • Token in: EMBER (CW20)
  • Token out: LUNC (native)
  • Amount: any
  • Network throttle applied: No

Severity: ~gas gas underestimation causes CW20-to-native swap failure on every submission; retry without code change will fail with the same estimate.

Related checklist items: PP-2, PP-3, PP-4

cc: @PlasticDigits

### Summary Swapping a CW20 token to LUNC (native output) fails after Keplr approval with: ``` Transaction needed more gas than estimated. Try again -- gas usage can vary slightly between blocks. ``` The gas limit computed by the frontend is insufficient for the full execution path. For CW20-to-native swaps, the router appends an extra unwrap sub-message after the final hop -- a CW20 send to the wrap-mapper contract, which then issues a bank send of native coins to the recipient. This extra sub-message is not counted in the gas estimate. Observed for: EMBER (CW20) to LUNC (native). --- ### Reproduction steps 1. Navigate to `/` (Swap page) on LocalTerra 2. Connect Keplr wallet (funded test account) 3. Select EMBER as token in, LUNC as token out 4. Enter an amount 5. Submit the swap and approve in Keplr 6. Observe: transaction broadcast succeeds, but execution fails with the out-of-gas message --- ### Expected behavior The gas estimate should cover all sub-messages in the execution path, including the unwrap sub-message appended by the router when the output token is native. The swap should execute and confirm successfully. --- ### Actual behavior The gas limit is computed from hop count only. The unwrap sub-message adds overhead that exceeds the safety margin, causing the transaction to run out of gas after broadcast. --- ### Verified root cause (code read and confirmed) **`frontend-dapp/src/services/terraclassic/terraGas.ts:156-177` -- `getGasLimitForTx` handles the CW20 `send` message:** ```ts } else if ('send' in executeMsg) { const sendMsg = executeMsg.send as { msg?: string } | undefined if (sendMsg?.msg) { try { const inner = JSON.parse(atob(sendMsg.msg)) as Record<string, unknown> // ... if ('execute_swap_operations' in inner) return gasLimitForSwapOperationsMsg(inner) } } } ``` `gasLimitForSwapOperationsMsg` (`terraGas.ts:93-100`) calls `gasLimitForExecuteSwapOperations(hops)`, where `hops = operations.length`. This formula is: ``` scaled = round(SWAP_GAS_PER_HOP * hops * SWAP_GAS_BUFFER) // 600000 * n * 1.3 padded = scaled + hops * SWAP_MULTIHOP_GAS_PADDING_PER_HOP // + 50000 per hop floor = hops * EXECUTE_SWAP_OPS_MIN_GAS_PER_HOP // 661000 per hop result = max(padded, floor) + SWAP_GAS_SAFETY_MARGIN // + 10000 ``` For a 1-hop swap this yields 840,000. For a 2-hop swap: 1,670,000. Neither `gasLimitForSwapOperationsMsg` nor `getGasLimitForTx` inspects the `unwrap_output` flag in the hook message. When `unwrap_output: true` is set (CW20-to-native output swap), the router's final `reply_swap_hop` (`router/src/contract.rs:365-382`) appends a CW20 send to the wrap-mapper, which then executes a bank send of native LUNC. This extra sub-message execution is not covered by the hop-count formula and pushes the total gas above the limit. The `UNWRAP_GAS_LIMIT = 400000` constant exists in `constants.ts:27` and is used for direct unwrap transactions, but is never added to the swap gas estimate when `unwrap_output` is true. --- ### Impact assessment - Blocks: all CW20-to-native swap directions (any token out is LUNC or USTC) - Combined with ISSUE-017 and ISSUE-018, no swap path of any type can successfully execute in the current QA environment - PP-2 and all downstream swap-dependent checklist items remain fully blocked --- ### Environment - Chain: localterra - LCD: [http://localhost:1317](http://localhost:1317) - Wallet: Keplr (Terra Classic) - Browser: Chromium - Page: `/` (Swap) - Token in: EMBER (CW20) - Token out: LUNC (native) - Amount: any - Network throttle applied: No --- **Severity:** ~gas gas underestimation causes CW20-to-native swap failure on every submission; retry without code change will fail with the same estimate. **Related checklist items:** PP-2, PP-3, PP-4 cc: @PlasticDigits
Brouie commented 2026-06-09 02:16:02 +00:00 (Migrated from gitlab.com)

mentioned in merge request !843

mentioned in merge request !843
Brouie commented 2026-06-09 02:17:30 +00:00 (Migrated from gitlab.com)

mentioned in issue #337

mentioned in issue #337
PlasticDigits commented 2026-06-09 06:49:17 +00:00 (Migrated from gitlab.com)

mentioned in commit 09e0849441

mentioned in commit 09e0849441fe413ae7fec722a3f54a2defce63b3
PlasticDigits commented 2026-06-09 06:53:24 +00:00 (Migrated from gitlab.com)

mentioned in merge request !845

mentioned in merge request !845
PlasticDigits commented 2026-06-09 06:53:41 +00:00 (Migrated from gitlab.com)

MR !845 opened (stacked on !844)

Changes: UNWRAP_GAS_LIMIT added when unwrap_output: true in gas limit math.

Verify

  • npm test -- transactions.test.ts (unwrap gas test)
  • EMBER→LUNC swap confirms on LocalTerra
## MR !845 opened (stacked on !844) **Changes:** `UNWRAP_GAS_LIMIT` added when `unwrap_output: true` in gas limit math. ### Verify - [ ] `npm test -- transactions.test.ts` (unwrap gas test) - [ ] EMBER→LUNC swap confirms on LocalTerra
ghost1 commented 2026-06-09 06:53:47 +00:00 (Migrated from gitlab.com)

mentioned in merge request !847

mentioned in merge request !847
ghost1 commented 2026-06-09 06:53:50 +00:00 (Migrated from gitlab.com)

mentioned in merge request !849

mentioned in merge request !849
ghost1 commented 2026-06-09 06:53:52 +00:00 (Migrated from gitlab.com)

mentioned in merge request !846

mentioned in merge request !846
ghost1 commented 2026-06-09 06:53:53 +00:00 (Migrated from gitlab.com)

mentioned in merge request !848

mentioned in merge request !848
ghost1 commented 2026-06-09 06:54:19 +00:00 (Migrated from gitlab.com)

mentioned in merge request !844

mentioned in merge request !844
ghost1 commented 2026-06-09 06:57:19 +00:00 (Migrated from gitlab.com)

mentioned in commit 8f2971ae3a

mentioned in commit 8f2971ae3a8de715109bf76c10544d72d88c9ee5
PlasticDigits commented 2026-06-09 07:07:17 +00:00 (Migrated from gitlab.com)

mentioned in commit f7f56610c4

mentioned in commit f7f56610c4d8e539080dc77590d76b1f9cbb2663
PlasticDigits commented 2026-06-09 08:32:48 +00:00 (Migrated from gitlab.com)

Verification complete — #343

All acceptance and verification criteria PASS on main (83dc192).

Results

Criterion Result Evidence
transactions.test.ts unwrap gas PASS npm test -- transactions.test.ts -t unwrap
EMBER→LUNC swap confirms PASS On-chain tx success alert (no out-of-gas)
UNWRAP_GAS_LIMIT in gas math PASS terraGas.ts adds limit when unwrap_output:true

Also verified EMBER→USTC native-output path via wrap-swap.spec.ts E2e-tx (same unwrap sub-message).

Merged fix: !845.

Re-verify checklist

  • npm test -- transactions.test.ts -t unwrap
  • EMBER→LUNC swap confirms on LocalTerra
## Verification complete — #343 All acceptance and verification criteria **PASS** on `main` (`83dc192`). ### Results | Criterion | Result | Evidence | |-----------|--------|----------| | `transactions.test.ts` unwrap gas | **PASS** | `npm test -- transactions.test.ts -t unwrap` | | EMBER→LUNC swap confirms | **PASS** | On-chain tx success alert (no out-of-gas) | | `UNWRAP_GAS_LIMIT` in gas math | **PASS** | `terraGas.ts` adds limit when `unwrap_output:true` | Also verified EMBER→USTC native-output path via `wrap-swap.spec.ts` E2e-tx (same unwrap sub-message). Merged fix: !845. ### Re-verify checklist - [ ] `npm test -- transactions.test.ts -t unwrap` - [ ] EMBER→LUNC swap confirms on LocalTerra
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-06-09 08:32:50 +00:00
Brouie commented 2026-06-10 01:51:42 +00:00 (Migrated from gitlab.com)

mentioned in issue #353

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

mentioned in issue #587

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

marked as related to #587

marked as related to #587
PlasticDigits commented 2026-08-23 03:05:41 +00:00 (Migrated from gitlab.com)

mentioned in issue #599

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

marked as related to #599

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

mentioned in issue #690

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