Native-wrap swap path always out of gas: router-mediated single hop priced as direct-to-pair (1.14M limit vs ~1.59M actual) #353

Closed
opened 2026-06-10 01:51:41 +00:00 by Brouie · 23 comments
Brouie commented 2026-06-10 01:51:41 +00:00 (Migrated from gitlab.com)

Summary

Swapping native LUNC to any CW20 through the auto-wrap path (LUNC -> LUNC-C -> token) fails deterministically with the humanized OOG message ("Transaction needed more gas than estimated. Try again..."). The broadcast gas limit is a fixed 1,140,000 while the transaction actually needs ~1.59M — every attempt lands code 11, and retrying can't help because the limit is a constant, not a simulation.

Evidence (LocalTerra, main 83dc192)

  • 4 consecutive identical failures, e.g. tx A125F7C8C09A8CB20DFCD32F79586720F2239B10AB433CDCD81243C3D9082AAF: code 11, gasWanted 1,140,000, aborted at gasUsed 1,187,454 ("out of gas in location: wasm contract").
  • Tx shape: msg1 = wrap_deposit (native funds) to the wrap mapper; msg2 = LUNC-C cw20 send -> router execute_swap_operations with ONE terra_swap op (LUNC-C -> EMBER). The send amount is the post-tax figure (99,502,487 = 100M x 0.995), so the #342 fix is composing the message correctly — this is purely a gas-budget failure.
  • Standalone leg measurements (terrad, same payload shapes):
    • wrap_deposit alone: gasUsed 301,061 — already over WRAP_GAS_LIMIT = 300000 on its own.
    • cw20 send -> router execute_swap_operations, single hop, pool-only: gasUsed 1,283,903.
    • Sum ≈ 1,584,964 vs the 1,140,000 budget. The failed tx's 1,187,454 is just the abort point, not the true cost.

Root cause (source layer)

getGasLimitForTx prices the two messages independently: wrap_deposit -> WRAP_GAS_LIMIT (300,000), and the swap msg -> the single-hop budget (840,000 via gasLimitForExecuteSwapOperations(1)), totalling 1,140,000.

The 840k single-hop budget was calibrated for direct-to-pair swaps — #249 deliberately routes ops.length == 1 straight to the pair contract. The wrap path is the exception: it always sends its single op through the router (router execution + reply handling + the extra CW20 hop), which measures ~1.28M and is never priced by the model. WRAP_GAS_LIMIT is also a hair under the real wrap_deposit cost.

Fix direction

  • When the swap leg is router-mediated because of wrap/unwrap composition, use a router-single-hop budget (measured 1,283,903 — ~1.4M with margin), not the direct-to-pair 840k.
  • Bump WRAP_GAS_LIMIT above the measured 301,061 (e.g. 400k).
  • Same treatment as the rest of the gas cluster: add the terraGas test assertions alongside, like the add-liquidity fix did.

Impact

Native LUNC is the primary retail entry on Terra Classic, and this blocks every native -> CW20 swap end to end. Found running SW-00-03 of #337 — that row stays FAIL until this lands. Cluster sibling of the add-liquidity OOG (fixed), create-pair fee+gas (#345, fixed), and the CW20 -> native unwrap fee estimate (#343, fixed): this is the fourth member, the wrap direction on swap.

cc @PlasticDigits

