Limit order gas: batch placement storage collapse + price-only Edit via UpdateLimitOrderPrice #247

Closed
opened 2026-05-31 12:21:29 +00:00 by PlasticDigits · 11 comments
PlasticDigits commented 2026-05-31 12:21:29 +00:00 (Migrated from gitlab.com)

Summary

Two related limit-order gas optimizations:

  1. Contract (no API change): Collapse redundant ORDER_NEXT_ID and PENDING_ESCROW_* read/writes inside execute_place_limit_orders_batch.
  2. Frontend + pair.ts: Wire Edit (price-only) to existing on-chain UpdateLimitOrderPrice instead of cancel + re-place.

Part A — Batch placement storage collapse

Current codebase

  • execute_place_limit_orders_batch (smartcontracts/contracts/pair/src/limit_placement.rs) loops rungs calling insert_bid / insert_ask.
  • Each insert (smartcontracts/contracts/pair/src/orderbook.rs):
    • next_order_id: load + save ORDER_NEXT_ID every rung.
    • load + save PENDING_ESCROW_TOKEN0|1 every rung.
    • Linear book walk + neighbor link updates (unchanged by this issue).
  • For N=10 rungs: ~20 redundant Item ops on ORDER_NEXT_ID + escrow counters alone.

Why needed

Batch placement is already the default path (even single orders use batch with orders.len() == 1). Removing per-rung global Item churn reduces gas ~linearly in N with zero message/API change.

Constraints / guardrails

  • No API change — external callers unchanged.
  • Atomicity: Same all-or-nothing / partial-skip semantics as today for LimitInsertStepsExceeded.
  • ID monotonicity: Reserved block must match sequential next_order_id behavior (no gaps, no reuse).
  • Escrow invariant L1: Final PENDING_ESCROW_* must equal sum of inserted remainings; prop tests must still pass.
  • In execute_place_limit_orders_batch: load escrow once; reserve N ids with one ORDER_NEXT_ID R/W; pass (pre_id, skip_escrow_write: true) into thin insert_bid_with_id / insert_ask_with_id helpers.
  • Save escrow once after loop (including skipped rungs — only placed rungs add to escrow).

Part A acceptance criteria

  • N-rung batch: single ORDER_NEXT_ID write, single escrow Item write per token side touched.
  • Order ids identical to pre-refactor sequential placement (regression test).
  • prop_escrow_dll_after_random_inserts and batch integration tests pass.

Part B — Edit → UpdateLimitOrderPrice

