Limit-book matcher trusts caller-supplied book_start_hint without side validation (cross-escrow drain) #272

Closed
opened 2026-06-03 07:08:20 +00:00 by Brouie · 19 comments
Brouie commented 2026-06-03 07:08:20 +00:00 (Migrated from gitlab.com)

Severity: Critical
Reachability: Permissionless. Any external wallet, through a single CW20 Swap carrying crafted hybrid params. No router, no privileged role.
Affected: pair limit-book matching (match_bids / match_asks) reached via the public hybrid swap path.
Root cause: the caller-supplied book_start_hint is validated for existence only — not that it, or the orders walked from it, belong to the side being matched.

Summary

The hybrid swap path lets the caller pass a book_start_hint so the matcher can start walking the book near the right place instead of from the head. Problem: the matcher only checks the hinted order id loads. It never checks the order is on the side it's matching, and it never re-checks side as it walks the next chain.

The per-side escrow pools (one backing bids, one backing asks) get debited based on which matcher ran, not on the actual side of the orders consumed. So a wrong-side hint makes one side's match settle against the other side's escrow pool. That breaks per-side escrow accounting and lets a caller pull value out of a pool that isn't backing what they matched — i.e. drain escrow and leave the pair short of what it owes its makers.

This is a fund-loss / insolvency bug. It's the top launch blocker.

Current codebase

  • book_start_hint is public on the swap message — it rides inside the hybrid params of Cw20HookMsg::Swap and is not gated to the router. Any sender sets it.
  • execute_swap forwards the hint verbatim into the matcher. The matcher branch (bid vs ask) is chosen by which token was sent in, not by the hint.
  • In the matcher, the start is accepted when the hinted order id merely loads, then the walk follows each order's next pointer and settles against the side escrow without asserting the walked order's side matches the leg.

Reproduction

We're pre-deploy, so here's the whole thing.

Exact lines:

  • orderbook.rs:1348-1356 — match_bids accepts book_start_hint on ORDERS.may_load(h).is_some() — existence only, no side check. Same shape in match_asks.
  • orderbook.rs:1367-1368 — the walk loads the order at cur and follows order.next; it never checks order.side equals the side being matched.
  • contract.rs:881 — the matcher branch is chosen by which token the taker sends (offer == token_a → match_bids, else match_asks), not by the hint.
  • orderbook.rs:1476 — match_bids debits the BID escrow pool (escrow_sub_pending_token1); match_asks debits the ASK pool (token0). The debit follows the matcher, not the actual side of the order consumed.

Consequence: match_bids will pay the taker token1 out of the bid escrow pool for whatever order it walks — including an ASK order, whose collateral lives in the token0 pool and never funded the bid pool.

Drain (self-dealing):

  1. Live pair with honest bid-side liquidity (bid pool funded with token1).
  2. Attacker places an ASK order A (escrows token0 in the ask pool); note its id.
  3. Attacker sends a CW20 Swap of token_a (token0) with hybrid params: book_input > 0, max_maker_fills >= 1, book_start_hint = A.
  4. token_a sent → match_bids runs. Accepts hint A (exists), walks A as if a bid, fills at A's price, pays the taker token1 from the BID pool, debits PENDING_ESCROW_TOKEN1, marks A consumed.
  5. The taker pulled token1 out of the bid pool by "matching" their own ask, which never funded it. A's token0 collateral is left stranded / double-counted.
  6. Repeat until the bid pool is empty → honest bid makers can't be paid → pair insolvent; attacker leaves with the token1.

Symmetric: send token_b (token1) to run match_asks against a wrong-side BID hint and drain the token0 pool instead.

Invariant broken: PENDING_ESCROW_TOKEN0 should cover open asks, PENDING_ESCROW_TOKEN1 open bids. After a wrong-side match the debited pool no longer reconciles to its open orders.

Why this matters

