Place Limit submits two on-chain transactions and burns gas when user has zero escrow token balance #130

Closed
opened 2026-05-05 12:39:44 +00:00 by totdking · 8 comments
totdking commented 2026-05-05 12:39:44 +00:00 (Migrated from gitlab.com)
No description provided.
totdking commented 2026-05-05 12:49:08 +00:00 (Migrated from gitlab.com)

Issue Summary

Placing a limit order with zero balance of the required escrow token submits two on-chain transactions before failing. The first transaction (increase_allowance CW20 approval) succeeds and consumes gas. The second transaction (place_limit_order) is rejected on-chain because the contract attempts a transfer_from against a zero balance. Both transactions deduct LUNC gas fees from the wallet. The frontend performs no balance check before submission, so the user receives no warning and loses gas on a guaranteed-to-fail operation.


Reproduction Steps

  1. Complete full local setup (LocalTerra running, contracts deployed, indexer running, frontend running at localhost:3000)
  2. Connect a Keplr wallet that has LUNC (for gas) but zero balance of the escrow token for the selected pair
  3. Navigate to /trade and select any token pair
  4. Select either Bid or Ask side note the required escrow token shown
  5. Enter any non-zero amount and click Place Limit
  6. Approve both transactions in Keplr
  7. Observe: first tx (increase_allowance) succeeds, second tx (place_limit_order) is rejected on-chain
  8. Check wallet: LUNC has been deducted for gas on both transactions despite the operation failing

Expected Behavior

Before submitting any transaction, the frontend should compare the entered amount against the user's escrow token balance (already fetched via useLimitOrderEscrowBalance). If the balance is insufficient, the Place Limit button should be disabled and an "Insufficient balance" error should be shown inline. No transaction should be sent.


Actual Behavior

The frontend skips balance validation and sends both transactions regardless of balance:

  1. executeTerraContract → increase_allowance on the CW20 escrow token — succeeds (allowance is a permission grant, does not require balance), gas consumed
  2. placeLimitOrder → contract calls transfer_from, finds zero balance — rejected on-chain, gas consumed

The wallet shows a net LUNC deduction across two gas payments with a final failed state. The UI displays a generic transaction error after the fact.


Screenshots

  1. Pre funded

    Screenshot 2026-05-05 at 13.36.42.png{width=900 height=547}

  2. Carries out tx

Screenshot 2026-05-05 at 13.36.58.png{width=900 height=547}
3. Tx submitted but zero amount of bid/ask required tokens so it reverts.

Screenshot 2026-05-05 at 13.37.14.png{width=900 height=547}
4. Gas consumed, tx failed, user is left with deficit of gas carried out

Screenshot 2026-05-05 at 13.37.29.png{width=900 height=547}

Stages:

  1. Balance: 14.335 lunc
  2. Balance: 14.335 - 5.665, (approve)
  3. Balance: 8.67 lunc after approval but tx fails due to zero amount / insufficient amout of escrow tokens
  4. Post balance: 8.67 lunc

Root Cause

TradeOrderTicket.tsx:88–91 , placeMutation.mutationFn calls executeTerraContract (allowance) then placeLimitOrder without checking escrowBalanceQuery.data against the entered amount. The balance data is already available in the component via useLimitOrderEscrowBalance (line 59) but is only used to populate a display field, not to gate submission.


Environment Details

Field Value
OS macOS (Apple M1 Pro, 14-inch)
Browser Google Chrome (desktop)
Network localterra (local Docker)
Frontend VITE_NETWORK=local npm run dev at localhost:3000
Indexer Running (make indexer-dev)
Contracts Deployed via make deploy-local

Wallet / Device Details

  • Wallet: Keplr browser extension
  • Network configured in Keplr: localterra — RPC http://localhost:26657
  • Wallet state: LUNC balance present (for gas), zero balance of the escrow token for the selected pair

Severity / Impact