Current codebase

  • On-chain ExecuteMsg::UpdateLimitOrderPrice exists (pair.rs ~194–199): owner-only relink at new price, no maker fee, no token movement (execute_update_limit_order_price, contract.rs ~1062–1098; orderbook::relink_limit_order_price).
  • Frontend does not call it: no updateLimitOrderPrice in frontend-dapp/src/services/terraclassic/pair.ts; grep shows zero frontend usage.
  • Edit UX prefills the limit ticket for a new placement (cancel + replace mental model) — see docs/limit-orders.md § Trade order book row actions (#162, #178).
  • Re-price today costs: cancel tx (~450k gas + CW20 transfer) + place tx (~950k+ gas + maker fee + transfers).

Why needed

Market makers editing price on resting size waste gas and pay maker fee twice. UpdateLimitOrderPrice was designed for this path but is unused in the dApp.

Constraints / guardrails

  • Price-only: If user changes amount/side/expiry, fall back to cancel + batch place (or block with clear copy).
  • Owner-only: Same as on-chain check.
  • Pause (L6): Blocked while paused (same gate as cancel).
  • Expiry: Reject if order already expired (execute_update_limit_order_price already checks).
  • max_adjust_steps / hint: Expose advanced placement gas settings; pass hint_after_order_id from indexer book position when available.
  • No escrow movement: Relink must not change remaining or PENDING_ESCROW_*.
  1. Add updateLimitOrderPrice(wallet, pair, orderId, price, hint?, maxAdjustSteps?) to pair.ts.
  2. In Edit flow (TradeOrderTicket, OrderBookPanel): if only price changed → call update; else prefill new order as today.
  3. Gas: new constant UPDATE_LIMIT_ORDER_PRICE_GAS_LIMIT (measure on localterra; expect ≪ cancel+place).
  4. Invalidate indexer queries on success (limitBookPage, limitPlacements).

Relevant files

Part Files
A limit_placement.rs, orderbook.rs, state.rs, limit_order_tests.rs
B pair.ts, TradeOrderTicket.tsx, OrderBookPanel.tsx, terraGas.ts, docs/limit-orders.md
Shared dex-common/src/pair.rs, contract.rs

Combined test plan — functional

Part A

  • Batch 10 rungs: escrow + order id sequence matches baseline.
  • Ladder expansion path unchanged.
  • Skipped rungs (steps exceeded): escrow only increments for placed rungs.

Part B

  • Edit price only → one tx, same order_id, book position updated, no CW20 transfer msgs.
  • Edit with amount change → does not call update (cancel+place or user messaging).
  • Expired order edit → clear error.
  • Paused pair → blocked.

Test plan — attack / abuse

Part A

  • ID overflow at u64::MAX → safe revert (existing invariant).
  • Escrow underflow if refactor mis-accounts skipped rungs → revert.

Part B

  • Non-owner cannot update price (on-chain + UI disabled).
  • Zero/negative price → revert.
  • Relink with max_adjust_steps too low → LimitInsertStepsExceeded; order must not be corrupted (detach/relink atomicity — verify order still restable on book or full revert).

Verification criteria

  • Contract unit + integration tests green; optional gas snapshot before/after Part A (document % savings for N=10).
  • Playwright or unit test for Edit price-only path.
  • docs/limit-orders.md documents Edit → UpdateLimitOrderPrice behavior and when cancel+replace is still required.
## Summary Two related limit-order gas optimizations: 1. **Contract (no API change):** Collapse redundant `ORDER_NEXT_ID` and `PENDING_ESCROW_*` read/writes inside `execute_place_limit_orders_batch`. 2. **Frontend + pair.ts:** Wire **Edit** (price-only) to existing on-chain **`UpdateLimitOrderPrice`** instead of cancel + re-place. ## Part A — Batch placement storage collapse ### Current codebase - `execute_place_limit_orders_batch` (`smartcontracts/contracts/pair/src/limit_placement.rs`) loops rungs calling `insert_bid` / `insert_ask`. - Each insert (`smartcontracts/contracts/pair/src/orderbook.rs`): - `next_order_id`: **load + save** `ORDER_NEXT_ID` every rung. - **load + save** `PENDING_ESCROW_TOKEN0|1` every rung. - Linear book walk + neighbor link updates (unchanged by this issue). - For N=10 rungs: ~20 redundant Item ops on `ORDER_NEXT_ID` + escrow counters alone. ### Why needed Batch placement is already the default path (even single orders use batch with `orders.len() == 1`). Removing per-rung global Item churn reduces gas ~linearly in N with **zero message/API change**. ### Constraints / guardrails - **No API change** — external callers unchanged. - **Atomicity:** Same all-or-nothing / partial-skip semantics as today for `LimitInsertStepsExceeded`. - **ID monotonicity:** Reserved block must match sequential `next_order_id` behavior (no gaps, no reuse). - **Escrow invariant L1:** Final `PENDING_ESCROW_*` must equal sum of inserted remainings; prop tests must still pass. ### Recommended direction - In `execute_place_limit_orders_batch`: load escrow once; reserve N ids with one `ORDER_NEXT_ID` R/W; pass `(pre_id, skip_escrow_write: true)` into thin `insert_bid_with_id` / `insert_ask_with_id` helpers. - Save escrow once after loop (including skipped rungs — only placed rungs add to escrow). ### Part A acceptance criteria - [ ] N-rung batch: single `ORDER_NEXT_ID` write, single escrow Item write per token side touched. - [ ] Order ids identical to pre-refactor sequential placement (regression test). - [ ] `prop_escrow_dll_after_random_inserts` and batch integration tests pass. --- ## Part B — Edit → UpdateLimitOrderPrice ### Current codebase - On-chain **`ExecuteMsg::UpdateLimitOrderPrice`** exists (`pair.rs` ~194–199): owner-only relink at new price, **no maker fee**, no token movement (`execute_update_limit_order_price`, `contract.rs` ~1062–1098; `orderbook::relink_limit_order_price`). - **Frontend does not call it:** no `updateLimitOrderPrice` in `frontend-dapp/src/services/terraclassic/pair.ts`; grep shows zero frontend usage. - **Edit UX** prefills the limit ticket for a **new** placement (cancel + replace mental model) — see `docs/limit-orders.md` § Trade order book row actions (#162, #178). - Re-price today costs: **cancel tx** (~450k gas + CW20 transfer) + **place tx** (~950k+ gas + maker fee + transfers). ### Why needed Market makers editing price on resting size waste gas and pay maker fee twice. `UpdateLimitOrderPrice` was designed for this path but is unused in the dApp. ### Constraints / guardrails - **Price-only:** If user changes amount/side/expiry, fall back to cancel + batch place (or block with clear copy). - **Owner-only:** Same as on-chain check. - **Pause (L6):** Blocked while paused (same gate as cancel). - **Expiry:** Reject if order already expired (`execute_update_limit_order_price` already checks). - **`max_adjust_steps` / hint:** Expose advanced placement gas settings; pass `hint_after_order_id` from indexer book position when available. - **No escrow movement:** Relink must not change `remaining` or `PENDING_ESCROW_*`. ### Recommended direction 1. Add `updateLimitOrderPrice(wallet, pair, orderId, price, hint?, maxAdjustSteps?)` to `pair.ts`. 2. In Edit flow (`TradeOrderTicket`, `OrderBookPanel`): if only price changed → call update; else prefill new order as today. 3. Gas: new constant `UPDATE_LIMIT_ORDER_PRICE_GAS_LIMIT` (measure on localterra; expect ≪ cancel+place). 4. Invalidate indexer queries on success (`limitBookPage`, `limitPlacements`). ## Relevant files | Part | Files | |------|-------| | A | `limit_placement.rs`, `orderbook.rs`, `state.rs`, `limit_order_tests.rs` | | B | `pair.ts`, `TradeOrderTicket.tsx`, `OrderBookPanel.tsx`, `terraGas.ts`, `docs/limit-orders.md` | | Shared | `dex-common/src/pair.rs`, `contract.rs` | ## Combined test plan — functional **Part A** - [ ] Batch 10 rungs: escrow + order id sequence matches baseline. - [ ] Ladder expansion path unchanged. - [ ] Skipped rungs (steps exceeded): escrow only increments for placed rungs. **Part B** - [ ] Edit price only → one tx, same `order_id`, book position updated, no CW20 transfer msgs. - [ ] Edit with amount change → does not call update (cancel+place or user messaging). - [ ] Expired order edit → clear error. - [ ] Paused pair → blocked. ## Test plan — attack / abuse **Part A** - [ ] ID overflow at `u64::MAX` → safe revert (existing invariant). - [ ] Escrow underflow if refactor mis-accounts skipped rungs → revert. **Part B** - [ ] Non-owner cannot update price (on-chain + UI disabled). - [ ] Zero/negative price → revert. - [ ] Relink with `max_adjust_steps` too low → `LimitInsertStepsExceeded`; order must not be corrupted (detach/relink atomicity — verify order still restable on book or full revert). ## Verification criteria - [ ] Contract unit + integration tests green; optional gas snapshot before/after Part A (document % savings for N=10). - [ ] Playwright or unit test for Edit price-only path. - [ ] `docs/limit-orders.md` documents Edit → `UpdateLimitOrderPrice` behavior and when cancel+replace is still required.
PlasticDigits commented 2026-05-31 13:08:14 +00:00 (Migrated from gitlab.com)

mentioned in commit 0babbb6967

mentioned in commit 0babbb6967ad47e758a0e01b9b3e54d6911d0110
PlasticDigits commented 2026-05-31 13:08:24 +00:00 (Migrated from gitlab.com)

Implementation summary (GitLab #247)

Merged to main at 0babbb6.

Part A — Batch placement storage collapse (contract)

  • execute_place_limit_orders_batch now reserves all rung ids with one ORDER_NEXT_ID write via reserve_order_id_block.
  • Escrow accumulates in memory during the loop; one PENDING_ESCROW_TOKEN0/1 write per token side touched after successful placements.
  • New helpers: insert_bid_with_id / insert_ask_with_id(..., update_escrow: false) in orderbook.rs.
  • Regression test: batch_placement_order_ids_match_sequential_singles.
  • Invariant L12 documented in docs/contracts-security-audit.md; skill §9 in skills/AGENTS_LIMIT_ORDER_BATCH_LADDER.md.

Part B — Edit → UpdateLimitOrderPrice (frontend)

  • updateLimitOrderPrice in pair.ts; useLimitOrderUpdatePriceMutation hook.
  • Book Edit prefill includes orderId, expiresAt, advisory hintAfterOrderId.
  • Ticket detects price-only edits → Update price button (one tx, no CW20, no maker fee).
  • Non-price changes blocked with cancel-first copy (limitOrderPriceEdit.ts).
  • Gas: UPDATE_LIMIT_ORDER_PRICE_GAS_LIMIT = 350k in terraGas.ts.

Docs / agent playbooks updated

  • docs/limit-orders.md, docs/frontend.md, docs/contracts-security-audit.md
  • skills/AGENTS_FRONTEND_ORDER_BOOK_ROW_ACTIONS.md, AGENTS_TERRACLASSIC_GAS.md, AGENTS_LIMIT_ORDER_BATCH_LADDER.md

Verification checklist

Part A (contract)

  • Batch 10 rungs: order id sequence matches sequential singles (batch_placement_order_ids_match_sequential_singles)
  • Skipped rungs (LimitInsertStepsExceeded): escrow increments only for placed rungs (limit_batch_partial_success_skips_book_walk_failures)
  • prop_escrow_dll_after_random_inserts still passes
  • Optional: LocalTerra gas snapshot before/after for N=10 batch placement

Part B (frontend / dApp)

  • Book Edit → change price only → one tx, same order_id, no CW20 transfers
  • Edit → change amount/side/expiry → blocked with cancel-first message (no accidental replace)
  • Paused pair: Edit / Update price disabled (L6)
  • Expired order price update reverts on-chain with clear error
  • Indexer book + placements refresh after successful update

Tests run locally

  • cargo test -p cl8y-dex-tests batch_placement_order_ids_match_sequential_singles
  • cargo test -p cl8y-dex-tests limit_batch
  • cargo test -p cl8y-dex-pair prop_escrow
  • npm test -- --run limitOrderPriceEdit pair OrderBookPanel

@qa agent team — please verify the checklist above on LocalTerra (Keplr/dev wallet per #235 matrix): book Edit price-only path, batch ladder gas behavior unchanged functionally, and that non-price edits still require cancel+place.

Issue left open pending QA sign-off.

## Implementation summary (GitLab #247) Merged to `main` at `0babbb6`. ### Part A — Batch placement storage collapse (contract) - `execute_place_limit_orders_batch` now reserves all rung ids with one `ORDER_NEXT_ID` write via `reserve_order_id_block`. - Escrow accumulates in memory during the loop; one `PENDING_ESCROW_TOKEN0/1` write per token side touched after successful placements. - New helpers: `insert_bid_with_id` / `insert_ask_with_id(..., update_escrow: false)` in `orderbook.rs`. - Regression test: `batch_placement_order_ids_match_sequential_singles`. - Invariant **L12** documented in `docs/contracts-security-audit.md`; skill §9 in `skills/AGENTS_LIMIT_ORDER_BATCH_LADDER.md`. ### Part B — Edit → `UpdateLimitOrderPrice` (frontend) - `updateLimitOrderPrice` in `pair.ts`; `useLimitOrderUpdatePriceMutation` hook. - Book **Edit** prefill includes `orderId`, `expiresAt`, advisory `hintAfterOrderId`. - Ticket detects price-only edits → **Update price** button (one tx, no CW20, no maker fee). - Non-price changes blocked with cancel-first copy (`limitOrderPriceEdit.ts`). - Gas: `UPDATE_LIMIT_ORDER_PRICE_GAS_LIMIT` = 350k in `terraGas.ts`. ### Docs / agent playbooks updated - `docs/limit-orders.md`, `docs/frontend.md`, `docs/contracts-security-audit.md` - `skills/AGENTS_FRONTEND_ORDER_BOOK_ROW_ACTIONS.md`, `AGENTS_TERRACLASSIC_GAS.md`, `AGENTS_LIMIT_ORDER_BATCH_LADDER.md` --- ## Verification checklist **Part A (contract)** - [ ] Batch 10 rungs: order id sequence matches sequential singles (`batch_placement_order_ids_match_sequential_singles`) - [ ] Skipped rungs (`LimitInsertStepsExceeded`): escrow increments only for placed rungs (`limit_batch_partial_success_skips_book_walk_failures`) - [ ] `prop_escrow_dll_after_random_inserts` still passes - [ ] Optional: LocalTerra gas snapshot before/after for N=10 batch placement **Part B (frontend / dApp)** - [ ] Book **Edit** → change price only → one tx, same `order_id`, no CW20 transfers - [ ] Edit → change amount/side/expiry → blocked with cancel-first message (no accidental replace) - [ ] Paused pair: Edit / Update price disabled (L6) - [ ] Expired order price update reverts on-chain with clear error - [ ] Indexer book + placements refresh after successful update **Tests run locally** - `cargo test -p cl8y-dex-tests batch_placement_order_ids_match_sequential_singles` - `cargo test -p cl8y-dex-tests limit_batch` - `cargo test -p cl8y-dex-pair prop_escrow` - `npm test -- --run limitOrderPriceEdit pair OrderBookPanel` --- **@qa agent team** — please verify the checklist above on LocalTerra (Keplr/dev wallet per #235 matrix): book Edit price-only path, batch ladder gas behavior unchanged functionally, and that non-price edits still require cancel+place. Issue left **open** pending QA sign-off.
PlasticDigits commented 2026-05-31 14:03:08 +00:00 (Migrated from gitlab.com)

mentioned in issue #261

mentioned in issue #261
PlasticDigits commented 2026-06-01 04:18:55 +00:00 (Migrated from gitlab.com)

mentioned in issue #266

mentioned in issue #266
Brouie commented 2026-06-02 17:06:45 +00:00 (Migrated from gitlab.com)

#247 verified — good to close. Both parts.

Part A — batch placement storage collapse (contract):

  • Single ORDER_NEXT_ID write per batch — reserve_order_id_block(storage, rung_count) reserves the whole id block at limit_placement.rs:154; rungs insert via insert_bid_with_id_for_batch / insert_ask_with_id_for_batch with update_escrow:false, then one PENDING_ESCROW_* write per token side after the loop.
  • Order ids identical to sequential singles — batch_placement_order_ids_match_sequential_singles passes (ID monotonicity, no gaps/reuse).
  • Skipped rungs (LimitInsertStepsExceeded): escrow increments only for placed rungs — limit_batch_partial_success_skips_book_walk_failures passes.
  • Escrow invariant L1 holds after the refactor — prop_escrow_dll_after_random_inserts passes.
  • L12 documented in contracts-security-audit.md.

Part B — Edit -> UpdateLimitOrderPrice (frontend):

  • updateLimitOrderPrice in pair.ts + useLimitOrderUpdatePriceMutation; UPDATE_LIMIT_ORDER_PRICE_GAS_LIMIT = 350k (vs cancel+place ~1.4M + maker fee).
  • Price-only detection: isPriceOnlyLimitEdit returns false when side or amount changes, so non-price edits are routed to cancel-first copy, not an accidental relink.
  • vitest limitOrderPriceEdit + pair + OrderBookPanel — 8 files, 55 passed.
  • docs/limit-orders.md documents Edit -> UpdateLimitOrderPrice and when cancel+replace is still required.

Browser layer (book Edit price-only -> one tx, same order_id, no CW20 transfer; paused/expired disabled; indexer refresh) is the laptop path; the detection + msg encode + gas constant + on-chain relink are unit/contract-covered here.

Verified end to end. @PlasticDigits

#247 verified — good to close. Both parts. Part A — batch placement storage collapse (contract): - [x] Single ORDER_NEXT_ID write per batch — reserve_order_id_block(storage, rung_count) reserves the whole id block at limit_placement.rs:154; rungs insert via insert_bid_with_id_for_batch / insert_ask_with_id_for_batch with update_escrow:false, then one PENDING_ESCROW_* write per token side after the loop. - [x] Order ids identical to sequential singles — batch_placement_order_ids_match_sequential_singles passes (ID monotonicity, no gaps/reuse). - [x] Skipped rungs (LimitInsertStepsExceeded): escrow increments only for placed rungs — limit_batch_partial_success_skips_book_walk_failures passes. - [x] Escrow invariant L1 holds after the refactor — prop_escrow_dll_after_random_inserts passes. - [x] L12 documented in contracts-security-audit.md. Part B — Edit -> UpdateLimitOrderPrice (frontend): - [x] updateLimitOrderPrice in pair.ts + useLimitOrderUpdatePriceMutation; UPDATE_LIMIT_ORDER_PRICE_GAS_LIMIT = 350k (vs cancel+place ~1.4M + maker fee). - [x] Price-only detection: isPriceOnlyLimitEdit returns false when side or amount changes, so non-price edits are routed to cancel-first copy, not an accidental relink. - [x] vitest limitOrderPriceEdit + pair + OrderBookPanel — 8 files, 55 passed. - [x] docs/limit-orders.md documents Edit -> UpdateLimitOrderPrice and when cancel+replace is still required. Browser layer (book Edit price-only -> one tx, same order_id, no CW20 transfer; paused/expired disabled; indexer refresh) is the laptop path; the detection + msg encode + gas constant + on-chain relink are unit/contract-covered here. Verified end to end. @PlasticDigits
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-06-03 02:05:05 +00:00
Brouie commented 2026-06-04 07:08:51 +00:00 (Migrated from gitlab.com)

mentioned in issue #294

mentioned in issue #294
PlasticDigits commented 2026-06-05 04:08:29 +00:00 (Migrated from gitlab.com)

mentioned in issue #312

mentioned in issue #312
PlasticDigits commented 2026-06-08 08:14:05 +00:00 (Migrated from gitlab.com)

mentioned in issue #338

mentioned in issue #338
Brouie commented 2026-06-09 02:17:29 +00:00 (Migrated from gitlab.com)

mentioned in issue #337

mentioned in issue #337
PlasticDigits commented 2026-08-17 10:26:07 +00:00 (Migrated from gitlab.com)

mentioned in issue #546

mentioned in issue #546
PlasticDigits commented 2026-08-17 10:26:07 +00:00 (Migrated from gitlab.com)

marked as related to #546

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