Limit book match: auto-flush sub-10-unit dust remainders after fill #264

Closed
opened 2026-06-01 03:42:13 +00:00 by PlasticDigits · 16 comments
PlasticDigits commented 2026-06-01 03:42:13 +00:00 (Migrated from gitlab.com)

Summary

During hybrid book matching, integer rounding can leave resting limit orders with 1–9 smallest-unit remainders instead of fully unlinking them. Each dust row keeps an ORDERS map entry and DLL links alive, so high fill volume slowly bloats chain state. Auto-flush orders whose post-fill remaining is > 0 and < 10 in the same match invocation: release escrow, remove the row from the active book, and hand the dust refund to the maker via the existing EXPIRED_LIMIT_CLAIMS claim path (no extra CW20 transfer in the swap tx).

Origin: discovered during live QA of GitLab #255 (batched pending-escrow subtract) — five bid fills each left remaining = 1 token1 from floor(fill × price) rounding while escrow accounting stayed exact (L1, L13).

Relationship to #263: #263 adds permissionless CleanLimitBook with governance-configurable per-side thresholds and a separate keeper tx. This issue is proactive flush at match time with a protocol constant threshold (10 raw units) so takers do not depend on an external sweeper to reclaim ORDERS storage after every near-complete fill.

Current codebase

  • Book match unlink rule: match_bids / match_asks (smartcontracts/contracts/pair/src/orderbook.rs) unlink an order only when order.remaining.is_zero() after subtracting fill cost; otherwise they ORDERS.save the partial row.
  • Bid escrow semantics: bid remaining is token1 (escrow currency). Fill size is token0; cost = fill.checked_mul_floor(price) can leave 0 < remaining < cost when the bid is economically exhausted (QA: floor(94_380_952 × 1.05) left 1 token1).
  • Ask escrow semantics: ask remaining is token0; fills subtract fill_t0 directly — dust is rarer but still possible on partial fills near budget caps.
  • Escrow release on fill: batched escrow_sub_pending_token{0,1} subtracts consumed escrow per side (L13, #255); dust left on-book keeps matching escrow in PENDING_ESCROW_*.
  • Existing dust cleanup (#263): CleanLimitBook + governance min_remaining_token{0,1} parks sub-threshold live orders into EXPIRED_LIMIT_CLAIMS via park_limit_order_for_clean — requires a separate permissionless tx and non-zero governance config; default thresholds are 0 / 0 (time-expired only).
  • Maker refund paths: cancel / claim decrement pending escrow and CW20-refund remaining; park during match/expiry creates EXPIRED_LIMIT_CLAIMS row with no CW20 in the park tx (L1).
  • Simulation: simulate_match_bids / simulate_match_asks mirror fill math but do not mutate storage — they must stay aligned with execute semantics for L8 hybrid quotes.

Why this is needed

  • State bloat: Dust rows (remaining ∈ [1, 9]) are unfillable at FIFO prices (next taker walk skips zero-size fills) but remain in ORDERS + DLL indefinitely unless a maker cancels or a keeper runs CleanLimitBook.
  • Operational cost: Every dust row adds taker scan steps and indexer limit-book noise; at scale this is pure chain/storage overhead with no trading utility.
  • Post-#255 visibility: Batched escrow release proved accounting is exact through dust remainders — the bug is lifecycle / storage, not escrow math. We should finish the fill by zeroing book presence when economically complete within 10 units.
  • Complement #263: Governance dust policy targets $0.01-equivalent notionals per pair; this constant catches rounding tails immediately without governance setup or keeper latency.

Constraints and guardrails

  • Threshold: Hard-coded LIMIT_ORDER_DUST_FLUSH_THRESHOLD = 10 (smallest units of the side's escrow token: token1 for bids, token0 for asks) in dex-common. Not governance-configurable in v1 — keep diff minimal; #263 remains the knob for larger notionals.
  • Trigger: After a successful fill in match_bids / match_asks, if 0 < order.remaining < 10, flush; if remaining ≥ 10, current partial-save behavior unchanged.
  • Flush action (recommended):
    1. Add dust remaining to the batched escrow subtract for that side (release from PENDING_ESCROW_*).
    2. park_limit_order_for_clean with force_expired = true (reuse #263 helper) — unlink ORDERS row; store dust in EXPIRED_LIMIT_CLAIMS for maker ClaimExpiredLimitOrder.
    3. Emit limit_order_expired_parked with force_expired=true (and/or extend fill event attrs — document choice).
  • Do not silently burn maker dust into taker output or treasury without an explicit governance decision.
  • Do not add per-dust CW20 transfers inside execute_swap (preserve L10 transfer aggregation); claim path only.
  • Pause / L6: Parked dust follows existing claim pause gate — no change.
  • Caps: Dust flush during match counts toward existing MAX_EXPIRED_PARKS_PER_SWAP if reusing park budget, or define a separate counter — document and test interaction with time-expiry parks (recommend: dust flush does not consume the 15 time-expiry park cap; it is a fill consequence, not a scan-only park).
  • Simulation: Update simulate_match_* to treat sub-threshold remainders as gone from the book for subsequent walk steps (in-memory only); hybrid simulation quotes must reflect execute flush.
  • Migration: none (behavior change on new wasm only).
  • Indexer / dApp: force_expired=true parks already indexed as parked_expired (#263); confirm parser needs no change.

Relevant files

Area Path
Match execute smartcontracts/contracts/pair/src/orderbook.rs (match_bids, match_asks, park_limit_order_for_clean)
Match simulate smartcontracts/contracts/pair/src/orderbook.rs (simulate_match_bids, simulate_match_asks)
Park / unlink smartcontracts/contracts/pair/src/orderbook.rs (unlink_order, park_limit_order_for_clean)
Constant smartcontracts/packages/dex-common/src/pair.rs (or new limit_dust.rs)
State / claims smartcontracts/contracts/pair/src/state.rs (ORDERS, EXPIRED_LIMIT_CLAIMS, PENDING_ESCROW_*)
Swap orchestration smartcontracts/contracts/pair/src/contract.rs (execute_swap)
Existing clean (#263) smartcontracts/contracts/pair/src/limit_book_clean.rs
Tests smartcontracts/contracts/pair/src/orderbook.rs (aggregation_tests, proptest_limits), smartcontracts/tests/src/limit_order_tests.rs
Docs / invariants docs/limit-orders.md, docs/contracts-security-audit.md (new L16 row), docs/integrators.md
Indexer indexer/src/indexer/parser.rs (confirm force_expired attrs)
  1. Add pub const LIMIT_ORDER_DUST_FLUSH_THRESHOLD: Uint128 = Uint128::new(10) in dex-common.
  2. Extract a small helper, e.g. fn should_flush_dust(remaining: Uint128) -> bool, and fn flush_dust_after_fill(...) that:
    • extends the batched escrow subtract by remaining;
    • calls park_limit_order_for_clean(..., force_expired: true, refund_expires_at: None);
    • returns the park event for inclusion in match fill events.
  3. In match_bids / match_asks, replace the if remaining.is_zero() { unlink } else { save } branch with:
    • zero → unlink (unchanged);
    • < 10 → flush helper (no ORDERS.save);
    • else → save partial (unchanged).
  4. Mirror flush semantics in simulate_match_* (zero out in-memory remaining and do not treat row as resting for subsequent steps).
  5. Unit test reproducing #255 QA: multi-fill bid ladder where each fill would leave remaining = 1 → after match, ORDERS keys gone, EXPIRED_LIMIT_CLAIMS hold dust, PENDING_ESCROW_TOKEN1 reduced by sum(costs) + sum(dust), maker can claim.
  6. Document distinction vs #263 governance clean and vs cancel.

Acceptance criteria

  • Post-fill 0 < remaining < 10 on bids or asks never leaves a row in ORDERS / DLL after match_bids / match_asks.
  • Dust escrow is released from PENDING_ESCROW_* in the same batched subtract as fill costs (L13 + L1 hold).
  • Makers can recover dust via ClaimExpiredLimitOrder; claim amount equals pre-flush dust remaining.
  • remaining ≥ 10 partial fills behave exactly as today.
  • simulate_match_* / HybridSimulation quotes match execute for scenarios with sub-10 remainders (L8).
  • Wasm events allow indexers to mark rows parked_expired with force_expired=true.
  • New invariant L16 documented in docs/contracts-security-audit.md.

Test plan (functional paths)

Path Expectation
Bid fill leaving remaining = 1 Order flushed; claim row = 1 token1; escrow −= cost + 1
Ask partial leaving remaining = 5 Same on token0 side
Fill leaving remaining = 0 Unlink only (no claim row) — unchanged
Fill leaving remaining = 10 Partial save — unchanged
Multi-maker swap (#255 repro) N near-complete fills → N claim rows or aggregated behavior documented; zero dust ORDERS rows
Maker claim after flush CW20 refund = dust; escrow decrement; claim row removed
Batch claim expired Dust rows claimable via ClaimExpiredLimitOrders
HybridSimulation vs execute Same book_return / fill count on dust-flush scenario
Time-expired park + dust flush same tx Both succeed; park caps documented

Test plan (attack / abuse / hack vectors)

Vector Verification
Maker griefing taker gas via forced parks Dust flush only after actual fill on that order; no permissionless flush in match
Threshold boundary (9 vs 10) Property tests: remaining = 9 flushes, 10 persists
Escrow underflow on flush checked_sub on pending escrow; flush + fill cannot exceed pre-fill escrow
Double park same order_id Second flush impossible — row already unlinked
Inflated remaining in forged state Cannot inject — only match path mutates after validated fill
Simulation vs execute mismatch (sandwich / slippage) L8 regression: sim and execute both flush or both keep
Claim steal Owner-only claim unchanged
Pause bypass Flush creates claim row; claim still blocked when paused (L6)

Verification criteria

  • cargo test -p cl8y-dex-pair orderbook:: and cargo test -p cl8y-dex-tests limit_order green.
  • Reproduce #255 live scenario on LocalTerra: after multi-fill hybrid swap, OrderBookHead walk shows no orders with remaining < 10; parked claims queryable.
  • L1: pair CW20 balance = reserves + pending escrow before/after flush tx.
  • L13: batched escrow subtract equals sum(fill costs) + sum(flushed dust) per side.
  • Indexer ingestion: limit_order_expired_parked + force_expired=true → parked_expired lifecycle (existing #263 tests as template).
## Summary During hybrid book matching, integer rounding can leave resting limit orders with **1–9 smallest-unit remainders** instead of fully unlinking them. Each dust row keeps an `ORDERS` map entry and DLL links alive, so high fill volume slowly bloats chain state. **Auto-flush** orders whose post-fill `remaining` is **> 0 and < 10** in the same match invocation: release escrow, remove the row from the active book, and hand the dust refund to the maker via the existing **`EXPIRED_LIMIT_CLAIMS`** claim path (no extra CW20 transfer in the swap tx). **Origin:** discovered during live QA of GitLab **#255** (batched pending-escrow subtract) — five bid fills each left **`remaining = 1`** token1 from `floor(fill × price)` rounding while escrow accounting stayed exact (**L1**, **L13**). **Relationship to #263:** [#263](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/263) adds **permissionless `CleanLimitBook`** with **governance-configurable** per-side thresholds and a separate keeper tx. This issue is **proactive flush at match time** with a **protocol constant** threshold (**10** raw units) so takers do not depend on an external sweeper to reclaim `ORDERS` storage after every near-complete fill. ## Current codebase - **Book match unlink rule:** `match_bids` / `match_asks` (`smartcontracts/contracts/pair/src/orderbook.rs`) unlink an order only when `order.remaining.is_zero()` after subtracting fill cost; otherwise they **`ORDERS.save`** the partial row. - **Bid escrow semantics:** bid `remaining` is **token1** (escrow currency). Fill size is token0; `cost = fill.checked_mul_floor(price)` can leave **`0 < remaining < cost`** when the bid is economically exhausted (QA: `floor(94_380_952 × 1.05)` left **1** token1). - **Ask escrow semantics:** ask `remaining` is **token0**; fills subtract `fill_t0` directly — dust is rarer but still possible on partial fills near budget caps. - **Escrow release on fill:** batched `escrow_sub_pending_token{0,1}` subtracts consumed escrow per side (**L13**, #255); dust left on-book keeps matching escrow in `PENDING_ESCROW_*`. - **Existing dust cleanup (#263):** `CleanLimitBook` + governance `min_remaining_token{0,1}` parks sub-threshold **live** orders into `EXPIRED_LIMIT_CLAIMS` via `park_limit_order_for_clean` — requires a **separate** permissionless tx and non-zero governance config; default thresholds are **0 / 0** (time-expired only). - **Maker refund paths:** cancel / claim decrement pending escrow and CW20-refund `remaining`; park during match/expiry creates `EXPIRED_LIMIT_CLAIMS` row with **no** CW20 in the park tx (**L1**). - **Simulation:** `simulate_match_bids` / `simulate_match_asks` mirror fill math but do not mutate storage — they must stay aligned with execute semantics for **L8** hybrid quotes. ## Why this is needed - **State bloat:** Dust rows (`remaining ∈ [1, 9]`) are **unfillable** at FIFO prices (next taker walk skips zero-size fills) but remain in `ORDERS` + DLL indefinitely unless a maker cancels or a keeper runs `CleanLimitBook`. - **Operational cost:** Every dust row adds taker scan steps and indexer `limit-book` noise; at scale this is pure chain/storage overhead with no trading utility. - **Post-#255 visibility:** Batched escrow release proved accounting is exact through dust remainders — the bug is **lifecycle / storage**, not escrow math. We should finish the fill by zeroing book presence when economically complete within **10** units. - **Complement #263:** Governance dust policy targets **$0.01-equivalent** notionals per pair; this constant catches **rounding tails** immediately without governance setup or keeper latency. ## Constraints and guardrails - **Threshold:** Hard-coded **`LIMIT_ORDER_DUST_FLUSH_THRESHOLD = 10`** (smallest units of the side's escrow token: token1 for bids, token0 for asks) in `dex-common`. **Not** governance-configurable in v1 — keep diff minimal; #263 remains the knob for larger notionals. - **Trigger:** After a successful fill in `match_bids` / `match_asks`, if **`0 < order.remaining < 10`**, flush; if **`remaining ≥ 10`**, current partial-save behavior unchanged. - **Flush action (recommended):** 1. Add dust `remaining` to the batched escrow subtract for that side (release from `PENDING_ESCROW_*`). 2. **`park_limit_order_for_clean`** with `force_expired = true` (reuse #263 helper) — **unlink** `ORDERS` row; store dust in `EXPIRED_LIMIT_CLAIMS` for maker **`ClaimExpiredLimitOrder`**. 3. Emit **`limit_order_expired_parked`** with `force_expired=true` (and/or extend fill event attrs — document choice). - **Do not** silently burn maker dust into taker output or treasury without an explicit governance decision. - **Do not** add per-dust CW20 transfers inside `execute_swap` (preserve **L10** transfer aggregation); claim path only. - **Pause / L6:** Parked dust follows existing claim pause gate — no change. - **Caps:** Dust flush during match counts toward existing **`MAX_EXPIRED_PARKS_PER_SWAP`** if reusing park budget, **or** define a separate counter — document and test interaction with time-expiry parks (recommend: dust flush **does not** consume the 15 time-expiry park cap; it is a fill consequence, not a scan-only park). - **Simulation:** Update `simulate_match_*` to treat sub-threshold remainders as **gone from the book** for subsequent walk steps (in-memory only); hybrid simulation quotes must reflect execute flush. - **Migration:** none (behavior change on new wasm only). - **Indexer / dApp:** `force_expired=true` parks already indexed as `parked_expired` (#263); confirm parser needs no change. ## Relevant files | Area | Path | |------|------| | Match execute | `smartcontracts/contracts/pair/src/orderbook.rs` (`match_bids`, `match_asks`, `park_limit_order_for_clean`) | | Match simulate | `smartcontracts/contracts/pair/src/orderbook.rs` (`simulate_match_bids`, `simulate_match_asks`) | | Park / unlink | `smartcontracts/contracts/pair/src/orderbook.rs` (`unlink_order`, `park_limit_order_for_clean`) | | Constant | `smartcontracts/packages/dex-common/src/pair.rs` (or new `limit_dust.rs`) | | State / claims | `smartcontracts/contracts/pair/src/state.rs` (`ORDERS`, `EXPIRED_LIMIT_CLAIMS`, `PENDING_ESCROW_*`) | | Swap orchestration | `smartcontracts/contracts/pair/src/contract.rs` (`execute_swap`) | | Existing clean (#263) | `smartcontracts/contracts/pair/src/limit_book_clean.rs` | | Tests | `smartcontracts/contracts/pair/src/orderbook.rs` (`aggregation_tests`, `proptest_limits`), `smartcontracts/tests/src/limit_order_tests.rs` | | Docs / invariants | `docs/limit-orders.md`, `docs/contracts-security-audit.md` (new **L16** row), `docs/integrators.md` | | Indexer | `indexer/src/indexer/parser.rs` (confirm `force_expired` attrs) | ## Recommended direction 1. Add `pub const LIMIT_ORDER_DUST_FLUSH_THRESHOLD: Uint128 = Uint128::new(10)` in `dex-common`. 2. Extract a small helper, e.g. `fn should_flush_dust(remaining: Uint128) -> bool`, and `fn flush_dust_after_fill(...)` that: - extends the batched escrow subtract by `remaining`; - calls `park_limit_order_for_clean(..., force_expired: true, refund_expires_at: None)`; - returns the park event for inclusion in match fill events. 3. In **`match_bids`** / **`match_asks`**, replace the `if remaining.is_zero() { unlink } else { save }` branch with: - `zero` → unlink (unchanged); - `< 10` → flush helper (no `ORDERS.save`); - else → save partial (unchanged). 4. Mirror flush **semantics** in **`simulate_match_*`** (zero out in-memory `remaining` and do not treat row as resting for subsequent steps). 5. Unit test reproducing #255 QA: multi-fill bid ladder where each fill would leave `remaining = 1` → after match, **`ORDERS` keys gone**, **`EXPIRED_LIMIT_CLAIMS`** hold dust, **`PENDING_ESCROW_TOKEN1`** reduced by sum(costs) + sum(dust), maker can claim. 6. Document distinction vs #263 governance clean and vs cancel. ## Acceptance criteria - [ ] Post-fill **`0 < remaining < 10`** on bids or asks **never** leaves a row in `ORDERS` / DLL after `match_bids` / `match_asks`. - [ ] Dust escrow is released from `PENDING_ESCROW_*` in the same batched subtract as fill costs (**L13** + **L1** hold). - [ ] Makers can recover dust via **`ClaimExpiredLimitOrder`**; claim amount equals pre-flush dust `remaining`. - [ ] **`remaining ≥ 10`** partial fills behave exactly as today. - [ ] **`simulate_match_*` / `HybridSimulation`** quotes match execute for scenarios with sub-10 remainders (**L8**). - [ ] Wasm events allow indexers to mark rows `parked_expired` with `force_expired=true`. - [ ] New invariant **L16** documented in `docs/contracts-security-audit.md`. ## Test plan (functional paths) | Path | Expectation | |------|-------------| | Bid fill leaving `remaining = 1` | Order flushed; claim row = 1 token1; escrow −= cost + 1 | | Ask partial leaving `remaining = 5` | Same on token0 side | | Fill leaving `remaining = 0` | Unlink only (no claim row) — unchanged | | Fill leaving `remaining = 10` | Partial save — unchanged | | Multi-maker swap (#255 repro) | N near-complete fills → N claim rows or aggregated behavior documented; **zero** dust `ORDERS` rows | | Maker claim after flush | CW20 refund = dust; escrow decrement; claim row removed | | Batch claim expired | Dust rows claimable via `ClaimExpiredLimitOrders` | | `HybridSimulation` vs execute | Same book_return / fill count on dust-flush scenario | | Time-expired park + dust flush same tx | Both succeed; park caps documented | ## Test plan (attack / abuse / hack vectors) | Vector | Verification | |--------|----------------| | Maker griefing taker gas via forced parks | Dust flush only after **actual fill** on that order; no permissionless flush in match | | Threshold boundary (`9` vs `10`) | Property tests: `remaining = 9` flushes, `10` persists | | Escrow underflow on flush | `checked_sub` on pending escrow; flush + fill cannot exceed pre-fill escrow | | Double park same `order_id` | Second flush impossible — row already unlinked | | Inflated `remaining` in forged state | Cannot inject — only match path mutates after validated fill | | Simulation vs execute mismatch (sandwich / slippage) | **L8** regression: sim and execute both flush or both keep | | Claim steal | Owner-only claim unchanged | | Pause bypass | Flush creates claim row; claim still blocked when paused (**L6**) | ## Verification criteria - `cargo test -p cl8y-dex-pair orderbook::` and `cargo test -p cl8y-dex-tests limit_order` green. - Reproduce #255 live scenario on LocalTerra: after multi-fill hybrid swap, **`OrderBookHead` walk** shows **no** orders with `remaining < 10`; parked claims queryable. - **L1:** pair CW20 balance = reserves + pending escrow before/after flush tx. - **L13:** batched escrow subtract equals sum(fill costs) + sum(flushed dust) per side. - Indexer ingestion: `limit_order_expired_parked` + `force_expired=true` → `parked_expired` lifecycle (existing #263 tests as template).
PlasticDigits commented 2026-06-01 03:42:29 +00:00 (Migrated from gitlab.com)

marked as related to #255

marked as related to #255
PlasticDigits commented 2026-06-01 03:42:31 +00:00 (Migrated from gitlab.com)

mentioned in issue #255

mentioned in issue #255
PlasticDigits commented 2026-06-01 03:48:45 +00:00 (Migrated from gitlab.com)

mentioned in commit a743fa173b

mentioned in commit a743fa173b7162251ebd38890fd9d36526cae108
PlasticDigits commented 2026-06-01 03:48:52 +00:00 (Migrated from gitlab.com)

Implementation summary (#264)

Implemented match-time dust flush for hybrid limit book fills.

What changed

  • Added protocol constant LIMIT_ORDER_DUST_FLUSH_THRESHOLD = 10 in dex-common::pair.
  • After a successful fill in match_bids / match_asks, when 0 < remaining < 10 (token1 for bids, token0 for asks):
    • Order is parked via park_limit_order_for_clean(..., force_expired=true) — removed from ORDERS/DLL, row stored in EXPIRED_LIMIT_CLAIMS.
    • limit_order_expired_parked wasm event with force_expired=true (indexer → existing parked_expired lifecycle).
    • Does not consume MAX_EXPIRED_PARKS_PER_SWAP (15) — only time-expired head parks count toward that cap.
  • remaining ≥ 10 partial fills unchanged; remaining = 0 unlink-only unchanged.
  • simulate_match_* zeroes in-memory sub-threshold remainders so HybridSimulation stays aligned with execute (L8).
  • L1 escrow: fill batched subtract releases cost only; dust remaining stays in PENDING_ESCROW_* until maker ClaimExpiredLimitOrder (same economics as time-expiry / CleanLimitBook parks).

Docs / invariants

  • New invariant L16 in docs/contracts-security-audit.md
  • docs/limit-orders.md § Match-time dust flush
  • docs/integrators.md § match-time dust flush
  • Cross-linked: skills/AGENTS_FRONTEND_LIMIT_PARKED_EXPIRED.md, skills/AGENTS_LOCALNET_TRADING_SWARM.md

Tests run (green)

  • cargo test -p cl8y-dex-pair orderbook::
  • cargo test -p cl8y-dex-tests limit_order (63 tests)

Merged to main @ a743fa1.


Verification checklist

  • Bid dust repro: Place bid at price 1.05 sized so a fill of ~94.38M token0 leaves remaining = 1 token1; hybrid swap → LimitOrder query fails, ExpiredLimitRefund shows 1, event force_expired=true.
  • Ask dust: Partial ask fill leaving remaining in 1…9 → same park path on token0 side.
  • Boundary: remaining = 9 flushes; remaining = 10 stays on book.
  • Multi-maker (#255-style): N near-complete fills → zero dust rows in limit-book walk; N claim rows (or batched claim).
  • Maker claim: ClaimExpiredLimitOrder / batch → CW20 refund = dust; pending escrow decrements; claim row removed.
  • L8: HybridSimulation vs execute on dust-flush scenario — same return_amount / fill count.
  • L1 balance: Pair CW20 balance = reserves + pending escrow before/after swap + claim.
  • Indexer: limit_order_expired_parked + force_expired=true → parked_expired in limit-placements feed (no parser change expected).
  • Pause (L6): Flush during swap OK; claim still blocked while paused.

QA agent team

Please run the checklist above on LocalTerra (or staging) against wasm built from main @ a743fa1, with emphasis on the #255 multi-fill bid ladder scenario and HybridSimulation parity.

Issue left open pending QA sign-off.

## Implementation summary (#264) Implemented **match-time dust flush** for hybrid limit book fills. ### What changed - Added protocol constant **`LIMIT_ORDER_DUST_FLUSH_THRESHOLD = 10`** in `dex-common::pair`. - After a successful fill in `match_bids` / `match_asks`, when **`0 < remaining < 10`** (token1 for bids, token0 for asks): - Order is **parked** via `park_limit_order_for_clean(..., force_expired=true)` — removed from `ORDERS`/DLL, row stored in **`EXPIRED_LIMIT_CLAIMS`**. - **`limit_order_expired_parked`** wasm event with **`force_expired=true`** (indexer → existing `parked_expired` lifecycle). - **Does not** consume **`MAX_EXPIRED_PARKS_PER_SWAP`** (15) — only time-expired head parks count toward that cap. - **`remaining ≥ 10`** partial fills unchanged; **`remaining = 0`** unlink-only unchanged. - **`simulate_match_*`** zeroes in-memory sub-threshold remainders so **HybridSimulation** stays aligned with execute (**L8**). - **L1 escrow:** fill batched subtract releases **cost** only; dust **`remaining`** stays in **`PENDING_ESCROW_*`** until maker **`ClaimExpiredLimitOrder`** (same economics as time-expiry / `CleanLimitBook` parks). ### Docs / invariants - New invariant **L16** in `docs/contracts-security-audit.md` - `docs/limit-orders.md` § [Match-time dust flush](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/limit-orders.md#match-time-dust-flush-gitlab-264) - `docs/integrators.md` § match-time dust flush - Cross-linked: `skills/AGENTS_FRONTEND_LIMIT_PARKED_EXPIRED.md`, `skills/AGENTS_LOCALNET_TRADING_SWARM.md` ### Tests run (green) - `cargo test -p cl8y-dex-pair orderbook::` - `cargo test -p cl8y-dex-tests limit_order` (63 tests) Merged to **`main`** @ `a743fa1`. --- ## Verification checklist - [ ] **Bid dust repro:** Place bid at price **1.05** sized so a fill of ~94.38M token0 leaves **`remaining = 1`** token1; hybrid swap → **`LimitOrder` query fails**, **`ExpiredLimitRefund`** shows **1**, event **`force_expired=true`**. - [ ] **Ask dust:** Partial ask fill leaving **`remaining` in 1…9** → same park path on token0 side. - [ ] **Boundary:** **`remaining = 9`** flushes; **`remaining = 10`** stays on book. - [ ] **Multi-maker (#255-style):** N near-complete fills → **zero** dust rows in `limit-book` walk; N claim rows (or batched claim). - [ ] **Maker claim:** `ClaimExpiredLimitOrder` / batch → CW20 refund = dust; pending escrow decrements; claim row removed. - [ ] **L8:** `HybridSimulation` vs execute on dust-flush scenario — same `return_amount` / fill count. - [ ] **L1 balance:** Pair CW20 balance = reserves + pending escrow before/after swap + claim. - [ ] **Indexer:** `limit_order_expired_parked` + `force_expired=true` → `parked_expired` in limit-placements feed (no parser change expected). - [ ] **Pause (L6):** Flush during swap OK; claim still blocked while paused. --- ## QA agent team Please run the checklist above on LocalTerra (or staging) against wasm built from **`main`** @ `a743fa1`, with emphasis on the **#255 multi-fill bid ladder** scenario and **HybridSimulation parity**. Issue left **open** pending QA sign-off.
Brouie commented 2026-06-01 14:12:48 +00:00 (Migrated from gitlab.com)

mentioned in merge request !733

mentioned in merge request !733
Brouie commented 2026-06-01 15:28:38 +00:00 (Migrated from gitlab.com)

@PlasticDigits heads up — make test-contracts is red on main (d6701c4): the pair-lib
proptest prop_match_bids_maker_cap / prop_match_asks_maker_cap fail, so the whole
integration suite never runs.

Root cause is in #264's wave, and it's test-only — NOT an escrow leak. The helper
assert_escrow_matches_lists (orderbook.rs) still asserts on-book remaining == PENDING_ESCROW,
but #264 now parks sub-10 dust off-book into EXPIRED_LIMIT_CLAIMS while escrow keeps backing it
until claim (L1). So escrow = on-book + parked dust. The helper under-counted and tripped on any
random budget that left <10 dust (deltas were all 1/3/6/9).

Conservation itself is correct — the dedicated #264 tests pass and prove it
(match_bid_dust_remainder_one_flushes_to_expired_claim: escrow drops by cost only, dust stays
pending == claimable). I fixed the helper to add the parked-dust total per side:
walk_bid_sum + parked_dust_token1 == PENDING_ESCROW_TOKEN1 (and ask/token0).

Branch qa/fix-264-proptest-escrow-parked-dust, commit de07725 (orderbook.rs only, +19/-2).
After the fix: make test-contracts = 402 passed, 0 failed. Needs your verification + merge —
this is gating verification of the whole contract wave.

@PlasticDigits heads up — `make test-contracts` is red on main (d6701c4): the pair-lib proptest `prop_match_bids_maker_cap` / `prop_match_asks_maker_cap` fail, so the whole integration suite never runs. Root cause is in #264's wave, and it's test-only — NOT an escrow leak. The helper `assert_escrow_matches_lists` (orderbook.rs) still asserts on-book remaining == PENDING_ESCROW, but #264 now parks sub-10 dust off-book into EXPIRED_LIMIT_CLAIMS while escrow keeps backing it until claim (L1). So escrow = on-book + parked dust. The helper under-counted and tripped on any random budget that left <10 dust (deltas were all 1/3/6/9). Conservation itself is correct — the dedicated #264 tests pass and prove it (match_bid_dust_remainder_one_flushes_to_expired_claim: escrow drops by cost only, dust stays pending == claimable). I fixed the helper to add the parked-dust total per side: walk_bid_sum + parked_dust_token1 == PENDING_ESCROW_TOKEN1 (and ask/token0). Branch qa/fix-264-proptest-escrow-parked-dust, commit de07725 (orderbook.rs only, +19/-2). After the fix: make test-contracts = 402 passed, 0 failed. Needs your verification + merge — this is gating verification of the whole contract wave.
Brouie commented 2026-06-01 15:33:58 +00:00 (Migrated from gitlab.com)

mentioned in issue #262

mentioned in issue #262
Brouie commented 2026-06-01 15:38:43 +00:00 (Migrated from gitlab.com)

Verified #264 on d6701c4 (with the !733 proptest fix applied for a green suite).

Tests (all green in make test-contracts = 402/0):

  • orderbook::aggregation_tests::match_bid_dust_remainder_one_flushes_to_expired_claim,
    match_ask_dust_remainder_five_flushes_to_expired_claim, should_flush_dust_boundary_nine_yes_ten_no
    (9 flushes / 10 stays), match_bids_multi_maker_dust_flush_sim_matches_execute (L8 sim==execute).
  • limit_order_tests::match_dust_flush_bid_hybrid_then_maker_claims (full lifecycle: force_expired park
    -> ORDERS gone -> claim row=1 -> claim refunds exactly 1; L1).
  • limit_order_tests::claim_expired_limit_order_blocked_while_pair_paused_then_succeeds_after_unpause (L6).
  • L16 documented in contracts-security-audit.md with cited tests.

Live repro on LocalTerra (fresh genesis), the explicit checklist scenario:

  • bid escrow 1_000_000 token1 @ 1.05; taker fills 952_380 token0 -> cost floor(952380*1.05)=999_999,
    remaining = 1 -> match-time dust flush.
  • limit_order_expired_parked with force_expired=true (1 park); LimitOrder{order_id} now not found.
  • ExpiredLimitRefund{order_id} = {side:bid, remaining:"1", expires_at:null} -> claimable, off-book.
  • Maker ClaimExpiredLimitOrder -> token1 balance delta = +1 (exact dust), claim row removed. L1 holds.
  • Dust flush did NOT consume MAX_EXPIRED_PARKS_PER_SWAP.

Indexer: apply_parked_expired keys on the limit_order_expired_parked action (force_expired is an attr) ->
active→parked_expired→refunded; no parser change needed (will confirm live ingestion under #267/#269).

Good to close from my side once !733 merges. @PlasticDigits

Verified #264 on d6701c4 (with the !733 proptest fix applied for a green suite). Tests (all green in make test-contracts = 402/0): - orderbook::aggregation_tests::match_bid_dust_remainder_one_flushes_to_expired_claim, match_ask_dust_remainder_five_flushes_to_expired_claim, should_flush_dust_boundary_nine_yes_ten_no (9 flushes / 10 stays), match_bids_multi_maker_dust_flush_sim_matches_execute (L8 sim==execute). - limit_order_tests::match_dust_flush_bid_hybrid_then_maker_claims (full lifecycle: force_expired park -> ORDERS gone -> claim row=1 -> claim refunds exactly 1; L1). - limit_order_tests::claim_expired_limit_order_blocked_while_pair_paused_then_succeeds_after_unpause (L6). - L16 documented in contracts-security-audit.md with cited tests. Live repro on LocalTerra (fresh genesis), the explicit checklist scenario: - bid escrow 1_000_000 token1 @ 1.05; taker fills 952_380 token0 -> cost floor(952380*1.05)=999_999, remaining = 1 -> match-time dust flush. - limit_order_expired_parked with force_expired=true (1 park); LimitOrder{order_id} now not found. - ExpiredLimitRefund{order_id} = {side:bid, remaining:"1", expires_at:null} -> claimable, off-book. - Maker ClaimExpiredLimitOrder -> token1 balance delta = +1 (exact dust), claim row removed. L1 holds. - Dust flush did NOT consume MAX_EXPIRED_PARKS_PER_SWAP. Indexer: apply_parked_expired keys on the limit_order_expired_parked action (force_expired is an attr) -> active→parked_expired→refunded; no parser change needed (will confirm live ingestion under #267/#269). Good to close from my side once !733 merges. @PlasticDigits
Brouie commented 2026-06-01 15:52:53 +00:00 (Migrated from gitlab.com)

mentioned in issue #263

mentioned in issue #263
PlasticDigits commented 2026-06-02 06:51:23 +00:00 (Migrated from gitlab.com)

mentioned in commit 52a865bfb7

mentioned in commit 52a865bfb73a0eddba244cc6aade010408640a01
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-06-02 06:52:24 +00:00
PlasticDigits commented 2026-06-02 07:00:00 +00:00 (Migrated from gitlab.com)

mentioned in issue #271

mentioned in issue #271
PlasticDigits commented 2026-06-02 07:00:01 +00:00 (Migrated from gitlab.com)

marked as related to #271

marked as related to #271
Brouie commented 2026-06-02 12:45:13 +00:00 (Migrated from gitlab.com)

mentioned in merge request !734

mentioned in merge request !734
Brouie commented 2026-06-03 07:20:07 +00:00 (Migrated from gitlab.com)

mentioned in issue #289

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

mentioned in issue #309

mentioned in issue #309
leonardocolucci commented 2026-07-31 17:34:20 +00:00 (Migrated from gitlab.com)

mentioned in issue #504

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