Medium. Users lose real LUNC to gas fees when attempting a limit order they cannot afford. The gas loss from the unnecessary increase_allowance transaction is avoidable entirely, it should never be sent if balance validation fails. The fix is low-effort: add a pre-submission guard using the already-fetched escrowBalanceQuery.data in TradeOrderTicket.tsx. The same validation gap exists on the /limits page (LimitOrdersPage.tsx).

cc: @PlasticDigits

### Issue Summary Placing a limit order with zero balance of the required escrow token submits two on-chain transactions before failing. The first transaction (`increase_allowance` CW20 approval) succeeds and consumes gas. The second transaction (`place_limit_order`) is rejected on-chain because the contract attempts a `transfer_from` against a zero balance. Both transactions deduct LUNC gas fees from the wallet. The frontend performs no balance check before submission, so the user receives no warning and loses gas on a **guaranteed-to-fail** operation. --- ### Reproduction Steps 1. Complete full local setup (LocalTerra running, contracts deployed, indexer running, frontend running at `localhost:3000`) 2. Connect a Keplr wallet that has LUNC (for gas) but zero balance of the escrow token for the selected pair 3. Navigate to `/trade` and select any token pair 4. Select either **Bid** or **Ask** side note the required escrow token shown 5. Enter any non-zero amount and click **Place Limit** 6. Approve both transactions in Keplr 7. Observe: first tx (`increase_allowance`) succeeds, second tx (`place_limit_order`) is rejected on-chain 8. Check wallet: LUNC has been deducted for gas on both transactions despite the operation failing --- ### Expected Behavior Before submitting any transaction, the frontend should compare the entered amount against the user's escrow token balance (already fetched via `useLimitOrderEscrowBalance`). If the balance is insufficient, the Place Limit button should be disabled and an "Insufficient balance" error should be shown inline. No transaction should be sent. --- ### Actual Behavior The frontend skips balance validation and sends both transactions regardless of balance: 1. `executeTerraContract` → `increase_allowance` on the CW20 escrow token — **succeeds** (allowance is a permission grant, does not require balance), gas consumed 2. `placeLimitOrder` → contract calls `transfer_from`, finds zero balance — **rejected on-chain**, gas consumed The wallet shows a net LUNC deduction across two gas payments with a final failed state. The UI displays a generic transaction error after the fact. --- ### Screenshots 1. Pre funded ![Screenshot 2026-05-05 at 13.36.42.png](/uploads/691062fe2d00effe79f8a457d5354e0c/Screenshot_2026-05-05_at_13.36.42.png){width=900 height=547} 2. Carries out tx ![Screenshot 2026-05-05 at 13.36.58.png](/uploads/6a81df95d5729dcfa6d0bb4220e4c5f1/Screenshot_2026-05-05_at_13.36.58.png){width=900 height=547} 3. Tx submitted but zero amount of bid/ask required tokens so it reverts. ![Screenshot 2026-05-05 at 13.37.14.png](/uploads/da0f8d32a81a263e88e71d86c8ce2a36/Screenshot_2026-05-05_at_13.37.14.png){width=900 height=547} 4. Gas consumed, tx failed, user is left with deficit of gas carried out ![Screenshot 2026-05-05 at 13.37.29.png](/uploads/a627a0be8f81d792e67371155858021b/Screenshot_2026-05-05_at_13.37.29.png){width=900 height=547} **Stages:** 1. Balance: 14.335 lunc 2. Balance: 14.335 - 5.665, (approve) 3. Balance: 8.67 lunc after approval but tx fails due to zero amount / insufficient amout of escrow tokens 4. Post balance: 8.67 lunc --- ### Root Cause `TradeOrderTicket.tsx:88–91` , `placeMutation.mutationFn` calls `executeTerraContract` (allowance) then `placeLimitOrder` without checking `escrowBalanceQuery.data` against the entered amount. The balance data is already available in the component via `useLimitOrderEscrowBalance` (line 59) but is only used to populate a display field, not to gate submission. --- ### Environment Details | Field | Value | |-------|-------| | OS | macOS (Apple M1 Pro, 14-inch) | | Browser | Google Chrome (desktop) | | Network | `localterra` (local Docker) | | Frontend | `VITE_NETWORK=local npm run dev` at `localhost:3000` | | Indexer | Running (`make indexer-dev`) | | Contracts | Deployed via `make deploy-local` | --- ### Wallet / Device Details - **Wallet:** Keplr browser extension - **Network configured in Keplr:** localterra — RPC `http://localhost:26657` - **Wallet state:** LUNC balance present (for gas), zero balance of the escrow token for the selected pair --- ### Severity / Impact **Medium.** Users lose real LUNC to gas fees when attempting a limit order they cannot afford. The gas loss from the unnecessary `increase_allowance` transaction is avoidable entirely, it should never be sent if balance validation fails. The fix is low-effort: add a pre-submission guard using the already-fetched `escrowBalanceQuery.data` in `TradeOrderTicket.tsx`. The same validation gap exists on the `/limits` page (`LimitOrdersPage.tsx`). cc: @PlasticDigits
PlasticDigits commented 2026-05-05 13:04:48 +00:00 (Migrated from gitlab.com)

