All articles
solanaphoenixclobdefi

Phoenix: the crankless on-chain order book, and the perps extension

Phoenix is Solana's crankless on-chain limit order book — atomic fills, no separate crank, all in one Rust program. Here's the market architecture and design.

Phoenix is the cleanest fully on-chain limit order book on Solana. Its headline innovation: it's crankless. Earlier on-chain order books (Serum/OpenBook) needed a separate "crank" process to match resting orders and settle fills. Phoenix settles atomically inside the taker's own transaction — no crank, no separate settlement step, no MEV window between match and settle.

Why the crank was a problem

Serum-style books worked like this: makers post orders; takers post orders; a permissionless "crank" transaction later matches them and pushes fills to an event queue; another step settles balances. Three issues:

  • Latency. Your fill wasn't final until the crank ran — could be several slots later.
  • MEV. The gap between match and settle was a window for extraction.
  • Liveness dependence. If nobody cranked, the book stalled.

Phoenix's crankless design

Phoenix collapses match + settle into the taker's instruction. When you send a market order (or a marketable limit order):

text
Taker submits an order in transaction T:
  1. Phoenix walks the resting order book in-program
  2. Matches against resting maker orders, computing fills
  3. Atomically credits/debits both the taker AND the matched makers'
     balances — all inside transaction T
  4. Resting orders that got filled are removed from the book
  5. Any unfilled taker remainder rests on the book (for limit) or
     is cancelled (for IOC/market)

No crank. No event queue to drain. Fill is final when T confirms.

The makers don't need to be online or sign anything at fill time — their resting orders already committed their funds to the market. Phoenix moves the funds on their behalf when a taker crosses their price.

The account model

rust
// Market account — the order book itself
pub struct MarketHeader {
    pub base_mint:        Pubkey,       // e.g. SOL
    pub quote_mint:       Pubkey,       // e.g. USDC
    pub base_vault:       Pubkey,       // token vault holding deposited base
    pub quote_vault:      Pubkey,       // token vault holding deposited quote
    pub base_lot_size:    u64,          // smallest tradeable base increment
    pub quote_lot_size:   u64,          // smallest price increment
    pub tick_size:        u64,
    pub authority:        Pubkey,
    pub fee_recipient:    Pubkey,
    // followed by the bids tree + asks tree + trader registry (in account data)
}

// Seat — a trader's authorization to place orders on a market
// (Phoenix gates makers via seats; takers can trade without one)
pub struct Seat {
    pub market:           Pubkey,
    pub trader:           Pubkey,
    pub approval_status:  SeatApprovalStatus,
}

The order book lives entirely in the market account's data — a pair of red-black-tree-like structures for bids and asks, plus a trader registry tracking each participant's locked and free balances. Reading the book is a single getAccountInfo + deserialize.

Placing orders

typescript
import { Client, Side, OrderPacket } from "@ellipsis-labs/phoenix-sdk"
import { Connection, Keypair } from "@solana/web3.js"

const conn = new Connection("https://api.mainnet-beta.solana.com")
const client = await Client.create(conn)
const market = client.marketStates.get(marketAddress)!

// A limit order: rest on the book at a price
const limitOrder: OrderPacket = {
  __kind: "Limit",
  side: Side.Bid,
  priceInTicks: market.floatPriceToTicks(142.50),
  numBaseLots: market.rawBaseUnitsToBaseLots(10),   // 10 SOL
  selfTradeBehavior: 0,
  matchLimit: null,
  clientOrderId: 0,
  useOnlyDepositedFunds: false,
}

const placeIx = client.createPlaceLimitOrderInstruction(limitOrder, marketAddress, trader.publicKey)

// An immediate-or-cancel market buy: cross the book now
const marketOrder: OrderPacket = {
  __kind: "ImmediateOrCancel",
  side: Side.Bid,
  priceInTicks: null,                                // null = take any price
  numBaseLots: market.rawBaseUnitsToBaseLots(5),
  minBaseLotsToFill: 0,
  numQuoteLots: 0,
  minQuoteLotsToFill: 0,
  selfTradeBehavior: 0,
  matchLimit: null,
  clientOrderId: 0,
  useOnlyDepositedFunds: false,
}

Orders are priced and sized in lots — integer multiples of the market's base_lot_size and quote_lot_size. This keeps all arithmetic in integers (no float drift in the matching engine) and bounds the precision of the book.

Reading the live book

typescript
const ladder = market.getUiLadder(10)   // top 10 levels each side
console.log("bids:", ladder.bids)        // [{ price, quantity }, …]
console.log("asks:", ladder.asks)

// Or get the L3 (per-order) view
const book = market.getLadder()

The perps extension

Phoenix's core is a spot CLOB. The perps layer builds the same crankless matching primitive into a derivatives market — positions instead of spot balances, funding payments instead of settlement, mark-price from the book combined with an oracle. The order-matching engine underneath is the same atomic, crankless design; the difference is what gets settled (position deltas + funding vs token balances).

Why this still matters

  • It proved fully on-chain CLOBs are viable. The conventional wisdom was "order books need an off-chain sequencer." Phoenix showed you can do it on-chain on Solana with atomic settlement.
  • No MEV from match/settle gaps. The atomic fill means there's no window to sandwich between matching and settlement.
  • Composable. Because fills are atomic, another program can CPI into Phoenix and know the fill is final within the same transaction — critical for aggregators and structured products.

References

Phoenix is the reference design for crankless on-chain order books. Whether you trade on it, build on it, or just want to understand how a fully-on-chain CLOB settles atomically — the crankless model above is the core idea.

Phoenix: the crankless on-chain order book, and the perps extension | devrels.xyz