Per-side escrow solvency is the core invariant of the book — each side's pool must always cover the open orders on that side. If a caller can settle a match against the wrong pool, the pair's escrow no longer covers its outstanding orders: makers can't be paid, and a motivated caller can walk away with the mismatch. Constant-product reserves and book escrow share the same contract, so this is real money, not just a counter being wrong.

  1. Validate the hint: resolve book_start_hint, require its side equals the side being matched; if it doesn't (or doesn't exist), ignore it and start from the correct head. Honest callers never notice.
  2. Defense in depth: assert each walked order's side matches the active matcher before consuming/settling it; skip or stop on mismatch.
  3. Make the escrow debit follow the actual side of the order consumed, so a mismatch can never silently cross pools even if a future hint check regresses.

Acceptance criteria

  • A Swap whose hybrid hint points at an order on the opposite side cannot consume that order or touch the opposite escrow pool.
  • Bid matching only ever debits the bid escrow pool; ask matching only the ask pool — enforced in code, not by convention.
  • An invalid / stale / wrong-side hint falls back to the correct head with no error to honest callers.
  • Per-side escrow totals reconcile to the sum of open orders on that side after an arbitrary sequence of hybrid swaps (property test).

Test plan (functional)

case expect
valid same-side hint matches from hint, identical result to head-start
stale / nonexistent hint falls back to head, swap still succeeds
hint mid-book on the correct side starts there, never re-reads earlier orders

Test plan (attack / abuse)

case expect
hint points at an opposite-side order rejected / ignored, no opposite-pool debit
self-dealing wrong-side hint (the drain above) per-side escrow reconciles, no extraction
repeated hybrid swaps with adversarial hints (fuzz) per-side escrow reconciles every time

Verification

Per-side escrow invariant holds under fuzzing, the self-dealing drain reverts/no-ops, and the attack cases above are locked by named regression tests in the pair suite. Localnet check: fund both escrow pools via honest orders, run the wrong-side-hint swap, assert the opposite pool was NOT debited and sum(open bids) == bid-pool balance afterward.


@PlasticDigits this is the #1 pre-mainnet blocker. Full repro is in here now since nothing's deployed. Nothing goes live with money in a pair until the hint is side-validated.