### Summary Swapping native LUNC to any CW20 through the auto-wrap path (LUNC -> LUNC-C -> token) fails deterministically with the humanized OOG message ("Transaction needed more gas than estimated. Try again..."). The broadcast gas limit is a fixed 1,140,000 while the transaction actually needs ~1.59M — every attempt lands code 11, and retrying can't help because the limit is a constant, not a simulation. ### Evidence (LocalTerra, main 83dc192) - 4 consecutive identical failures, e.g. tx `A125F7C8C09A8CB20DFCD32F79586720F2239B10AB433CDCD81243C3D9082AAF`: code 11, gasWanted 1,140,000, aborted at gasUsed 1,187,454 ("out of gas in location: wasm contract"). - Tx shape: msg1 = `wrap_deposit` (native funds) to the wrap mapper; msg2 = LUNC-C cw20 `send` -> **router** `execute_swap_operations` with ONE `terra_swap` op (LUNC-C -> EMBER). The send amount is the post-tax figure (99,502,487 = 100M x 0.995), so the #342 fix is composing the message correctly — this is purely a gas-budget failure. - Standalone leg measurements (terrad, same payload shapes): - `wrap_deposit` alone: gasUsed **301,061** — already over `WRAP_GAS_LIMIT = 300000` on its own. - cw20 send -> router `execute_swap_operations`, single hop, pool-only: gasUsed **1,283,903**. - Sum ≈ **1,584,964** vs the 1,140,000 budget. The failed tx's 1,187,454 is just the abort point, not the true cost. ### Root cause (source layer) `getGasLimitForTx` prices the two messages independently: `wrap_deposit` -> `WRAP_GAS_LIMIT` (300,000), and the swap msg -> the single-hop budget (840,000 via `gasLimitForExecuteSwapOperations(1)`), totalling 1,140,000. The 840k single-hop budget was calibrated for **direct-to-pair** swaps — #249 deliberately routes `ops.length == 1` straight to the pair contract. The wrap path is the exception: it always sends its single op through the **router** (router execution + reply handling + the extra CW20 hop), which measures ~1.28M and is never priced by the model. `WRAP_GAS_LIMIT` is also a hair under the real `wrap_deposit` cost. ### Fix direction - When the swap leg is router-mediated because of wrap/unwrap composition, use a router-single-hop budget (measured 1,283,903 — ~1.4M with margin), not the direct-to-pair 840k. - Bump `WRAP_GAS_LIMIT` above the measured 301,061 (e.g. 400k). - Same treatment as the rest of the gas cluster: add the terraGas test assertions alongside, like the add-liquidity fix did. ### Impact Native LUNC is the primary retail entry on Terra Classic, and this blocks every native -> CW20 swap end to end. Found running SW-00-03 of #337 — that row stays FAIL until this lands. Cluster sibling of the add-liquidity OOG (fixed), create-pair fee+gas (#345, fixed), and the CW20 -> native unwrap fee estimate (#343, fixed): this is the fourth member, the wrap direction on swap. cc @PlasticDigits
Brouie commented 2026-06-10 02:07:13 +00:00 (Migrated from gitlab.com)

Two additions from continuing the browser pass:

  1. Not wrap-specific — plain CW20->CW20 multi-hop has the same failure. EMBER->JADE->RUBY (2 hops, Expert Mode on, hop spread 3.25% so all guards pass): five identical broadcasts, every one code 11 with gasWanted 1,670,000 vs gasUsed ~1,717,973 (e.g. tx 24517C6A9B9C3EEEEAC4F...). So the 2-hop router budget is ~3% under actual too. Practical effect: #341's preflight fix works (multi-hop now reaches broadcast) but multi-hop execution still fails end to end — on gas instead. Worth treating both calibrations in one pass.

  2. The dapp already disagrees with itself about this cost. The Max-button fee reserve (estimateNativeSwapUlunaFeesTotal) prices the same native wrap-swap at 1,970,000 gas (reserve 55.80 LUNC — verified to the micro-unit against the Max fill), while the broadcast path (getGasLimitForTx summing WRAP_GAS_LIMIT + single-hop) only grants 1,140,000. The reserve-side envelope is the safe one; aligning broadcast limits with it (or with fresh measurements) closes the gap.

Two additions from continuing the browser pass: 1. Not wrap-specific — plain CW20->CW20 multi-hop has the same failure. EMBER->JADE->RUBY (2 hops, Expert Mode on, hop spread 3.25% so all guards pass): five identical broadcasts, every one code 11 with gasWanted 1,670,000 vs gasUsed ~1,717,973 (e.g. tx 24517C6A9B9C3EEEEAC4F...). So the 2-hop router budget is ~3% under actual too. Practical effect: #341's preflight fix works (multi-hop now reaches broadcast) but multi-hop execution still fails end to end — on gas instead. Worth treating both calibrations in one pass. 2. The dapp already disagrees with itself about this cost. The Max-button fee reserve (estimateNativeSwapUlunaFeesTotal) prices the same native wrap-swap at 1,970,000 gas (reserve 55.80 LUNC — verified to the micro-unit against the Max fill), while the broadcast path (getGasLimitForTx summing WRAP_GAS_LIMIT + single-hop) only grants 1,140,000. The reserve-side envelope is the safe one; aligning broadcast limits with it (or with fresh measurements) closes the gap.
Brouie commented 2026-06-10 02:31:07 +00:00 (Migrated from gitlab.com)

mentioned in issue #337

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

mentioned in commit 4eaeba6d12

mentioned in commit 4eaeba6d1219bbf4ec62d936a837279d212cdeb4
PlasticDigits commented 2026-06-10 03:37:25 +00:00 (Migrated from gitlab.com)

MR !854. Router 1-hop 1.4M, 2-hop floor 900k/hop, WRAP 400k. Verify: LUNC→CW20 wrap-swap and 2-hop CW20 swap broadcast succeed on LocalTerra.

MR !854. Router 1-hop 1.4M, 2-hop floor 900k/hop, WRAP 400k. Verify: LUNC→CW20 wrap-swap and 2-hop CW20 swap broadcast succeed on LocalTerra.
PlasticDigits commented 2026-06-10 03:37:26 +00:00 (Migrated from gitlab.com)

mentioned in merge request !854

mentioned in merge request !854
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-06-10 05:31:50 +00:00
PlasticDigits commented 2026-06-10 05:31:51 +00:00 (Migrated from gitlab.com)

mentioned in commit aa15ec1734

mentioned in commit aa15ec1734af7bfe59cac4ddc21279eb436c7637
Brouie commented 2026-06-10 06:29:45 +00:00 (Migrated from gitlab.com)

Verified the merged fix (!854, commit 4eaeba6) at source + unit-test layer on e198dcb. Router/wrap-swap legs now price through gasLimitForRouterExecuteSwapOperations: single-hop 1.4M, multi-hop floored at 900k/hop (2-hop 1.81M), and WRAP_GAS_LIMIT 300k->400k; direct-to-pair swap stays at 840k by design. These clear the on-chain actuals I measured (wrap_deposit 301k, router single-hop 1.28M, 2-hop 1.72M), and estimate + broadcast share the same path so they stay aligned. transactions.test 51/0 plus feeEstimate/hybridSwapGas/batchCancel 14/0; the new assertions encode the observed OOG floor (~1,718,000). Live dapp broadcast of a native wrap-swap and a 2-hop swap is the laptop re-test, in progress.

Verified the merged fix (!854, commit 4eaeba6) at source + unit-test layer on e198dcb. Router/wrap-swap legs now price through gasLimitForRouterExecuteSwapOperations: single-hop 1.4M, multi-hop floored at 900k/hop (2-hop 1.81M), and WRAP_GAS_LIMIT 300k->400k; direct-to-pair swap stays at 840k by design. These clear the on-chain actuals I measured (wrap_deposit 301k, router single-hop 1.28M, 2-hop 1.72M), and estimate + broadcast share the same path so they stay aligned. transactions.test 51/0 plus feeEstimate/hybridSwapGas/batchCancel 14/0; the new assertions encode the observed OOG floor (~1,718,000). Live dapp broadcast of a native wrap-swap and a 2-hop swap is the laptop re-test, in progress.
Brouie commented 2026-06-10 06:51:02 +00:00 (Migrated from gitlab.com)

mentioned in issue #357

mentioned in issue #357
Brouie commented 2026-06-11 03:26:04 +00:00 (Migrated from gitlab.com)

The 2-hop half is still short — fresh live evidence at 3169af0, dapp + Simulated Wallet, EMBER -> JADE -> RUBY (no direct pair), 0.1 EMBER, router execute_swap_operations.

Three attempts, all code 11 against the recalibrated 1,810,000 two-hop budget:

  • E2F9BD8E96B3AD02 gasUsed 1,810,073
  • 7C33FA1C424AFB39 gasUsed 1,810,138
  • 9E92EF6F84BB4A76 gasUsed 1,810,003