We need a modularized, global utiltiy and component for balance validation wherever user is inputing amounts to spend.

We need a modularized, global utiltiy and component for balance validation wherever user is inputing amounts to spend.
PlasticDigits commented 2026-05-05 13:31:11 +00:00 (Migrated from gitlab.com)

mentioned in commit 0d645f7dff

mentioned in commit 0d645f7dff2dbc599f9b28e9734db9310b8f8916
PlasticDigits commented 2026-05-05 13:32:03 +00:00 (Migrated from gitlab.com)

Implemented on main (0d645f7)

Summary: Added a shared preflight gate so the dApp never broadcasts increase_allowance when the limit escrow amount (raw units) exceeds the wallet CW20 balance, or when the balance query is still loading / unreadable. This addresses the two-tx gas burn described in the issue.

Code

  • frontend-dapp/src/utils/limitOrderEscrowBalanceGate.ts — evaluateLimitOrderEscrowPlaceGate (BigInt compare on toRawAmount vs balance string; conservative when uncertain).
  • frontend-dapp/src/components/trade/LimitOrderEscrowPlaceGuardMessage.tsx — inline status / error under Place limit.
  • Wired in TradeOrderTicket.tsx (trade workspace) and LimitOrdersPage.tsx — disabled Place button + mutationFn guard (defense in depth).

Docs / agent playbooks

  • docs/limit-orders.md — dApp retail section: #130 preflight invariants + links to code.
  • skills/AGENTS_TERRACLASSIC_GAS.md — canonical table + rule of thumb for escrow vs balance.
  • skills/AGENTS_LOCALNET_TRADING_SWARM.md — cross-link for localnet QA.

Verification checklist (for QA / reviewers)

  • LocalTerra + dApp: wallet with LUNC only, zero balance of the escrow token for the chosen side → enter amount > 0 → Place limit stays disabled; inline Insufficient balance (or loading copy while balance fetches).
  • Same wallet after minting enough of the escrow token → gate clears; place limit still works end-to-end (allowance + place).
  • /trade order ticket and /limits page both behave the same for bid/ask side switches.
  • With indexer temporarily unavailable for unrelated queries, CW20 balance query still resolves → gate uses real balance (not a regression to blind submit).
  • cd frontend-dapp && npm ci && npm run test:unit passes (includes limitOrderEscrowBalanceGate.test.ts).

@totdking — could you confirm on your LocalTerra + Keplr setup that you no longer get a successful increase_allowance when escrow balance is zero? Leaving the issue open until you verify.