**Severity:** Critical **Reachability:** Permissionless. Any external wallet, through a single CW20 `Swap` carrying crafted hybrid params. No router, no privileged role. **Affected:** pair limit-book matching (`match_bids` / `match_asks`) reached via the public hybrid swap path. **Root cause:** the caller-supplied `book_start_hint` is validated for *existence only* — not that it, or the orders walked from it, belong to the side being matched. ## Summary The hybrid swap path lets the caller pass a `book_start_hint` so the matcher can start walking the book near the right place instead of from the head. Problem: the matcher only checks the hinted order id *loads*. It never checks the order is on the side it's matching, and it never re-checks side as it walks the `next` chain. The per-side escrow pools (one backing bids, one backing asks) get debited based on *which matcher ran*, not on the actual side of the orders consumed. So a wrong-side hint makes one side's match settle against the *other* side's escrow pool. That breaks per-side escrow accounting and lets a caller pull value out of a pool that isn't backing what they matched — i.e. drain escrow and leave the pair short of what it owes its makers. This is a fund-loss / insolvency bug. It's the top launch blocker. ## Current codebase - `book_start_hint` is public on the swap message — it rides inside the hybrid params of `Cw20HookMsg::Swap` and is **not** gated to the router. Any sender sets it. - `execute_swap` forwards the hint verbatim into the matcher. The matcher branch (bid vs ask) is chosen by *which token was sent in*, not by the hint. - In the matcher, the start is accepted when the hinted order id merely loads, then the walk follows each order's `next` pointer and settles against the side escrow **without asserting the walked order's side matches the leg**. ## Reproduction We're pre-deploy, so here's the whole thing. Exact lines: - `orderbook.rs:1348-1356` — `match_bids` accepts `book_start_hint` on `ORDERS.may_load(h).is_some()` — existence only, no side check. Same shape in `match_asks`. - `orderbook.rs:1367-1368` — the walk loads the order at `cur` and follows `order.next`; it never checks `order.side` equals the side being matched. - `contract.rs:881` — the matcher branch is chosen by which token the taker **sends** (`offer == token_a` → `match_bids`, else `match_asks`), not by the hint. - `orderbook.rs:1476` — `match_bids` debits the BID escrow pool (`escrow_sub_pending_token1`); `match_asks` debits the ASK pool (token0). The debit follows the matcher, not the actual side of the order consumed. Consequence: `match_bids` will pay the taker token1 out of the bid escrow pool for whatever order it walks — including an ASK order, whose collateral lives in the token0 pool and never funded the bid pool. Drain (self-dealing): 1. Live pair with honest bid-side liquidity (bid pool funded with token1). 2. Attacker places an ASK order A (escrows token0 in the ask pool); note its id. 3. Attacker sends a CW20 `Swap` of token_a (token0) with hybrid params: `book_input > 0`, `max_maker_fills >= 1`, `book_start_hint = A`. 4. token_a sent → `match_bids` runs. Accepts hint A (exists), walks A as if a bid, fills at A's price, pays the taker token1 from the BID pool, debits `PENDING_ESCROW_TOKEN1`, marks A consumed. 5. The taker pulled token1 out of the bid pool by "matching" their own ask, which never funded it. A's token0 collateral is left stranded / double-counted. 6. Repeat until the bid pool is empty → honest bid makers can't be paid → pair insolvent; attacker leaves with the token1. Symmetric: send token_b (token1) to run `match_asks` against a wrong-side BID hint and drain the token0 pool instead. Invariant broken: `PENDING_ESCROW_TOKEN0` should cover open asks, `PENDING_ESCROW_TOKEN1` open bids. After a wrong-side match the debited pool no longer reconciles to its open orders. ## Why this matters Per-side escrow solvency is the core invariant of the book — each side's pool must always cover the open orders on that side. If a caller can settle a match against the wrong pool, the pair's escrow no longer covers its outstanding orders: makers can't be paid, and a motivated caller can walk away with the mismatch. Constant-product reserves and book escrow share the same contract, so this is real money, not just a counter being wrong. ## Recommended direction 1. Validate the hint: resolve `book_start_hint`, require its `side` equals the side being matched; if it doesn't (or doesn't exist), ignore it and start from the correct head. Honest callers never notice. 2. Defense in depth: assert each walked order's `side` matches the active matcher before consuming/settling it; skip or stop on mismatch. 3. Make the escrow debit follow the *actual* side of the order consumed, so a mismatch can never silently cross pools even if a future hint check regresses. ## Acceptance criteria - [ ] A `Swap` whose hybrid hint points at an order on the opposite side cannot consume that order or touch the opposite escrow pool. - [ ] Bid matching only ever debits the bid escrow pool; ask matching only the ask pool — enforced in code, not by convention. - [ ] An invalid / stale / wrong-side hint falls back to the correct head with no error to honest callers. - [ ] Per-side escrow totals reconcile to the sum of open orders on that side after an arbitrary sequence of hybrid swaps (property test). ## Test plan (functional) | case | expect | |---|---| | valid same-side hint | matches from hint, identical result to head-start | | stale / nonexistent hint | falls back to head, swap still succeeds | | hint mid-book on the correct side | starts there, never re-reads earlier orders | ## Test plan (attack / abuse) | case | expect | |---|---| | hint points at an opposite-side order | rejected / ignored, no opposite-pool debit | | self-dealing wrong-side hint (the drain above) | per-side escrow reconciles, no extraction | | repeated hybrid swaps with adversarial hints (fuzz) | per-side escrow reconciles every time | ## Verification Per-side escrow invariant holds under fuzzing, the self-dealing drain reverts/no-ops, and the attack cases above are locked by named regression tests in the pair suite. Localnet check: fund both escrow pools via honest orders, run the wrong-side-hint swap, assert the opposite pool was NOT debited and `sum(open bids) == bid-pool balance` afterward. --- @PlasticDigits this is the #1 pre-mainnet blocker. Full repro is in here now since nothing's deployed. Nothing goes live with money in a pair until the hint is side-validated.
Brouie commented 2026-06-03 07:24:34 +00:00 (Migrated from gitlab.com)

