Skip to content

Order book: evict the worst order when a side is full - #158

Merged
mikemaccana merged 2 commits into
mainfrom
claude/alex-book-feedback-4eciar
Sep 23, 2026
Merged

mikemaccana merged 2 commits into
mainfrom
claude/alex-book-feedback-4eciar

Conversation

@mikemaccana

@mikemaccana mikemaccana commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

A full side of the order book refused every new resting order. Anyone willing to lock the minimum order size and pay rent for each slot could fill a side with orders far from the spread and keep every new order off that side for as long as they liked. The market authority had no way to clear them, and the market PDA's seeds are the two mints, so the pair could not move to a new market.

What changes

  • Eviction in place_order(). When the order will rest on a full side and beats that side's worst price, the worst order is evicted. Its owner is refunded through their unsettled balance, exactly as cancel_order() refunds, and the order is removed from the book and from its owner's open orders, then stamped Cancelled. An order that does not beat the worst price still gets OrderBookFull. Equal is not better, because the resting order was there first.
  • Accounts. The caller passes the worst order and its owner's MarketUser after the maker pairs. When the worst order is the caller's own, only the order is passed, and the caller's market_user is credited. Anchor v2 refuses a writable account that appears twice. In Anchor v1, a second loaded copy would be overwritten when the instruction writes its accounts back. The check that remaining accounts come in whole pairs is gone, and the per-fill length check still covers every maker pair.
  • Shared refund. credit_unfilled_lock() now holds the refund arithmetic that cancel_order() had inline, and eviction calls it too.
  • New errors. MissingEvictedAccounts and EvictedAccountMismatch.
  • Capacity fix. A side holds 512 orders, not 1,024. Every order after the first adds a leaf and an inner node to the side's 1,024-node tree. MAX_ORDERS_PER_SIDE is now MAX_TREE_NODES / 2, and the comments and READMEs say so.

All three variants change: Anchor v2, Anchor v1, and Quasar. Anchor v1 follows the CONTRIBUTING.md rule that landed on main while this PR was open: an Anchor v1 copy tracks its Anchor v2 counterpart. The Anchor v1 copy is not rustfmt-clean on main and sits outside the workspace CI formats, so the port is left unformatted to keep its diff to the change itself. The Kani proofs only model remaining_quantity(), so they are unaffected.

The branch is rebased on main, as two commits: the Anchor v2 and Quasar change, then the Anchor v1 port.

Tests

Each variant gains five tests that fill the bid side with 512 bids from 27 traders:

  • full_side_refuses_an_order_no_better_than_its_worst
  • better_order_evicts_the_worst_and_rests
  • evicted_maker_settles_their_refund
  • eviction_rejects_missing_or_wrong_evicted_accounts
  • trader_can_evict_their_own_worst_order

Run locally with Solana 3.1.14 on the rebased branch. The Anchor counts include main's two critbit depth tests, which still pass with eviction in place_order().

Variant Build Tests
Anchor v2 cargo build-sbf 34 passed: 29 from main and 5 new
Anchor v1 cargo build-sbf 34 passed: 29 from main and 5 new
Quasar quasar build at the pinned rev 11 passed: 6 from main and 5 new

anchor build could not run here, because it tries to install the Solana version pinned in Anchor.toml from a host this environment cannot reach. It wraps the same cargo build-sbf call, and CI runs the real command.

The book's Order Book Exchange chapter describes this eviction in quicknode/solana-book#170. That PR should merge after this one, so the book never names an error that has not shipped.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RBfNjrQd2J6muoi3thCUin

A full side refused every new resting order, so anyone willing to lock the
minimum order size and pay rent for each slot could fill a side with orders
far from the spread and keep every new order on it out. The market authority
had no remedy, and the market PDA's seeds are the two mints, so the pair could
not move to a new market.

place_order now evicts: when the order will rest on a full side and beats that
side's worst price, the worst order is refunded through its owner's unsettled
balance, exactly as cancel_order refunds it (the refund is now one shared
function, credit_unfilled_lock), removed from the book and its owner's open
orders, and stamped Cancelled. An order that does not beat the worst price
still gets OrderBookFull. The caller passes the worst order and its owner's
MarketUser after the maker pairs, or only the order when it is their own,
because Anchor refuses a writable account that appears twice. New errors:
MissingEvictedAccounts and EvictedAccountMismatch.

Also corrects the stated capacity: a side holds 512 orders, not 1024, since
every order after the first adds a leaf and an inner node to the side's
1024-node tree. MAX_ORDERS_PER_SIDE is now MAX_TREE_NODES / 2.

Both the Anchor v2 and Quasar variants change, each with five tests that fill
the bid side to 512 orders. Anchor v1 is a frozen snapshot per CONTRIBUTING.md
and does not change.

Claude-Session: https://claude.ai/code/session_01RBfNjrQd2J6muoi3thCUin
CONTRIBUTING.md now says an Anchor v1 copy tracks its v2 counterpart, so
eviction goes into all three variants rather than only Anchor v2 and Quasar.

The port matches the v2 copy: worst_leaf and OrderBook::worst, the
512-order capacity fix, credit_unfilled_lock shared with cancel_order, the
MissingEvictedAccounts and EvictedAccountMismatch errors, the eviction step
in place_order, the README and CHANGELOG, and the same five tests. When the
worst order is the caller's own, only the order is passed and the caller's
market_user is credited, as in v2. Anchor v1 would accept the caller's
MarketUser a second time, but a second loaded copy would be overwritten
when the instruction writes its accounts back, so the rule matters here too.

This copy is not rustfmt-clean on main and is outside the workspace CI
formats, so the change is left unformatted to keep the diff to the port.

Claude-Session: https://claude.ai/code/session_01RBfNjrQd2J6muoi3thCUin
@mikemaccana
mikemaccana force-pushed the claude/alex-book-feedback-4eciar branch from baeed30 to 5dde8f6 Compare September 23, 2026 14:22
@mikemaccana
mikemaccana merged commit c36f336 into main Sep 23, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant