bug: LUNC↔USTR wrap+multihop out of gas; disclose LUNC network fee on Swap #587

Closed
opened 2026-08-21 11:29:48 +00:00 by PlasticDigits · 25 comments
PlasticDigits commented 2026-08-21 11:29:48 +00:00 (Migrated from gitlab.com)

Summary

Retail report (2026-08-21): swapping LUNC → USTR on Swap failed with the humanized out-of-gas toast (“Transaction needed more gas than estimated. Try again — gas usage can vary slightly between blocks.”). The trader reserved ~1000 LUNC for fees, held 0 USTC, and only succeeded by doing separate txs: LUNC → cLUNC → UST1 → USTR. They asked (1) where the gas fee comes from (LUNC vs USTC) and (2) to fix direct LUNC ↔ USTR (and reverse) so hop-by-hop is not required.

Bundle into one issue. Do not split “raise wrap+2hop gas” vs “show LUNC network fee” vs “E2E hub path” vs “FAQ/docs”. Those are the same user journey.

Related: #353 (closed: wrap + single-hop router underpriced as direct-pair), #343 (unwrap_output add-on), #213 (Max reserve), #475 (retail gas inventory), #115 / #127 (fee envelope), #507 (wrap UX), #489 (short retail copy), #429 (mainnet extension fee-guard risk acceptance).

Current codebase

A single-tx native LUNC → USTR path already exists. It is wrap + client-BFS router hops, not three user-signed txs. The report is that this path OOGs (or is unreliable) on the hub graph, so traders fall back to manual hops. Network fees are always bank uluna (LUNC) — never USTC / uusd. ~1000 LUNC reserved is far above the fee amount; the toast is a gas limit miss (gasUsed > gasWanted), not “not enough LUNC to pay the fee.”

Layer Behavior today
Fee denom buildTerraClassicFee sets Fee.amount to uluna only at effectiveGasPriceUluna() (floor 28.325). USTC is never a gas denom in this dApp.
Native route findRouteWithNativeSupport substitutes uluna→cLUNC / uusd→cUSTC, then BFS (findRoute, max 4 hops). Sets needsWrapInput / needsUnwrapOutput. Hub graph: cLUNC → UST1 → USTR = 2 hops.
Quote SwapPage nativeRouteInfo branch calls simulateNativeSwap (wrap-fee net then wallet simulate_swap_operations). Does not call indexer GET /route/solve (quoteCw20ViaRouteSolve is CW20↔CW20 only). Hops are pool-only (hybrid omitted).
Submit Same page swapMutation: isWrapOrUnwrap || nativeRouteInfo → executeNativeSwap. Native input: one executeTerraContractMulti — msg1 treasury.wrap_deposit (gross native), msg2 cLUNC send → router execute_swap_operations (post–wrap-fee amount). Native output: unwrap_output: true (+ UNWRAP_GAS_LIMIT 800k, #343).
Gas wanted broadcastTerraExecuteContracts uses estimateTerraClassicFeeForEntries = sum of getGasLimitForTx per msg. Wrap = WRAP_GAS_LIMIT 400k (~301k measured, #353). Router N-hop = gasLimitForRouterExecuteSwapOperations: 1-hop 1.4M; N≥2 floor ROUTER_SWAP_OPS_MIN_GAS_PER_HOP 950k + 10k margin (#353: 900k/hop sat on gasUsed 1,810,064 vs wanted 1,810,000).
Numeric envelope (current constants) Wrap+2hop = 400k + 1,910,000 = 2,310,000 gas → ~65.43 LUNC fee. Wrap+2hop+unwrap = 3,110,000 → ~88 LUNC. Flat hybrid ceiling 15M → ~425 LUNC. 1000 LUNC covers the fee amount; OOG means the limit was too low (or the wallet undershot it).
#353 coverage gap #353 fixed wrap + router single hop (LUNC→EMBER). E2E wrap-swap.spec.ts covers LUNC→EMBER, LUNC→USTC, LUNC↔cLUNC — not LUNC↔USTR (hub 2-hop). terraGasRetailInventory.ts has wrap_deposit and send→2hop separately, not the combined multi-msg envelope.
Why hop-by-hop worked Direct wrap 400k; each CW20 hop uses indexer hybrid + per-hop 840k / 1.4M. Those limits are adequate alone. Combined wrap+2hop on economic hub pairs (discount registry, larger state) can exceed the gem-calibrated 1.91M router floor + 400k wrap sum. Docs still say 900k/hop in docs/frontend.md while code is 950k.
Swap fee UX Trade summary Fee row is the AMM fee_bps, not network gas. TerraClassicTxFeeHint exists (“Network fee (est.): ~X LUNC”) but is not mounted on Swap (only its own Vitest). Max already subtracts estimateNativeSwapUlunaFeesTotal (hopCount from nativeRouteInfo.operations.length). No Swap submit gate equivalent to #132/#147 when bank LUNC < fee (Max is the only reserve).
Error copy humanizeTerraTxError maps /out of gas/i to the screenshot string. Retrying the same constant limit cannot succeed (#353).
Indexer GET /route/solve is CW20 addresses. Native uluna is out of scope for this issue (follow-up only if the wrap+BFS path still cannot quote after gas is fixed).

Why this is needed

Native LUNC is the retail gas token and the primary on-ramp. Hub destination USTR is two CW20 hops after wrap. If the composed tx OOGs, users burn the failed-tx fee, conclude “direct swap is broken,” and burn three successful fees instead. The USTC question is a product bug: Swap never says the network fee is LUNC, and the AMM Fee row looks like it might be the gas line.

Constraints / guardrails

  1. Fee denom stays uluna. Do not add USTC / uusd / multi-denom fees. Terra Classic dApp fee math is LUNC-only (MIN_GAS_PRICE_ULUNA 28.325).
  2. Do not LCD-simulate as the sole envelope (AGENTS_TERRACLASSIC_GAS). Wallet extensions rewrite gas; dApp supplies explicit Fee. If LCD sim is used for tuning, new constants must still exceed measured gasUsed with margin.
  3. If gasUsed from a failed tx is known, new defaults must exceed it with wasm/storage headroom (Classic variance is not tight). Same rule as #353 / #115.
  4. Keep hop math in lockstep with packages/localnet-trading-swarm/src/gas.ts when changing ROUTER_* / SWAP_GAS_* / wrap combo floors.
  5. Retail inventory (#475). Any new execute shape or combined wrap+router fixture must land in terraGasRetailInventory.ts + getGasLimitForTx / estimateTerraClassicFeeForEntries. Do not fall through to BASE_GAS_LIMIT (200k) or legacy SWAP_GAS_LIMIT (600k) if send.msg atob fails — a parse miss on wrap+2hop would grant 400k+600k = 1.0M and OOG deterministically.
  6. Max reserve (#213). If gas limits rise, estimateNativeSwapUlunaFeesTotal / Max on native LUNC pay must use the same envelope as broadcast (including unwrap add-on). No magic uluna constants in UI.
  7. Copy (#489). No always-on gas essay (“You pay network gas in LUNC…”). Live Network fee row: ~X LUNC (≤ ~5 word label). Do not show uluna / gas × price on the primary Swap card (current TerraClassicTxFeeHint parenthetical is too technical for default chrome — compact or Swap-specific). Optional internals only inside Trade details <details>.
  8. AMM Fee row stays AMM. Do not rename swap fee_bps to “Network fee.” Two different things.
  9. Native path stays pool-only unless a follow-up adds indexer hybrid. Do not attach default hybrid / book_input > 0 to wrap+multihop in this issue — that would jump toward HYBRID_SWAP_GAS_LIMIT (15M) per hop (#249 / #260) and blow retail fees.
  10. Do not enable mainnet extension fee-guard here (#429 / SEC-E08). Diagnose wallet undershoot separately; this issue is the dApp envelope + disclosure.
  11. Wrap economics unchanged. Gross wrap_deposit coins; CW20 send = post–fee_wrap_bps mint (#512 / #516). minimum_receive on unwrap = post-fee pre-tax (R3).
  12. Out of scope. Indexer route/solve for native uluna/uusd. Changing wrap-mapper bps. Station-on-LocalTerra fee QA (#235). New LUNC/USTR factory pair (routing already has cLUNC–UST1–USTR).

Relevant files

Path Role
frontend-dapp/src/utils/constants.ts WRAP_GAS_LIMIT, UNWRAP_GAS_LIMIT, ROUTER_*, SWAP_GAS_*, MIN_GAS_PRICE_ULUNA
frontend-dapp/src/services/terraclassic/terraGas.ts getGasLimitForTx, gasLimitForRouterExecuteSwapOperations, totalGasLimitForExecuteMsgs
frontend-dapp/src/services/terraclassic/terraClassicFeeEstimate.ts Combined multi-msg envelope used at broadcast
frontend-dapp/src/services/terraclassic/terraBroadcast.ts Signs Fee.gasLimit from that estimate
frontend-dapp/src/services/terraclassic/transactions.ts estimateNativeSwapUlunaFeesTotal
frontend-dapp/src/services/terraclassic/router.ts findRouteWithNativeSupport, executeNativeSwap, simulateNativeSwap
frontend-dapp/src/pages/SwapPage.tsx Native quote/submit; mount network-fee hint
frontend-dapp/src/components/common/TerraClassicTxFeeHint.tsx Reuse / compact for Swap
frontend-dapp/src/utils/humanizeTerraTxError.ts OOG retail copy (keep; fix the limit)
frontend-dapp/src/services/terraclassic/terraGasRetailInventory.ts Combined wrap+2hop (+unwrap) fixtures
frontend-dapp/src/services/terraclassic/__tests__/transactions.test.ts Envelope assertions
frontend-dapp/src/utils/__tests__/maxSpendableAmount.test.ts Native Max reserve tracks new limits
frontend-dapp/e2e/wrap-swap.spec.ts Add hub LUNC↔USTR (or LocalTerra cLUNC–UST1–USTR stand-in)
packages/localnet-trading-swarm/src/gas.ts Lockstep hop floors
docs/frontend.md Gas table: 950k/hop + wrap+N-hop combo; fee denom
skills/AGENTS_TERRACLASSIC_GAS.md Playbook row for wrap+multihop
NATIVE_TOKEN_WRAPPING.md Architecture (do not contradict)
AGENTS.md make verify-issue-<iid>
  1. Measure first. On LocalTerra (hub-like path: wrap + cLUNC→UST1→USTR, and reverse unwrap), record gas_used for the combined two-msg tx — not wrap alone + hop alone. If LocalTerra seed lacks UST1/USTR, use the shortest ≥2 hop wrap+router path that exists and document the stand-in. Prefer a failed or successful columbus-5 hash if the reporter can share one (gasWanted / gasUsed).
  2. Fix the envelope, not the product shape. Keep executeNativeSwap (wrap + router ops). Raise whatever is short:
    • combo multi-msg overhead on totalGasLimitForExecuteMsgs / wrap+router entries, and/or
    • ROUTER_SWAP_OPS_MIN_GAS_PER_HOP / wrap+N-hop floor so wrap+2hop (and wrap+2hop+unwrap) exceed measured gasUsed with the same margin discipline as #353.
      Do not “fix” by telling users to hop manually.
  3. Guard parse fallback. Unit-test that a wrap+send→execute_swap_operations (2–4 ops, optional unwrap_output) never prices the send leg as 600k/BASE. If atob fails, fail closed (throw / refuse broadcast) rather than under-gas.
  4. Disclose LUNC network fee on Swap. When a quote is ready, show Network fee (est.) ~X LUNC from the same estimateTerraClassicFeeForEntries / estimateNativeSwapUlunaFeesTotal as submit (testid e.g. swap-network-fee). CW20-only swaps too (they also pay LUNC). Label answers “LUNC, not USTC.”
  5. Docs / skill. Correct 900k→950k drift. Add wrap+2hop / wrap+2hop+unwrap rows. One FAQ sentence is enough: Terra Classic network fees on this dApp are paid in LUNC (uluna). USTC is not used for gas. Prefer the live Swap row over a new incident-FAQ section (user-incident-faq.md is pause/blacklist, not gas school).
  6. E2E. Playwright e2e-tx (1 worker): LUNC→USTR (or documented stand-in) one submit, success, no OOG toast; reverse USTR→LUNC with unwrap. Keep existing wrap-swap cases.

Acceptance criteria

  1. Selecting LUNC → USTR on Swap quotes and submits one tx (wrap + router hops). It succeeds on LocalTerra hub-equivalent path; hop-by-hop is not required.
  2. USTR → LUNC succeeds in one tx (unwrap_output + unwrap gas add-on).
  3. Combined wrap+≥2hop gasWanted > measured gasUsed with documented margin (not sitting on the ceiling like #353’s 1,810,000).
  4. Network fees remain uluna / LUNC only. 0 USTC does not block a LUNC-funded swap.
  5. Swap shows Network fee (est.) ~X LUNC aligned with the broadcast envelope. No USTC, no uluna/gas× on the primary card.
  6. AMM Fee row remains pool/discount bps — not relabeled as network fee.
  7. Native LUNC Max still reserves the updated envelope (#213).
  8. Retail inventory + transactions.test.ts cover wrap+2hop and wrap+2hop+unwrap combined limits.
  9. docs/frontend.md + AGENTS_TERRACLASSIC_GAS.md + make verify-issue-<iid>.
  10. Existing #353 / #343 / #475 / wrap-swap cases still pass.

Test plan (all paths)

Unit (Vitest)

  1. Fee denom — buildTerraClassicFee amount denom is uluna for wrap, wrap+2hop, unwrap_output, CW20 2hop.
  2. Wrap+1hop — combined ≥ WRAP_GAS_LIMIT + ROUTER_SINGLE_HOP_GAS_LIMIT (1.8M today); no regression vs #353.
  3. Wrap+2hop — combined = wrap + gasLimitForRouterExecuteSwapOperations(2) (+ any new overhead); > 2,310,000 if constants rise; never 1.0M (400k+600k fallback).
  4. Wrap+3hop / +4hop — scales with hop floor; Max reserve increases.
  5. USTR→LUNC / unwrap_output — send inner adds UNWRAP_GAS_LIMIT; wrap+2hop+unwrap > wrap+2hop.
  6. Parse failure — malformed send.msg does not silently grant 600k for a router swap (throw or explicit high fallback — pick one and test).
  7. estimateNativeSwapUlunaFeesTotal — hopCount: 2, needsWrapInput: true matches estimateTerraClassicFeeForEntries of the executeNativeSwap msg pair; needsUnwrapOutput: true matches.
  8. Max — native LUNC Max spendable drops when wrap+2hop reserve rises; CW20 Max unchanged.
  9. Inventory — new combined fixtures in RETAIL_GAS_SHAPE_FIXTURES or an adjacent combined-envelope table; make verify-issue-475 still green.
  10. Swap RTL — with native LUNC→USTR mock quote, swap-network-fee shows LUNC and not USTC; AMM swap-fee-row still present.
  11. Copy — network-fee label ≤ ~5 words; no “You pay gas in LUNC because…” paragraph on the card.
  12. Humanize — OOG string unchanged (regression humanizeTerraTxError.test.ts).
  13. Swarm lockstep — if ROUTER_SWAP_OPS_MIN_GAS_PER_HOP changes, swarm gas.ts matches (or a drift test fails).

E2E (e2e-tx, 1 worker; LocalTerra)

  1. LUNC → USTR (or documented ≥2hop stand-in): fill amount, quote settles, one Swap submit, assertTxResultAlert success; no OOG toast.
  2. USTR → LUNC: one submit, unwrap path, success.
  3. LUNC → cLUNC direct wrap still succeeds (E4).
  4. LUNC → EMBER wrap+1hop still succeeds (#353 / E1).
  5. Network fee visible before submit (swap-network-fee contains LUNC).
  6. 0 USTC: do not require a uusd balance for LUNC→USTR.
  7. Smoke wrap-swap E2–E6 / pause / rate-limit still pass.

Manual / mainnet (if hash available)

  1. Record columbus-5 gasWanted / gasUsed for LUNC→USTR wrap+2hop. New limit must exceed gasUsed. Attach hash on the issue when implementing.

Test plan (attack, hack, abuse)

  1. Fee grief via inflated gas — do not default wrap+multihop to 15M hybrid. Assert wrap+2hop fee stays in the tens-of-LUNC class at 28.325 (not hundreds) unless hybrid is explicitly on the wire.
  2. Hybrid injection — executeNativeSwap must not accept attacker hybrid / book_input from query string or indexer on this path. Ops come from client BFS findRoute only.
  3. Hop-count spoof — UI hopCount for Max/fee hint must equal operations.length actually submitted. A crafted shorter hint must not produce a smaller Fee.gas than execute.
  4. send.msg tamper / decode fail — refuse undersized fallback rather than broadcasting 600k against a real router payload.
  5. Wallet undershoot — document: mainnet extension may still cut gas_wanted (#429). Do not treat that as “raise constants to 15M.” LocalTerra post-sign guard remains LocalTerra-only.
  6. Insufficient LUNC after Max — if user edits pay above Max, submit must not leave bank uluna < Fee.amount (spend + fee). Prefer blocking CTA over OOG/insufficient-fee after sign.
  7. Wrap amount vs fee drain — wrap_deposit coins are the swap amount; fee is a separate Fee.amount from remaining bank LUNC. Do not wrap the fee reserve into the deposit (would OOG or fail bank).
  8. Slippage / sandwich — raising gas must not disable max_spread / minimum_receive. OOG fix is not a license to widen default slippage.
  9. Gem-bridge — production still rejects gem hops on economic quotes (#562). LUNC→USTR must not route through RUBY/EMBER.
  10. XSS in fee hint — fee string is formatted number + LUNC; no raw error.message / denom interpolation from LCD.
  11. DoS via 4-hop BFS — cap remains 4; gas scales; Max reserve must cover 4-hop wrap so a long route cannot empty the wallet then fail fee.
  12. Dust wrap — tiny LUNC→USTR still uses the full wrap+2hop limit (cannot shrink gasWanted with amount). Fee is paid even if swap is dust — same as today; document, do not add a hidden min that looks like a pause.

Verification criteria

# Unit / inventory (no chain)
bash scripts/with-node.sh --cwd frontend-dapp -- npm run test:run -- \
  src/services/terraclassic/__tests__/transactions.test.ts \
  src/services/terraclassic/__tests__/terraGas.retailShapes.test.ts \
  src/utils/__tests__/maxSpendableAmount.test.ts \
  src/utils/__tests__/humanizeTerraTxError.test.ts \
  src/components/common/__tests__/TerraClassicTxFeeHint.test.tsx \
  src/pages/SwapPage.test.tsx
make verify-issue-475
make verify-issue-<iid>    # added by implementation

# Chain (Cloud Agent: make setup-cloud-localterra first)
bash scripts/with-node.sh --cwd frontend-dapp -- npx playwright test \
  e2e/wrap-swap.spec.ts --project=e2e-tx

Manual on /: LUNC→USTR shows Route LUNC → cLUNC → UST1 → USTR (or equivalent), Network fee ~N LUNC, 0 USTC in wallet, one Swap, success. Reverse USTR→LUNC one tx. Hop-by-hop must be unnecessary.

Done when ACs 1–10 pass, verify script is green, and wrap+multihop + LUNC fee denom are in docs/frontend.md + AGENTS_TERRACLASSIC_GAS.md.

## Summary Retail report (2026-08-21): swapping **LUNC → USTR** on Swap failed with the humanized out-of-gas toast (*“Transaction needed more gas than estimated. Try again — gas usage can vary slightly between blocks.”*). The trader reserved **~1000 LUNC** for fees, held **0 USTC**, and only succeeded by doing **separate txs**: LUNC → cLUNC → UST1 → USTR. They asked (1) **where the gas fee comes from** (LUNC vs USTC) and (2) to **fix direct LUNC ↔ USTR** (and reverse) so hop-by-hop is not required. Bundle into **one** issue. Do **not** split “raise wrap+2hop gas” vs “show LUNC network fee” vs “E2E hub path” vs “FAQ/docs”. Those are the same user journey. Related: [#353](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/353) (closed: wrap + **single-hop** router underpriced as direct-pair), [#343](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/343) (`unwrap_output` add-on), [#213](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/213) (Max reserve), [#475](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/475) (retail gas inventory), [#115](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/115) / [#127](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/127) (fee envelope), [#507](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/507) (wrap UX), [#489](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/489) (short retail copy), [#429](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/429) (mainnet extension fee-guard risk acceptance). ## Current codebase A **single-tx** native LUNC → USTR path **already exists**. It is wrap + client-BFS router hops, not three user-signed txs. The report is that this path **OOGs** (or is unreliable) on the hub graph, so traders fall back to manual hops. Network fees are **always bank `uluna` (LUNC)** — never USTC / `uusd`. ~1000 LUNC reserved is far above the **fee amount**; the toast is a **gas limit** miss (`gasUsed > gasWanted`), not “not enough LUNC to pay the fee.” | Layer | Behavior today | |-------|----------------| | **Fee denom** | [`buildTerraClassicFee`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/terraGas.ts) sets `Fee.amount` to **`uluna` only** at `effectiveGasPriceUluna()` (floor **28.325**). USTC is never a gas denom in this dApp. | | **Native route** | [`findRouteWithNativeSupport`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/router.ts) substitutes `uluna`→cLUNC / `uusd`→cUSTC, then BFS (`findRoute`, max 4 hops). Sets `needsWrapInput` / `needsUnwrapOutput`. Hub graph: **cLUNC → UST1 → USTR** = **2 hops**. | | **Quote** | [`SwapPage`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/SwapPage.tsx) `nativeRouteInfo` branch calls [`simulateNativeSwap`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/router.ts) (wrap-fee net then wallet `simulate_swap_operations`). **Does not** call indexer `GET /route/solve` (`quoteCw20ViaRouteSolve` is CW20↔CW20 only). Hops are **pool-only** (`hybrid` omitted). | | **Submit** | Same page `swapMutation`: `isWrapOrUnwrap \|\| nativeRouteInfo` → [`executeNativeSwap`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/router.ts). Native input: **one** `executeTerraContractMulti` — msg1 `treasury.wrap_deposit` (gross native), msg2 cLUNC `send` → router `execute_swap_operations` (post–wrap-fee amount). Native output: `unwrap_output: true` (+ [`UNWRAP_GAS_LIMIT`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/constants.ts) **800k**, #343). | | **Gas wanted** | [`broadcastTerraExecuteContracts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/terraBroadcast.ts) uses [`estimateTerraClassicFeeForEntries`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/terraClassicFeeEstimate.ts) = **sum** of `getGasLimitForTx` per msg. Wrap = `WRAP_GAS_LIMIT` **400k** (~301k measured, #353). Router N-hop = [`gasLimitForRouterExecuteSwapOperations`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/terraGas.ts): 1-hop **1.4M**; N≥2 floor **`ROUTER_SWAP_OPS_MIN_GAS_PER_HOP` 950k** + 10k margin (#353: 900k/hop sat on `gasUsed` 1,810,064 vs wanted 1,810,000). | | **Numeric envelope (current constants)** | Wrap+**2hop** = 400k + 1,910,000 = **2,310,000** gas → **~65.43 LUNC** fee. Wrap+2hop+unwrap = **3,110,000** → **~88 LUNC**. Flat hybrid ceiling 15M → ~425 LUNC. **1000 LUNC covers the fee amount**; OOG means the **limit** was too low (or the wallet undershot it). | | **#353 coverage gap** | #353 fixed wrap + **router single hop** (LUNC→EMBER). E2E [`wrap-swap.spec.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/e2e/wrap-swap.spec.ts) covers LUNC→EMBER, LUNC→USTC, LUNC↔cLUNC — **not** LUNC↔USTR (hub 2-hop). [`terraGasRetailInventory.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/terraGasRetailInventory.ts) has wrap_deposit and send→2hop **separately**, not the **combined** multi-msg envelope. | | **Why hop-by-hop worked** | Direct wrap 400k; each CW20 hop uses indexer hybrid + per-hop 840k / 1.4M. Those limits are adequate **alone**. Combined wrap+2hop on **economic** hub pairs (discount registry, larger state) can exceed the gem-calibrated 1.91M router floor + 400k wrap sum. Docs still say 900k/hop in [`docs/frontend.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/frontend.md#terra-classic-gas-limits) while code is **950k**. | | **Swap fee UX** | Trade summary **Fee** row is the **AMM `fee_bps`**, not network gas. [`TerraClassicTxFeeHint`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/common/TerraClassicTxFeeHint.tsx) exists (**“Network fee (est.): ~X LUNC”**) but is **not mounted** on Swap (only its own Vitest). Max already subtracts [`estimateNativeSwapUlunaFeesTotal`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/transactions.ts) (`hopCount` from `nativeRouteInfo.operations.length`). No Swap submit gate equivalent to #132/#147 when bank LUNC &lt; fee (Max is the only reserve). | | **Error copy** | [`humanizeTerraTxError`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/humanizeTerraTxError.ts) maps `/out of gas/i` to the screenshot string. Retrying the **same** constant limit cannot succeed (#353). | | **Indexer** | `GET /route/solve` is CW20 addresses. Native `uluna` is **out of scope** for this issue (follow-up only if the wrap+BFS path still cannot quote after gas is fixed). | ## Why this is needed Native **LUNC** is the retail gas token and the primary on-ramp. Hub destination **USTR** is two CW20 hops after wrap. If the composed tx OOGs, users burn the failed-tx fee, conclude “direct swap is broken,” and burn **three** successful fees instead. The USTC question is a product bug: Swap never says the network fee is **LUNC**, and the AMM **Fee** row looks like it might be the gas line. ## Constraints / guardrails 1. **Fee denom stays `uluna`.** Do not add USTC / `uusd` / multi-denom fees. Terra Classic dApp fee math is LUNC-only (`MIN_GAS_PRICE_ULUNA` 28.325). 2. **Do not LCD-simulate as the sole envelope** (`AGENTS_TERRACLASSIC_GAS`). Wallet extensions rewrite gas; dApp supplies explicit `Fee`. If LCD sim is used for **tuning**, new constants must still exceed measured `gasUsed` with margin. 3. **If `gasUsed` from a failed tx is known**, new defaults must exceed it with wasm/storage headroom (Classic variance is not tight). Same rule as #353 / #115. 4. **Keep hop math in lockstep** with [`packages/localnet-trading-swarm/src/gas.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/packages/localnet-trading-swarm/src/gas.ts) when changing `ROUTER_*` / `SWAP_GAS_*` / wrap combo floors. 5. **Retail inventory (#475).** Any new execute shape or combined wrap+router fixture must land in `terraGasRetailInventory.ts` + `getGasLimitForTx` / `estimateTerraClassicFeeForEntries`. Do not fall through to `BASE_GAS_LIMIT` (200k) or legacy `SWAP_GAS_LIMIT` (600k) if `send.msg` `atob` fails — a parse miss on wrap+2hop would grant **400k+600k = 1.0M** and OOG deterministically. 6. **Max reserve (#213).** If gas limits rise, `estimateNativeSwapUlunaFeesTotal` / Max on native LUNC pay must use the **same** envelope as broadcast (including unwrap add-on). No magic uluna constants in UI. 7. **Copy (#489).** No always-on gas essay (“You pay network gas in LUNC…”). Live **Network fee** row: `~X LUNC` (≤ ~5 word label). Do **not** show `uluna` / `gas × price` on the primary Swap card (current `TerraClassicTxFeeHint` parenthetical is too technical for default chrome — compact or Swap-specific). Optional internals only inside **Trade details** `<details>`. 8. **AMM Fee row stays AMM.** Do not rename swap `fee_bps` to “Network fee.” Two different things. 9. **Native path stays pool-only unless a follow-up adds indexer hybrid.** Do **not** attach default hybrid / `book_input > 0` to wrap+multihop in this issue — that would jump toward `HYBRID_SWAP_GAS_LIMIT` (15M) per hop (#249 / #260) and blow retail fees. 10. **Do not enable mainnet extension fee-guard** here (#429 / SEC-E08). Diagnose wallet undershoot separately; this issue is the dApp envelope + disclosure. 11. **Wrap economics unchanged.** Gross `wrap_deposit` coins; CW20 send = post–`fee_wrap_bps` mint (#512 / #516). `minimum_receive` on unwrap = post-fee pre-tax (R3). 12. **Out of scope.** Indexer `route/solve` for native `uluna`/`uusd`. Changing wrap-mapper bps. Station-on-LocalTerra fee QA (#235). New LUNC/USTR factory pair (routing already has cLUNC–UST1–USTR). ## Relevant files | Path | Role | |------|------| | [`frontend-dapp/src/utils/constants.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/constants.ts) | `WRAP_GAS_LIMIT`, `UNWRAP_GAS_LIMIT`, `ROUTER_*`, `SWAP_GAS_*`, `MIN_GAS_PRICE_ULUNA` | | [`frontend-dapp/src/services/terraclassic/terraGas.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/terraGas.ts) | `getGasLimitForTx`, `gasLimitForRouterExecuteSwapOperations`, `totalGasLimitForExecuteMsgs` | | [`frontend-dapp/src/services/terraclassic/terraClassicFeeEstimate.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/terraClassicFeeEstimate.ts) | Combined multi-msg envelope used at broadcast | | [`frontend-dapp/src/services/terraclassic/terraBroadcast.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/terraBroadcast.ts) | Signs `Fee.gasLimit` from that estimate | | [`frontend-dapp/src/services/terraclassic/transactions.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/transactions.ts) | `estimateNativeSwapUlunaFeesTotal` | | [`frontend-dapp/src/services/terraclassic/router.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/router.ts) | `findRouteWithNativeSupport`, `executeNativeSwap`, `simulateNativeSwap` | | [`frontend-dapp/src/pages/SwapPage.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/SwapPage.tsx) | Native quote/submit; mount network-fee hint | | [`frontend-dapp/src/components/common/TerraClassicTxFeeHint.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/common/TerraClassicTxFeeHint.tsx) | Reuse / compact for Swap | | [`frontend-dapp/src/utils/humanizeTerraTxError.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/humanizeTerraTxError.ts) | OOG retail copy (keep; fix the limit) | | [`frontend-dapp/src/services/terraclassic/terraGasRetailInventory.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/terraGasRetailInventory.ts) | Combined wrap+2hop (+unwrap) fixtures | | [`frontend-dapp/src/services/terraclassic/__tests__/transactions.test.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/__tests__/transactions.test.ts) | Envelope assertions | | [`frontend-dapp/src/utils/__tests__/maxSpendableAmount.test.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/__tests__/maxSpendableAmount.test.ts) | Native Max reserve tracks new limits | | [`frontend-dapp/e2e/wrap-swap.spec.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/e2e/wrap-swap.spec.ts) | Add hub LUNC↔USTR (or LocalTerra cLUNC–UST1–USTR stand-in) | | [`packages/localnet-trading-swarm/src/gas.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/packages/localnet-trading-swarm/src/gas.ts) | Lockstep hop floors | | [`docs/frontend.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/frontend.md) | Gas table: 950k/hop + wrap+N-hop combo; fee denom | | [`skills/AGENTS_TERRACLASSIC_GAS.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/skills/AGENTS_TERRACLASSIC_GAS.md) | Playbook row for wrap+multihop | | [`NATIVE_TOKEN_WRAPPING.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/NATIVE_TOKEN_WRAPPING.md) | Architecture (do not contradict) | | [`AGENTS.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/AGENTS.md) | `make verify-issue-<iid>` | ## Recommended direction 1. **Measure first.** On LocalTerra (hub-like path: wrap + cLUNC→UST1→USTR, and reverse unwrap), record `gas_used` for the **combined** two-msg tx — not wrap alone + hop alone. If LocalTerra seed lacks UST1/USTR, use the shortest **≥2 hop** wrap+router path that exists and document the stand-in. Prefer a failed or successful columbus-5 hash if the reporter can share one (`gasWanted` / `gasUsed`). 2. **Fix the envelope, not the product shape.** Keep `executeNativeSwap` (wrap + router ops). Raise whatever is short: - combo **multi-msg overhead** on `totalGasLimitForExecuteMsgs` / wrap+router entries, and/or - `ROUTER_SWAP_OPS_MIN_GAS_PER_HOP` / wrap+N-hop floor so wrap+2hop (and wrap+2hop+unwrap) exceed measured `gasUsed` with the same margin discipline as #353. Do not “fix” by telling users to hop manually. 3. **Guard parse fallback.** Unit-test that a wrap+`send`→`execute_swap_operations` (2–4 ops, optional `unwrap_output`) never prices the send leg as 600k/`BASE`. If `atob` fails, fail closed (throw / refuse broadcast) rather than under-gas. 4. **Disclose LUNC network fee on Swap.** When a quote is ready, show **Network fee (est.) ~X LUNC** from the **same** `estimateTerraClassicFeeForEntries` / `estimateNativeSwapUlunaFeesTotal` as submit (testid e.g. `swap-network-fee`). CW20-only swaps too (they also pay LUNC). Label answers “LUNC, not USTC.” 5. **Docs / skill.** Correct 900k→950k drift. Add wrap+2hop / wrap+2hop+unwrap rows. One FAQ sentence is enough: *Terra Classic network fees on this dApp are paid in LUNC (`uluna`). USTC is not used for gas.* Prefer the live Swap row over a new incident-FAQ section (`user-incident-faq.md` is pause/blacklist, not gas school). 6. **E2E.** Playwright `e2e-tx` (1 worker): LUNC→USTR (or documented stand-in) **one** submit, success, no OOG toast; reverse USTR→LUNC with unwrap. Keep existing wrap-swap cases. ## Acceptance criteria 1. Selecting **LUNC → USTR** on Swap quotes and submits **one** tx (wrap + router hops). It **succeeds** on LocalTerra hub-equivalent path; hop-by-hop is not required. 2. **USTR → LUNC** succeeds in **one** tx (`unwrap_output` + unwrap gas add-on). 3. Combined wrap+≥2hop `gasWanted` **&gt; measured `gasUsed`** with documented margin (not sitting on the ceiling like #353’s 1,810,000). 4. Network fees remain **`uluna` / LUNC only**. 0 USTC does not block a LUNC-funded swap. 5. Swap shows **Network fee (est.) ~X LUNC** aligned with the broadcast envelope. No USTC, no `uluna`/`gas×` on the primary card. 6. AMM **Fee** row remains pool/discount bps — not relabeled as network fee. 7. Native LUNC **Max** still reserves the updated envelope (#213). 8. Retail inventory + `transactions.test.ts` cover wrap+2hop and wrap+2hop+unwrap **combined** limits. 9. `docs/frontend.md` + `AGENTS_TERRACLASSIC_GAS.md` + `make verify-issue-<iid>`. 10. Existing #353 / #343 / #475 / wrap-swap cases still pass. ## Test plan (all paths) ### Unit (Vitest) 1. **Fee denom** — `buildTerraClassicFee` amount denom is `uluna` for wrap, wrap+2hop, unwrap_output, CW20 2hop. 2. **Wrap+1hop** — combined ≥ `WRAP_GAS_LIMIT + ROUTER_SINGLE_HOP_GAS_LIMIT` (1.8M today); no regression vs #353. 3. **Wrap+2hop** — combined = wrap + `gasLimitForRouterExecuteSwapOperations(2)` (+ any new overhead); **&gt;** 2,310,000 if constants rise; never 1.0M (400k+600k fallback). 4. **Wrap+3hop / +4hop** — scales with hop floor; Max reserve increases. 5. **USTR→LUNC / unwrap_output** — send inner adds `UNWRAP_GAS_LIMIT`; wrap+2hop+unwrap &gt; wrap+2hop. 6. **Parse failure** — malformed `send.msg` does not silently grant 600k for a router swap (throw or explicit high fallback — pick one and test). 7. **`estimateNativeSwapUlunaFeesTotal`** — `hopCount: 2`, `needsWrapInput: true` matches `estimateTerraClassicFeeForEntries` of the executeNativeSwap msg pair; `needsUnwrapOutput: true` matches. 8. **Max** — native LUNC Max spendable drops when wrap+2hop reserve rises; CW20 Max unchanged. 9. **Inventory** — new combined fixtures in `RETAIL_GAS_SHAPE_FIXTURES` or an adjacent combined-envelope table; `make verify-issue-475` still green. 10. **Swap RTL** — with native LUNC→USTR mock quote, `swap-network-fee` shows `LUNC` and not `USTC`; AMM `swap-fee-row` still present. 11. **Copy** — network-fee label ≤ ~5 words; no “You pay gas in LUNC because…” paragraph on the card. 12. **Humanize** — OOG string unchanged (regression `humanizeTerraTxError.test.ts`). 13. **Swarm lockstep** — if `ROUTER_SWAP_OPS_MIN_GAS_PER_HOP` changes, swarm `gas.ts` matches (or a drift test fails). ### E2E (`e2e-tx`, 1 worker; LocalTerra) 14. **LUNC → USTR** (or documented ≥2hop stand-in): fill amount, quote settles, **one** Swap submit, `assertTxResultAlert` success; no OOG toast. 15. **USTR → LUNC**: one submit, unwrap path, success. 16. **LUNC → cLUNC** direct wrap still succeeds (E4). 17. **LUNC → EMBER** wrap+1hop still succeeds (#353 / E1). 18. **Network fee visible** before submit (`swap-network-fee` contains `LUNC`). 19. **0 USTC**: do not require a `uusd` balance for LUNC→USTR. 20. Smoke wrap-swap E2–E6 / pause / rate-limit still pass. ### Manual / mainnet (if hash available) 21. Record columbus-5 `gasWanted` / `gasUsed` for LUNC→USTR wrap+2hop. New limit must exceed `gasUsed`. Attach hash on the issue when implementing. ## Test plan (attack, hack, abuse) 1. **Fee grief via inflated gas** — do not default wrap+multihop to 15M hybrid. Assert wrap+2hop fee stays in the tens-of-LUNC class at 28.325 (not hundreds) unless hybrid is explicitly on the wire. 2. **Hybrid injection** — `executeNativeSwap` must not accept attacker `hybrid` / `book_input` from query string or indexer on this path. Ops come from client BFS `findRoute` only. 3. **Hop-count spoof** — UI `hopCount` for Max/fee hint must equal `operations.length` actually submitted. A crafted shorter hint must not produce a smaller `Fee.gas` than execute. 4. **`send.msg` tamper / decode fail** — refuse undersized fallback rather than broadcasting 600k against a real router payload. 5. **Wallet undershoot** — document: mainnet extension may still cut `gas_wanted` (#429). Do not treat that as “raise constants to 15M.” LocalTerra post-sign guard remains LocalTerra-only. 6. **Insufficient LUNC after Max** — if user edits pay above Max, submit must not leave bank uluna &lt; `Fee.amount` (spend + fee). Prefer blocking CTA over OOG/insufficient-fee after sign. 7. **Wrap amount vs fee drain** — `wrap_deposit` coins are the **swap** amount; fee is a separate `Fee.amount` from remaining bank LUNC. Do not wrap the fee reserve into the deposit (would OOG or fail bank). 8. **Slippage / sandwich** — raising gas must not disable `max_spread` / `minimum_receive`. OOG fix is not a license to widen default slippage. 9. **Gem-bridge** — production still rejects gem hops on economic quotes (#562). LUNC→USTR must not route through RUBY/EMBER. 10. **XSS in fee hint** — fee string is formatted number + `LUNC`; no raw `error.message` / denom interpolation from LCD. 11. **DoS via 4-hop BFS** — cap remains 4; gas scales; Max reserve must cover 4-hop wrap so a long route cannot empty the wallet then fail fee. 12. **Dust wrap** — tiny LUNC→USTR still uses the full wrap+2hop limit (cannot shrink `gasWanted` with amount). Fee is paid even if swap is dust — same as today; document, do not add a hidden min that looks like a pause. ## Verification criteria ```bash # Unit / inventory (no chain) bash scripts/with-node.sh --cwd frontend-dapp -- npm run test:run -- \ src/services/terraclassic/__tests__/transactions.test.ts \ src/services/terraclassic/__tests__/terraGas.retailShapes.test.ts \ src/utils/__tests__/maxSpendableAmount.test.ts \ src/utils/__tests__/humanizeTerraTxError.test.ts \ src/components/common/__tests__/TerraClassicTxFeeHint.test.tsx \ src/pages/SwapPage.test.tsx make verify-issue-475 make verify-issue-<iid> # added by implementation # Chain (Cloud Agent: make setup-cloud-localterra first) bash scripts/with-node.sh --cwd frontend-dapp -- npx playwright test \ e2e/wrap-swap.spec.ts --project=e2e-tx ``` Manual on `/`: LUNC→USTR shows Route `LUNC → cLUNC → UST1 → USTR` (or equivalent), **Network fee ~N LUNC**, 0 USTC in wallet, one Swap, success. Reverse USTR→LUNC one tx. Hop-by-hop must be unnecessary. Done when ACs 1–10 pass, verify script is green, and wrap+multihop + LUNC fee denom are in `docs/frontend.md` + `AGENTS_TERRACLASSIC_GAS.md`.
PlasticDigits commented 2026-08-21 11:29:49 +00:00 (Migrated from gitlab.com)

marked as related to #353

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

marked as related to #343

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

marked as related to #475

marked as related to #475
PlasticDigits commented 2026-08-21 11:29:51 +00:00 (Migrated from gitlab.com)

marked as related to #213

marked as related to #213
PlasticDigits commented 2026-08-21 11:29:52 +00:00 (Migrated from gitlab.com)

marked as related to #507

marked as related to #507
PlasticDigits commented 2026-08-21 11:30:28 +00:00 (Migrated from gitlab.com)

No duplicate open issue for wrap + multihop LUNC↔USTR or gas-fee denom UX. Closest closed sibling remains #353 (wrap + single-hop router only).

Indexer confirmation: GET /route/solve is CW20 terra1 only — native uluna/uusd stay on executeNativeSwap (client BFS, pool-only). Still out of scope here; do not add “wrap first for best execution” copy (#489).

Implementer notes not to miss:

  1. Signed gas_wanted uses real operations.length. Max/reserve can briefly default hopCount to 1 while pairs are still loading (hints.hopCount ?? 1) — under-reserves ~13 LUNC vs a 2-hop wrap. Broadcast is unaffected; fix the hint once the route is known (and pass needsUnwrapOutput for USTR→LUNC).
  2. Optional Swap uluna preflight (same pattern as #132 / #147): before submit, require bank LUNC ≥ payRaw + estimateNativeSwapUlunaFeesTotal(...) so a non-Max amount cannot sign then fail fee. Not a substitute for raising the wrap+2hop envelope.
  3. USTR→LUNC fee amount (current constants): 2-hop + unwrap = 2,710,000 gas ≈ 76.76 LUNC (no wrap msg). LUNC→USTR wrap+2hop stays 2,310,000 ≈ 65.43 LUNC.
## Follow-up from codebase / issue search No duplicate open issue for **wrap + multihop LUNC↔USTR** or **gas-fee denom UX**. Closest closed sibling remains [#353](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/353) (wrap + **single-hop** router only). Indexer confirmation: `GET /route/solve` is **CW20 `terra1` only** — native `uluna`/`uusd` stay on `executeNativeSwap` (client BFS, pool-only). Still **out of scope** here; do not add “wrap first for best execution” copy (#489). Implementer notes not to miss: 1. **Signed `gas_wanted` uses real `operations.length`.** Max/reserve can briefly default `hopCount` to **1** while pairs are still loading (`hints.hopCount ?? 1`) — under-reserves ~13 LUNC vs a 2-hop wrap. Broadcast is unaffected; fix the hint once the route is known (and pass `needsUnwrapOutput` for USTR→LUNC). 2. **Optional Swap uluna preflight** (same pattern as #132 / #147): before submit, require bank LUNC ≥ `payRaw + estimateNativeSwapUlunaFeesTotal(...)` so a non-Max amount cannot sign then fail fee. Not a substitute for raising the wrap+2hop envelope. 3. **USTR→LUNC fee amount** (current constants): 2-hop + unwrap = **2,710,000** gas ≈ **76.76 LUNC** (no wrap msg). LUNC→USTR wrap+2hop stays **2,310,000** ≈ **65.43 LUNC**.
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-21 11:59:43 +00:00 (Migrated from gitlab.com)

Implementation in !1113.

  • Wrap+≥2hop combo overhead 400k (WRAP_ROUTER_COMBO_OVERHEAD_GAS) → wrap+2hop 2,710,000 (~76.76 LUNC). Wrap+1hop stays 1.8M.
  • Malformed send.msg throws SendHookGasDecodeError (no 600k fallback).
  • Swap shows Network fee (est.) ~X LUNC (swap-network-fee). AMM Fee row unchanged.
  • Max hopCount defaults to 2 until the route is known. Uluna preflight on submit.
  • make verify-issue-587 green (unit + docs, retested). Playwright E7/E8 added; needs full LocalTerra + indexer.
Implementation in !1113. - Wrap+≥2hop combo overhead 400k (`WRAP_ROUTER_COMBO_OVERHEAD_GAS`) → wrap+2hop **2,710,000** (~76.76 LUNC). Wrap+1hop stays 1.8M. - Malformed `send.msg` throws `SendHookGasDecodeError` (no 600k fallback). - Swap shows **Network fee (est.) ~X LUNC** (`swap-network-fee`). AMM Fee row unchanged. - Max hopCount defaults to 2 until the route is known. Uluna preflight on submit. - `make verify-issue-587` green (unit + docs, retested). Playwright E7/E8 added; needs full LocalTerra + indexer.
PlasticDigits commented 2026-08-22 06:08:18 +00:00 (Migrated from gitlab.com)

mentioned in commit 004929d222

mentioned in commit 004929d2222ad27a900d1fc5b3984f65bc0cb0e2
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-22 06:08:18 +00:00
PlasticDigits commented 2026-08-22 06:28:41 +00:00 (Migrated from gitlab.com)

Post-merge note (!1113 on main)

Merged: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/395

Sanity: make verify-issue-587 green (207 Vitest + docs, retested). Wrap+2hop envelope is 2,710,000 gas (~76.76 LUNC) via WRAP_ROUTER_COMBO_OVERHEAD_GAS 400k. Malformed send.msg throws SendHookGasDecodeError (no 600k fallback). Swap shows Network fee (est.) ~X LUNC; AMM Fee row unchanged.

Still open vs AC:

  1. Playwright e2e/wrap-swap.spec.ts E7/E8 (e2e-tx) need full LocalTerra + indexer — not in verify-issue-587. Confirm gas_used < gas_wanted for LUNC→USTR and USTR→LUNC on hub pairs.
  2. Native Max/hopCount defaults to 2 until the route is known (conservative over-reserve vs wrap+1hop).
  3. send without inner msg still prices as 600k; only bad base64 is fail-closed.
  4. Mainnet extension may still undershoot gas_wanted (#429) — out of scope here.

Do not default wrap+multihop to 15M hybrid.

## Post-merge note (!1113 on `main`) Merged: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/395 **Sanity:** `make verify-issue-587` green (207 Vitest + docs, retested). Wrap+2hop envelope is **2,710,000** gas (~76.76 LUNC) via `WRAP_ROUTER_COMBO_OVERHEAD_GAS` 400k. Malformed `send.msg` throws `SendHookGasDecodeError` (no 600k fallback). Swap shows **Network fee (est.) ~X LUNC**; AMM Fee row unchanged. **Still open vs AC:** 1. Playwright `e2e/wrap-swap.spec.ts` **E7/E8** (`e2e-tx`) need full LocalTerra + indexer — not in `verify-issue-587`. Confirm `gas_used < gas_wanted` for LUNC→USTR and USTR→LUNC on hub pairs. 2. Native Max/`hopCount` defaults to **2** until the route is known (conservative over-reserve vs wrap+1hop). 3. `send` **without** inner `msg` still prices as 600k; only bad base64 is fail-closed. 4. Mainnet extension may still undershoot `gas_wanted` (#429) — out of scope here. Do not default wrap+multihop to 15M hybrid.
PlasticDigits commented 2026-08-22 06:29:12 +00:00 (Migrated from gitlab.com)

mentioned in issue #590

mentioned in issue #590
PlasticDigits commented 2026-08-22 06:29:13 +00:00 (Migrated from gitlab.com)

marked as related to #590

marked as related to #590
PlasticDigits commented 2026-08-22 06:29:29 +00:00 (Migrated from gitlab.com)

Follow-up tracker: #590 (Playwright e2e-tx E7/E8).

Follow-up tracker: #590 (Playwright e2e-tx E7/E8).
PlasticDigits commented 2026-08-22 11:02:34 +00:00 (Migrated from gitlab.com)

mentioned in issue #595

mentioned in issue #595
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:41 +00:00 (Migrated from gitlab.com)

marked as related to #599

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

Follow-up (do not reopen): retail USTR → USTC still OOGs after the #587 wrap+≥2hop bake. LUNC→USTR and USTC→USTR succeed. New ticket #599 covers unwrap_output + 2 hub hops into taxed uusd (no wrap combo). Native path stays pool-only (H596-7).

Follow-up (do not reopen): retail **USTR → USTC** still OOGs after the #587 wrap+≥2hop bake. LUNC→USTR and USTC→USTR succeed. New ticket #599 covers unwrap_output + 2 hub hops into taxed `uusd` (no wrap combo). Native path stays pool-only (H596-7).
PlasticDigits commented 2026-08-23 03:21:26 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1118

mentioned in merge request !1118
PlasticDigits commented 2026-08-23 03:31:22 +00:00 (Migrated from gitlab.com)

mentioned in issue #600

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

mentioned in issue #679

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

marked as related to #679

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

mentioned in issue #681

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

marked as related to #681

marked as related to #681
PlasticDigits commented 2026-08-27 00:37:16 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1182

mentioned in merge request !1182
PlasticDigits commented 2026-08-27 11:52:21 +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#587
No description provided.