mentioned in issue #290

mentioned in issue #290
Brouie commented 2026-06-03 07:56:02 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
Brouie commented 2026-06-03 08:08:48 +00:00 (Migrated from gitlab.com)

Turned the repro into regression tests and confirmed it's live.

Added two tests on the match path: a bid-side match pointed at an attacker's own ask, and the symmetric ask-side case. On current code both fail — the bid matcher walks and consumes the wrong-side ask and debits the token1 (bid) escrow pool (ask side symmetric on token0). That's the cross-escrow drain, reproduced, not theoretical.

Fix is small — side-validate book_start_hint at the four resolution sites (match_bids, match_asks, simulate_match_bids, simulate_match_asks) and fall back to the head on a mismatch. Bid sites:

let mut cur = if let Some(h) = book_start_hint {
    match ORDERS.may_load(storage, h)? {
        Some(o) if o.side == LimitOrderSide::Bid => Some(h),
        _ => HEAD_BID.may_load(storage)?.flatten(),
    }
} else {
    HEAD_BID.may_load(storage)?.flatten()
};

Ask sites the same with LimitOrderSide::Ask / HEAD_ASK. With that in, both new tests pass and the full pair suite stays green (39/39). The existing insert-path wrong-side-hint test already passed — only the match path was open.

Turned the repro into regression tests and confirmed it's live. Added two tests on the match path: a bid-side match pointed at an attacker's own ask, and the symmetric ask-side case. On current code both fail — the bid matcher walks and consumes the wrong-side ask and debits the token1 (bid) escrow pool (ask side symmetric on token0). That's the cross-escrow drain, reproduced, not theoretical. Fix is small — side-validate `book_start_hint` at the four resolution sites (`match_bids`, `match_asks`, `simulate_match_bids`, `simulate_match_asks`) and fall back to the head on a mismatch. Bid sites: ``` let mut cur = if let Some(h) = book_start_hint { match ORDERS.may_load(storage, h)? { Some(o) if o.side == LimitOrderSide::Bid => Some(h), _ => HEAD_BID.may_load(storage)?.flatten(), } } else { HEAD_BID.may_load(storage)?.flatten() }; ``` Ask sites the same with `LimitOrderSide::Ask` / `HEAD_ASK`. With that in, both new tests pass and the full pair suite stays green (39/39). The existing insert-path wrong-side-hint test already passed — only the match path was open.
PlasticDigits commented 2026-06-03 10:29:15 +00:00 (Migrated from gitlab.com)

Please include an MR demonstrating the exploit, so we can add automated tests to verify the fix and no regressions.

Please include an MR demonstrating the exploit, so we can add automated tests to verify the fix and no regressions.
PlasticDigits commented 2026-06-03 10:33:21 +00:00 (Migrated from gitlab.com)

mentioned in commit dd70ea3283

mentioned in commit dd70ea3283eb2582646430d42fe91e82b74f0b80
PlasticDigits commented 2026-06-03 10:33:27 +00:00 (Migrated from gitlab.com)

Fix landed on main (dd70ea3)

Summary: Hybrid match walks now treat book_start_hint like insert hints: the hinted order must exist on the same side as the active matcher (match_bids / simulate_match_bids → bid; match_asks / simulate_match_asks → ask). Wrong-side, stale, or missing hints fall back to the correct book head with no error. During the walk, any order whose side does not match the matcher is skipped (no fill, no cross-pool PENDING_ESCROW_* debit).

Code: resolve_match_start_hint + order_on_match_side in smartcontracts/contracts/pair/src/orderbook.rs (execute + simulate paths).

Invariant: L17 in docs/contracts-security-audit.md (cross-linked from docs/limit-orders.md).

Agent playbook: skills/AGENTS_BOOK_MATCH_HINT_SECURITY.md (also linked from skills/AGENTS_HYBRID_QUOTING.md).