## Implemented on `main` (0d645f7) **Summary:** Added a shared preflight gate so the dApp never broadcasts `increase_allowance` when the limit escrow amount (raw units) exceeds the wallet CW20 balance, or when the balance query is still loading / unreadable. This addresses the two-tx gas burn described in the issue. **Code** - `frontend-dapp/src/utils/limitOrderEscrowBalanceGate.ts` — `evaluateLimitOrderEscrowPlaceGate` (BigInt compare on `toRawAmount` vs balance string; conservative when uncertain). - `frontend-dapp/src/components/trade/LimitOrderEscrowPlaceGuardMessage.tsx` — inline status / error under Place limit. - Wired in `TradeOrderTicket.tsx` (trade workspace) and `LimitOrdersPage.tsx` — disabled Place button + `mutationFn` guard (defense in depth). **Docs / agent playbooks** - `docs/limit-orders.md` — dApp retail section: #130 preflight invariants + links to code. - `skills/AGENTS_TERRACLASSIC_GAS.md` — canonical table + rule of thumb for escrow vs balance. - `skills/AGENTS_LOCALNET_TRADING_SWARM.md` — cross-link for localnet QA. **Verification checklist** (for QA / reviewers) - [ ] LocalTerra + dApp: wallet with LUNC only, **zero** balance of the escrow token for the chosen side → enter amount > 0 → **Place limit** stays disabled; inline **Insufficient balance** (or loading copy while balance fetches). - [ ] Same wallet after **minting** enough of the escrow token → gate clears; place limit still works end-to-end (allowance + place). - [ ] `/trade` order ticket and `/limits` page both behave the same for bid/ask side switches. - [ ] With indexer temporarily unavailable for unrelated queries, CW20 balance query still resolves → gate uses real balance (not a regression to blind submit). - [ ] `cd frontend-dapp && npm ci && npm run test:unit` passes (includes `limitOrderEscrowBalanceGate.test.ts`). @totdking — could you confirm on your LocalTerra + Keplr setup that you no longer get a successful `increase_allowance` when escrow balance is zero? Leaving the issue **open** until you verify.
totdking commented 2026-05-05 15:40:50 +00:00 (Migrated from gitlab.com)

Status:

issue is fixed, on zero escrow balance, there is no increase_allowance() as the button to carry out limit orders is disabled.

Verification checklist (for QA / reviewers)

  • LocalTerra + dApp: wallet with LUNC only, zero balance of the escrow token for the chosen side → enter amount > 0 → Place limit stays disabled; inline Insufficient balance (or loading copy while balance fetches).
  • Same wallet after minting enough of the escrow token → gate clears; place limit still works end-to-end (allowance + place).
  • /trade order ticket and /limits page both behave the same for bid/ask side switches.
  • With indexer temporarily unavailable for unrelated queries, CW20 balance query still resolves → gate uses real balance (not a regression to blind submit).
  • cd frontend-dapp && npm ci && npm run test:unit passes (includes limitOrderEscrowBalanceGate.test.ts).

Fixed @PlasticDigits

### Status: issue is fixed, on zero escrow balance, there is no increase_allowance() as the button to carry out limit orders is disabled. **Verification checklist** (for QA / reviewers) * [x] LocalTerra + dApp: wallet with LUNC only, **zero** balance of the escrow token for the chosen side → enter amount \> 0 → **Place limit** stays disabled; inline **Insufficient balance** (or loading copy while balance fetches). * [x] Same wallet after **minting** enough of the escrow token → gate clears; place limit still works end-to-end (allowance + place). * [x] `/trade` order ticket and `/limits` page both behave the same for bid/ask side switches. * [x] With indexer temporarily unavailable for unrelated queries, CW20 balance query still resolves → gate uses real balance (not a regression to blind submit). * [x] `cd frontend-dapp && npm ci && npm run test:unit` passes (includes `limitOrderEscrowBalanceGate.test.ts`). Fixed @PlasticDigits
totdking commented 2026-05-05 16:18:54 +00:00 (Migrated from gitlab.com)

mentioned in issue #132

mentioned in issue #132
Brouie commented 2026-05-05 23:55:35 +00:00 (Migrated from gitlab.com)

mentioned in issue #133

mentioned in issue #133
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-05-06 08:22:27 +00:00
PlasticDigits commented 2026-05-06 08:49:50 +00:00 (Migrated from gitlab.com)

mentioned in commit b4ebc07ba5

mentioned in commit b4ebc07ba5f648827415a3cc236b05fc812f574e
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#130
No description provided.