Real cost of this route is ~1.8101M — the budget misses by 3-138 gas across blocks, so the "try again" path can never save it. Why it runs heavier than the ~1.72M actuals the recalibration was measured against: the receiver had never held RUBY, so the final hop pays the cold-balance storage write that warm-wallet calibration runs never see. Same class as the add-liq (!843) and create-pair (#345) findings — fixed constants vs route/token variance.

The wrap half IS fixed, verified live: native LUNC -> EMBER wrap-swap tx 28EA7D0F, code 0, gasUsed 1,534,413 vs the new 1,800,000 budget — the old 1.14M would have OOG'd it. That part holds.

Suggestion: bump the per-hop floor 900k -> 950k (2hop -> 1.9M; today's heaviest clears with ~5% headroom), or move router paths to simulated gas instead of constants — third constant-related gas bug in this cluster. SW-00-03 on #337 stays FAIL on the 2-hop leg meanwhile. @PlasticDigits

The 2-hop half is still short — fresh live evidence at 3169af0, dapp + Simulated Wallet, EMBER -> JADE -> RUBY (no direct pair), 0.1 EMBER, router execute_swap_operations. Three attempts, all code 11 against the recalibrated 1,810,000 two-hop budget: - E2F9BD8E96B3AD02 gasUsed 1,810,073 - 7C33FA1C424AFB39 gasUsed 1,810,138 - 9E92EF6F84BB4A76 gasUsed 1,810,003 Real cost of this route is ~1.8101M — the budget misses by 3-138 gas across blocks, so the "try again" path can never save it. Why it runs heavier than the ~1.72M actuals the recalibration was measured against: the receiver had never held RUBY, so the final hop pays the cold-balance storage write that warm-wallet calibration runs never see. Same class as the add-liq (!843) and create-pair (#345) findings — fixed constants vs route/token variance. The wrap half IS fixed, verified live: native LUNC -> EMBER wrap-swap tx 28EA7D0F, code 0, gasUsed 1,534,413 vs the new 1,800,000 budget — the old 1.14M would have OOG'd it. That part holds. Suggestion: bump the per-hop floor 900k -> 950k (2hop -> 1.9M; today's heaviest clears with ~5% headroom), or move router paths to simulated gas instead of constants — third constant-related gas bug in this cluster. SW-00-03 on #337 stays FAIL on the 2-hop leg meanwhile. @PlasticDigits
Brouie commented 2026-06-12 04:34:14 +00:00 (Migrated from gitlab.com)

mentioned in merge request !869

mentioned in merge request !869
Brouie commented 2026-06-12 04:39:15 +00:00 (Migrated from gitlab.com)

Reopen evidence — the 2-hop half of this is still broken at main 8408689. The wrap fix landed, but the multi-hop router gas floor was never raised, so 2-hop swaps still OOG deterministically.

Live repro from the dapp (EMBER->JADE->RUBY, Expert Mode, on-chain):

tx code gasUsed / gasWanted
6CE61EEB 11 OOG 1,810,064 / 1,810,000
E106667E 11 OOG 1,810,134 / 1,810,000
C6FB8192 11 OOG 1,810,206 / 1,810,000

Same knife-edge I flagged on 06-11: gasLimitForRouterExecuteSwapOperations(2) = ROUTER_SWAP_OPS_MIN_GAS_PER_HOP (900k) x2 + 10k safety = exactly 1,810,000, which is precisely what the swap needs — no headroom, tips over every block. The 2-hop test only guarded > 1,718,000 (stale), so it never caught it.

Fix up in !869: floor -> 950k/hop (2-hop 1,910,000, ~100k headroom over the worst observed 1,810,206), single-hop and direct paths untouched, regression guard re-floored above the live number. transactions.test.ts 51/51.

@PlasticDigits this needs reopening — SW-00-03 2-hop on #337 is still a FAIL until !869 (or an equivalent bump) merges. The wrap half you fixed is good; it's just the 2-hop floor that got missed.

Reopen evidence — the 2-hop half of this is still broken at main 8408689. The wrap fix landed, but the multi-hop router gas floor was never raised, so 2-hop swaps still OOG deterministically. Live repro from the dapp (EMBER->JADE->RUBY, Expert Mode, on-chain): | tx | code | gasUsed / gasWanted | |----|------|---------------------| | 6CE61EEB | 11 OOG | 1,810,064 / 1,810,000 | | E106667E | 11 OOG | 1,810,134 / 1,810,000 | | C6FB8192 | 11 OOG | 1,810,206 / 1,810,000 | Same knife-edge I flagged on 06-11: `gasLimitForRouterExecuteSwapOperations(2)` = ROUTER_SWAP_OPS_MIN_GAS_PER_HOP (900k) x2 + 10k safety = exactly 1,810,000, which is precisely what the swap needs — no headroom, tips over every block. The 2-hop test only guarded > 1,718,000 (stale), so it never caught it. Fix up in !869: floor -> 950k/hop (2-hop 1,910,000, ~100k headroom over the worst observed 1,810,206), single-hop and direct paths untouched, regression guard re-floored above the live number. transactions.test.ts 51/51. @PlasticDigits this needs reopening — SW-00-03 2-hop on #337 is still a FAIL until !869 (or an equivalent bump) merges. The wrap half you fixed is good; it's just the 2-hop floor that got missed.
PlasticDigits commented 2026-06-12 07:54:32 +00:00 (Migrated from gitlab.com)

mentioned in commit c492682376

mentioned in commit c492682376bdaab243668060f6b293536fdb13e8
Brouie commented 2026-06-28 23:24:13 +00:00 (Migrated from gitlab.com)

mentioned in merge request !952

mentioned in merge request !952
PlasticDigits commented 2026-06-29 00:07:06 +00:00 (Migrated from gitlab.com)

mentioned in commit 767dcd1e1d

mentioned in commit 767dcd1e1d125b21f18ace8409438e6886d8806b
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:49 +00:00 (Migrated from gitlab.com)

marked as related to #587

marked as related to #587
PlasticDigits commented 2026-08-21 11:59:37 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1113

mentioned in merge request !1113
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:43 +00:00 (Migrated from gitlab.com)

marked as related to #599

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

mentioned in issue #679

mentioned in issue #679
PlasticDigits commented 2026-08-27 00:20:49 +00:00 (Migrated from gitlab.com)

marked as related to #679

marked as related to #679
PlasticDigits commented 2026-08-27 00:20:56 +00:00 (Migrated from gitlab.com)

mentioned in issue #681

mentioned in issue #681
PlasticDigits commented 2026-08-27 00:20:59 +00:00 (Migrated from gitlab.com)

marked as related to #681

marked as related to #681
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#353
No description provided.