Verification checklist

  • cd smartcontracts && cargo test -p cl8y-dex-pair book_start_hint_side_tests
  • cd smartcontracts && cargo test -p cl8y-dex-pair prop_match_bids_adversarial_wrong_side_hint
  • cd smartcontracts && cargo test -p cl8y-dex-tests hybrid_wrong_side_book_start_hint
  • cd smartcontracts && cargo test -p cl8y-dex-tests hybrid_same_side_book_start_hint_still_matches
  • cd smartcontracts && cargo test -p cl8y-dex-tests match_invalid_book_start_hint_falls_back_to_head
  • Re-read L17 row in docs/contracts-security-audit.md matches implementation
  • (Pre-mainnet) Localnet: fund bid + ask escrow with honest orders; run wrong-side-hint hybrid swap from issue repro; confirm opposite pool not debited and ask/bid remainings match expected head walk

Tests added

Area Names
Unit orderbook::book_start_hint_side_tests::*
Proptest prop_match_bids_adversarial_wrong_side_hint_preserves_escrow
Integration hybrid_wrong_side_book_start_hint_no_cross_escrow_drain, hybrid_wrong_side_book_start_hint_match_asks_symmetric, hybrid_same_side_book_start_hint_still_matches

Follow-up

  • Contract wasm must be rebuilt and redeployed before on-chain verification; this repo fix is pre-deploy only.

Request: Please run the checklist above on main and confirm the self-dealing drain repro from this issue no longer extracts from the opposite escrow pool.

Issue remains open until QA sign-off.

## Fix landed on `main` (dd70ea3) **Summary:** Hybrid match walks now treat `book_start_hint` like insert hints: the hinted order must exist on the **same side** as the active matcher (`match_bids` / `simulate_match_bids` → bid; `match_asks` / `simulate_match_asks` → ask). Wrong-side, stale, or missing hints fall back to the correct book head with no error. During the walk, any order whose `side` does not match the matcher is skipped (no fill, no cross-pool `PENDING_ESCROW_*` debit). **Code:** `resolve_match_start_hint` + `order_on_match_side` in `smartcontracts/contracts/pair/src/orderbook.rs` (execute + simulate paths). **Invariant:** **L17** in `docs/contracts-security-audit.md` (cross-linked from `docs/limit-orders.md`). **Agent playbook:** `skills/AGENTS_BOOK_MATCH_HINT_SECURITY.md` (also linked from `skills/AGENTS_HYBRID_QUOTING.md`). ### Verification checklist - [ ] `cd smartcontracts && cargo test -p cl8y-dex-pair book_start_hint_side_tests` - [ ] `cd smartcontracts && cargo test -p cl8y-dex-pair prop_match_bids_adversarial_wrong_side_hint` - [ ] `cd smartcontracts && cargo test -p cl8y-dex-tests hybrid_wrong_side_book_start_hint` - [ ] `cd smartcontracts && cargo test -p cl8y-dex-tests hybrid_same_side_book_start_hint_still_matches` - [ ] `cd smartcontracts && cargo test -p cl8y-dex-tests match_invalid_book_start_hint_falls_back_to_head` - [ ] Re-read **L17** row in `docs/contracts-security-audit.md` matches implementation - [ ] (Pre-mainnet) Localnet: fund bid + ask escrow with honest orders; run wrong-side-hint hybrid swap from issue repro; confirm opposite pool not debited and ask/bid remainings match expected head walk ### Tests added | Area | Names | |------|--------| | Unit | `orderbook::book_start_hint_side_tests::*` | | Proptest | `prop_match_bids_adversarial_wrong_side_hint_preserves_escrow` | | Integration | `hybrid_wrong_side_book_start_hint_no_cross_escrow_drain`, `hybrid_wrong_side_book_start_hint_match_asks_symmetric`, `hybrid_same_side_book_start_hint_still_matches` | ### Follow-up - Contract wasm must be **rebuilt and redeployed** before on-chain verification; this repo fix is pre-deploy only. --- **Request:** Please run the checklist above on `main` and confirm the self-dealing drain repro from this issue no longer extracts from the opposite escrow pool. Issue remains **open** until QA sign-off.
Brouie commented 2026-06-04 02:35:50 +00:00 (Migrated from gitlab.com)

mentioned in issue #292

mentioned in issue #292
Brouie commented 2026-06-04 02:40:49 +00:00 (Migrated from gitlab.com)

Verified the dd70ea3 fix on main (d167c45) — source + tests + a live wrong-side-hint repro on the new SDK53 localnet. The cross-escrow drain is closed: a wrong-side hint is rejected, the walk falls back to the correct head, and the opposite escrow pool is never touched.

Source / tests (cw-multitest, chain-independent) — cargo test whole workspace 412/0, every named test green:

  • orderbook::book_start_hint_side_tests::{match_bids_wrong_side_hint_falls_back_to_bid_head, match_asks_wrong_side_hint_falls_back_to_ask_head, simulate_match_bids_wrong_side_hint_matches_execute_start}
  • orderbook::proptest_limits::prop_match_bids_adversarial_wrong_side_hint_preserves_escrow
  • limit_order_tests::{hybrid_wrong_side_book_start_hint_no_cross_escrow_drain, _match_asks_symmetric, hybrid_same_side_book_start_hint_still_matches, match_invalid_book_start_hint_falls_back_to_head}

resolve_match_start_hint returns the hint only when order.side == expected_side, else falls back to HEAD_BID/HEAD_ASK; order_on_match_side skip on all four walk sites (execute + simulate). L17 row matches the code. The integration tests assert the right thing (opposite order untouched + correct head consumed + taker gain bounded), so they flip on the pre-fix existence-only check.

Reviewed the fix from completeness / escrow-accounting / list-walk / test-vacuousness / bypass angles — nothing real. bids/asks are separate DLLs so the per-step skip is defense-in-depth (correct to keep); recommendation #3 (debit-follows-consumed-side) is moot since the skip guarantees the matcher only consumes same-side orders.

Live on terrad v4 localnet (deployed d167c45, pair terra146ypn…c9mjav): honest bid + attacker ask, attacker sends a token0 hybrid swap with book_start_hint = <own ask id> → match_bids with a wrong-side hint. Result: ask untouched (49550→49550, token0 pool not debited), matcher fell back to the bid head and filled it (99100→78100), attacker got one legit fill (+20811 token1) not a pool drain. swap tx 163DC0A7… code 0.

Two optional non-gating hardening ideas: give clean_limit_book's loop the same per-node side guard for parity; tighten simulate_match_bids_wrong_side_hint_matches_execute_start (passes but is a tautology). Neither blocks.

Good to close from my side. @PlasticDigits

Verified the dd70ea3 fix on `main` (d167c45) — source + tests + a live wrong-side-hint repro on the new SDK53 localnet. The cross-escrow drain is closed: a wrong-side hint is rejected, the walk falls back to the correct head, and the opposite escrow pool is never touched. **Source / tests** (cw-multitest, chain-independent) — `cargo test` whole workspace **412/0**, every named test green: - `orderbook::book_start_hint_side_tests::{match_bids_wrong_side_hint_falls_back_to_bid_head, match_asks_wrong_side_hint_falls_back_to_ask_head, simulate_match_bids_wrong_side_hint_matches_execute_start}` - `orderbook::proptest_limits::prop_match_bids_adversarial_wrong_side_hint_preserves_escrow` - `limit_order_tests::{hybrid_wrong_side_book_start_hint_no_cross_escrow_drain, _match_asks_symmetric, hybrid_same_side_book_start_hint_still_matches, match_invalid_book_start_hint_falls_back_to_head}` `resolve_match_start_hint` returns the hint only when `order.side == expected_side`, else falls back to `HEAD_BID`/`HEAD_ASK`; `order_on_match_side` skip on all four walk sites (execute + simulate). L17 row matches the code. The integration tests assert the right thing (opposite order untouched + correct head consumed + taker gain bounded), so they flip on the pre-fix existence-only check. Reviewed the fix from completeness / escrow-accounting / list-walk / test-vacuousness / bypass angles — nothing real. bids/asks are separate DLLs so the per-step skip is defense-in-depth (correct to keep); recommendation #3 (debit-follows-consumed-side) is moot since the skip guarantees the matcher only consumes same-side orders. **Live on terrad v4 localnet** (deployed d167c45, pair terra146ypn…c9mjav): honest bid + attacker ask, attacker sends a token0 hybrid swap with `book_start_hint = <own ask id>` → match_bids with a wrong-side hint. Result: ask **untouched** (49550→49550, token0 pool not debited), matcher fell back to the **bid head** and filled it (99100→78100), attacker got one legit fill (+20811 token1) not a pool drain. swap tx `163DC0A7…` code 0. Two optional non-gating hardening ideas: give `clean_limit_book`'s loop the same per-node side guard for parity; tighten `simulate_match_bids_wrong_side_hint_matches_execute_start` (passes but is a tautology). Neither blocks. Good to close from my side. @PlasticDigits
Brouie commented 2026-06-04 06:30:07 +00:00 (Migrated from gitlab.com)

mentioned in issue #289

mentioned in issue #289
PlasticDigits commented 2026-06-05 03:57:39 +00:00 (Migrated from gitlab.com)

QA sign-off — GitLab #272 (limit-book book_start_hint cross-escrow drain)

Verified on main @ 9f0babe (includes fix dd70ea3). No repository changes; closing after checklist pass.

Results

Item Result How verified
cargo test -p cl8y-dex-pair book_start_hint_side_tests PASS 3/3: match_bids_wrong_side_hint_falls_back_to_bid_head, match_asks_wrong_side_hint_falls_back_to_ask_head, simulate_match_bids_wrong_side_hint_matches_execute_start
cargo test -p cl8y-dex-pair prop_match_bids_adversarial_wrong_side_hint PASS prop_match_bids_adversarial_wrong_side_hint_preserves_escrow
cargo test -p cl8y-dex-tests hybrid_wrong_side_book_start_hint PASS 2/2: hybrid_wrong_side_book_start_hint_no_cross_escrow_drain, _match_asks_symmetric
cargo test -p cl8y-dex-tests hybrid_same_side_book_start_hint_still_matches PASS 1/1
cargo test -p cl8y-dex-tests match_invalid_book_start_hint_falls_back_to_head PASS 1/1
L17 vs implementation PASS resolve_match_start_hint + order_on_match_side in orderbook.rs match docs/contracts-security-audit.md L17 and docs/limit-orders.md; playbook skills/AGENTS_BOOK_MATCH_HINT_SECURITY.md cross-linked from skills/AGENTS_HYBRID_QUOTING.md
Localnet wrong-side-hint repro (pre-mainnet) PASS LocalTerra + optimized pair wasm: honest bid (order 1) + attacker ask (order 2); CW20 hybrid swap of token0 with book_start_hint=2 → ask unchanged (49550000→49550000), bid head consumed (99100000→79100000), legit token1 gain (+20000000). Swap tx A7BBE348BDCFC0CFA755F1AFB1409609673945376EDEF95199A5D9D1A12A02D9

Acceptance criteria (issue body)

  • Opposite-side hint cannot consume order or debit opposite pool — PASS (unit, integration, proptest, localnet)
  • Bid/ask matchers debit only their side escrow — PASS (L17 + regression tests)
  • Invalid/wrong-side hint falls back to head without error — PASS
  • Per-side escrow reconciles under adversarial hints — PASS (proptest + integration)

Aligns with @Brouie verification on d167c45; re-confirmed on current main.

Note

make deploy-local failed at first pair create (missing --amount for pair_creation_fee_uluna); minimal pair creation with 100000000uluna was used for the localnet repro only — unrelated to #272 fix behavior.

## QA sign-off — GitLab #272 (limit-book `book_start_hint` cross-escrow drain) Verified on `main` @ `9f0babe` (includes fix `dd70ea3`). No repository changes; closing after checklist pass. ### Results | Item | Result | How verified | |------|--------|----------------| | `cargo test -p cl8y-dex-pair book_start_hint_side_tests` | **PASS** | 3/3: `match_bids_wrong_side_hint_falls_back_to_bid_head`, `match_asks_wrong_side_hint_falls_back_to_ask_head`, `simulate_match_bids_wrong_side_hint_matches_execute_start` | | `cargo test -p cl8y-dex-pair prop_match_bids_adversarial_wrong_side_hint` | **PASS** | `prop_match_bids_adversarial_wrong_side_hint_preserves_escrow` | | `cargo test -p cl8y-dex-tests hybrid_wrong_side_book_start_hint` | **PASS** | 2/2: `hybrid_wrong_side_book_start_hint_no_cross_escrow_drain`, `_match_asks_symmetric` | | `cargo test -p cl8y-dex-tests hybrid_same_side_book_start_hint_still_matches` | **PASS** | 1/1 | | `cargo test -p cl8y-dex-tests match_invalid_book_start_hint_falls_back_to_head` | **PASS** | 1/1 | | **L17** vs implementation | **PASS** | `resolve_match_start_hint` + `order_on_match_side` in `orderbook.rs` match `docs/contracts-security-audit.md` L17 and `docs/limit-orders.md`; playbook `skills/AGENTS_BOOK_MATCH_HINT_SECURITY.md` cross-linked from `skills/AGENTS_HYBRID_QUOTING.md` | | Localnet wrong-side-hint repro (pre-mainnet) | **PASS** | LocalTerra + optimized pair wasm: honest bid (order 1) + attacker ask (order 2); CW20 hybrid swap of token0 with `book_start_hint=2` → ask **unchanged** (49550000→49550000), bid head **consumed** (99100000→79100000), legit token1 gain (+20000000). Swap tx `A7BBE348BDCFC0CFA755F1AFB1409609673945376EDEF95199A5D9D1A12A02D9` | ### Acceptance criteria (issue body) - Opposite-side hint cannot consume order or debit opposite pool — **PASS** (unit, integration, proptest, localnet) - Bid/ask matchers debit only their side escrow — **PASS** (L17 + regression tests) - Invalid/wrong-side hint falls back to head without error — **PASS** - Per-side escrow reconciles under adversarial hints — **PASS** (proptest + integration) Aligns with @Brouie verification on `d167c45`; re-confirmed on current `main`. ### Note `make deploy-local` failed at first pair create (missing `--amount` for `pair_creation_fee_uluna`); minimal pair creation with `100000000uluna` was used for the localnet repro only — unrelated to #272 fix behavior.
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-06-05 03:57:39 +00:00
PlasticDigits commented 2026-06-05 04:12:10 +00:00 (Migrated from gitlab.com)

mentioned in issue #318

mentioned in issue #318
ghost1 commented 2026-06-05 11:02:54 +00:00 (Migrated from gitlab.com)

mentioned in commit 4c4c26846b

mentioned in commit 4c4c26846bff977763c2d66c787473c6627bbfc5
PlasticDigits commented 2026-06-05 11:03:25 +00:00 (Migrated from gitlab.com)

mentioned in merge request !795

mentioned in merge request !795
PlasticDigits commented 2026-06-05 13:44:36 +00:00 (Migrated from gitlab.com)

mentioned in issue #332

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

mentioned in merge request !816

mentioned in merge request !816
PlasticDigits commented 2026-06-13 07:09:04 +00:00 (Migrated from gitlab.com)

mentioned in issue #376

mentioned in issue #376
PlasticDigits commented 2026-06-29 00:21:34 +00:00 (Migrated from gitlab.com)

mentioned in issue #424

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

mentioned in issue #707

mentioned in issue #707
PlasticDigits commented 2026-08-30 05:24:16 +00:00 (Migrated from gitlab.com)

mentioned in issue #708

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