Engine & Code

The RAIN CLOB matching engine is a fully on-chain central limit order book written in Rust and compiled to WASM via Arbitrum Stylus. Order placement, cancellation, and matching are all on-chain state transitions — no off-chain matching server. This page shows the real engineering: the architecture, the source, and the evidence it works.

Building in the open. What follows is taken from the working engine and the on-chain Stylus contract. The reference engine and the contract are proven byte-identical by a heavy property-based differential test suite.
Live

Live on Arbitrum Sepolia

The RAIN CLOB matching-engine contract is deployed and activated on the Arbitrum Sepolia public testnet. It's verified working on-chain end-to-end: place an order, read it back. Everything below is public on-chain data — go inspect it yourself.

Network
Arbitrum Sepolia · chain-id 421614
On-chain size
23,250 bytes · under the 24,576-byte Stylus activation limit
Verified on-chain
init market → placeLimit (buy) → bestBid() = 500 ✓
Verified on-chain — place an order, read it back. A live smoke test against the deployed contract: initialize a market, submit a placeLimit buy at price 500, then call bestBid() — which returns 500. The on-chain book accepts the order and serves it back exactly. CEX-grade matching, on a dedicated app-specific L3, designed for gas-efficient sub-cent order operations — now demonstrably running on a public testnet.

Integration notes for developers

  • ABI selectors are camelCaseplaceLimit, bestBid, bestAsk (init is unchanged). snake_case calls (e.g. place_limit, best_bid) hit no selector and revert.
  • Tick range: the bitmap book supports ticks 0..1023. Use max_tick ≤ 1023 and keep prices < 1024.
bash · read bestBid() with foundry cast
# Replace the placeholder with your own Arbitrum Sepolia RPC endpoint.
cast call \
  --rpc-url <your-arbitrum-sepolia-rpc> \
  0xfb223f077E22DD7CDccdd21f0700697016cbc139 \
  "bestBid()(uint64)"

Overview

The system is split in two on purpose:

📦 Reference engine

A chain-agnostic, deterministic Rust library — zero blockchain dependencies. Integer-only arithmetic, no clocks, no randomness, no iteration over unordered collections. It is the normative implementation of the matching spec.

⛓️ Stylus contract

The same logic, ported into an Arbitrum Stylus #[entrypoint] contract with a storage-backed order book. Each engine event is emitted as one EVM log, byte-identical to the reference engine's event stream.

Because the two share the exact event encoding and the exact canonical state serialization, the on-chain contract is held to the reference engine's behavior byte-for-byte — and that equivalence is enforced by tests, not by hope.

Engine features

CapabilityWhat it does
On-chain matchingPlacement, cancel, and matching are all on-chain state transitions. The full book is reconstructable from event logs alone.
Price-time priorityStrict: better price first, then FIFO by arrival sequence within a level. No pro-rata, ever. Queue position is exactly modelable.
Order typeslimit and market.
Time-in-forceGTC, IOC (immediate-or-cancel), FOK (fill-or-kill, atomic feasibility).
Flagspost-only (reject if it would take) and reduce-only (may only decrease net position; auto-clamped).
Self-trade preventionThree switchable policies per order: CancelNewest, CancelOldest, Decrement.
Atomic replaceCancel + place in one op. A pure quantity decrease preserves queue position; price change or quantity increase re-queues.
Atomic batchAll-or-nothing place / cancel / replace — built for market-maker quote updates. Any failing op reverts the whole batch.
Bounded matchingA fill budget caps work per transaction; deep crosses suspend into a continuation and resume — so no single op can exceed the block gas limit, even against very deep books.
Canonical state hashA deterministic serialization of the entire book + positions, hashed with SHA-256 — the single value that proves two engines are in identical states.
Deterministic replayReplaying only the event log reproduces the book byte-for-byte. Auditable from logs, with no trust in any operator.

Engineering evidence

The engine is not a sketch. It is validated the way a matching engine should be — adversarially, and against itself.

97
Reference-engine tests passing
49 + 1,500
Stylus-contract tests + randomized differential cases
byte-identical
On-chain contract vs. reference engine, across every sequence
10,000
Resting orders — matching stays depth-independent
< 24 KB
Contract fits under the Stylus activation limit
live
Deployed & activated on Arbitrum Sepolia (details)

🎲 Differential property testing

The Stylus contract is run against the reference engine across 1,500+ randomized operation sequences. After every sequence, the canonical state hashes and the full event streams must match exactly. They do — byte-for-byte.

🔁 Deterministic replay

A replay harness reconstructs the book from the event log alone and checks its canonical hash against the live engine. Identical ordered input always yields identical state and identical events.

📏 Depth independence

Matching cost is governed by a fill budget, not by book depth. The engine is exercised against books up to 10,000 resting orders without blowing the transaction's work bound.

⛽ Gas-efficient by design

Written in Rust/Stylus with a storage layout tuned for the hot matching path. It is designed for sub-cent per-operation cost on a dedicated app-specific L3 — CEX-grade economics with on-chain verifiability.

Core types

Everything is integer-only — no floating point anywhere, including fee math. Newtypes keep prices, quantities, and sequence numbers from ever being confused. The order classification enums map one-to-one to the matching spec:

rust · matching-engine/src/types.rs
/// Book side (spec §3.1): buys match highest price first, sells lowest first.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Side {
    /// Bid: price priority = higher first.
    Buy,
    /// Ask: price priority = lower first.
    Sell,
}

impl Side {
    /// The opposite book side.
    pub const fn opposite(self) -> Side {
        match self {
            Side::Buy => Side::Sell,
            Side::Sell => Side::Buy,
        }
    }
}

/// Order type (spec §2.1). A market order has no price, can never rest, and
/// MUST carry `tif ∈ {IOC, FOK}` (GTC market ⇒ reject `InvalidTif`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OrderType {
    /// Limit order: `(side, price, qty, tif, flags, client_order_id)`.
    Limit,
    /// Market order: `(side, qty, tif ∈ {IOC, FOK}, flags, client_order_id)`.
    Market,
}

/// Time-in-force (spec §2.3).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TimeInForce {
    /// Good-till-canceled: match what crosses, remainder rests (spec §2.3).
    Gtc,
    /// Immediate-or-cancel: match now, cancel remainder
    /// (`OrderCanceled{reason: IocRemainder}`, spec §2.3).
    Ioc,
    /// Fill-or-kill: atomic feasibility check (spec §5.3); all-or-nothing,
    /// infeasible ⇒ `OrderCanceled{reason: FokUnfillable}` with no state
    /// change besides the event.
    Fok,
}

/// Order flags (spec §2.2). `post_only` is limit-only and rejected with IOC,
/// FOK, or market orders (`InvalidFlagCombination` / contradiction rules,
/// spec §2.2).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct OrderFlags {
    /// Post-only: if the order would take liquidity for ANY quantity it is
    /// rejected whole (`PostOnlyWouldCross`) — never re-priced, never
    /// partially posted (spec §2.2, E10/E11: the cross check precedes STP).
    pub post_only: bool,
    /// Reduce-only: may only decrease the account's net position; clamped at
    /// every position-changing event, auto-reduced (`OrderReduced`) or
    /// auto-canceled (`ReduceOnlyAuto`) as reducible amount shrinks
    /// (spec §2.2, E18).
    pub reduce_only: bool,
}

/// Self-trade-prevention policy (spec §7). Per-order parameter with a
/// chain-level default; all three are implemented and switchable.
///
/// Chain default: `CancelNewest` (the most queue-protective policy),
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum StpPolicy {
    /// Cancel the incoming taker remainder; resting order untouched; fills
    /// already executed stand (spec §7.1). **Chain default** (Q2).
    #[default]
    CancelNewest,
    /// Cancel the resting maker; the matching loop continues (spec §7.2).
    CancelOldest,
    /// Decrement both by `min(t, m)` with no fill, no fee, no trade print;
    /// emits `SelfTradeDecremented` (spec §7.3).
    Decrement,
}

The BookIndex trait

The matching loop is written against an abstract price-level index, so the same loop runs over the reference BTreeMap book and over the Stylus storage-backed book. Implementations must be fully deterministic — iteration orders are specified, not incidental:

rust · matching-engine/src/book.rs
/// Abstraction over price-level book operations used by the matching loop.
///
/// Implementations MUST be deterministic (spec D2): iteration orders are
/// fully specified — levels best-first, FIFO by `seq` within a level.
pub trait BookIndex {
    /// Best (highest) bid price, if any bids rest.
    fn best_bid(&self) -> Option<Price>;

    /// Best (lowest) ask price, if any asks rest.
    fn best_ask(&self) -> Option<Price>;

    /// Best price on `side` (`Buy` ⇒ best bid, `Sell` ⇒ best ask).
    fn best(&self, side: Side) -> Option<Price> {
        match side {
            Side::Buy => self.best_bid(),
            Side::Sell => self.best_ask(),
        }
    }

    /// Insert `order` at the tail of the FIFO at its price level on `side`,
    /// creating the level if absent. Returns the order's node handle.
    /// The order MUST be a restable limit order (`price` set).
    fn insert(&mut self, side: Side, price: Price, order: Order) -> u32;

    /// Remove the order with `handle` from `side`/`price`, returning it.
    /// Removes the level when it becomes empty.
    fn remove(&mut self, side: Side, price: Price, handle: u32) -> Order;

    /// Handle of the FIFO head (oldest `seq`) at `side`/`price`, if the
    /// level exists.
    fn level_head(&self, side: Side, price: Price) -> Option<u32>;

    /// Handle of the FIFO tail (newest `seq`) at `side`/`price`, if the
    /// level exists.
    fn level_tail(&self, side: Side, price: Price) -> Option<u32>;

    /// Aggregates for the level at `side`/`price`, if it exists.
    fn level_aggregates(&self, side: Side, price: Price) -> Option<LevelAggregates>;

    /// The next level on `side` strictly *worse* than `price` (for bids:
    /// next lower; for asks: next higher), if any.
    fn next_level_worse(&self, side: Side, price: Price) -> Option<Price>;

    /// Borrow the node behind `handle`.
    fn node(&self, handle: u32) -> &OrderNode;

    /// Decrease the remaining quantity of the order at `handle` by `dec`
    /// lots, keeping level aggregates consistent. Queue position is
    /// preserved (spec §3.1.3). Panics if `dec` exceeds remaining.
    fn decrease_remaining(&mut self, side: Side, price: Price, handle: u32, dec: Qty);

    /// Handle of the next order (later `seq`) after `handle` within its
    /// level, or `None` at the tail.
    fn next_in_level(&self, handle: u32) -> Option<u32>;

    /// Look up a resting order's location by id: `(side, price, handle)`.
    fn locate(&self, id: OrderId) -> Option<(Side, Price, u32)>;

    /// Total number of resting orders across both sides.
    fn order_count(&self) -> u32;

    /// All level prices on `side`, best-first (bids descending, asks
    /// ascending). Allocates; used by serialization/state-hash, not the hot
    /// matching path.
    fn levels_best_first(&self, side: Side) -> Vec<Price>;
}

The matching loop

Best-first across levels, strict FIFO within a level, fills always at the resting (maker) price so price improvement accrues to the taker. Self-trade prevention is enforced inline. This is the core algorithm, verbatim:

rust · matching-engine/src/engine.rs
fn match_loop(&mut self, incoming: &mut Order, budget: Option<u32>) -> (LoopExit, u32) {
        let opposite = incoming.side.opposite();
        let mut makers_touched: u32 = 0;

        while incoming.remaining.0 > 0 {
            let Some(best) = self.book.best(opposite) else {
                break; // empty book
            };
            if let Some(limit) = incoming.price {
                if !Self::crosses(incoming.side, limit, best) {
                    break; // no longer crosses
                }
            }
            if let Some(b) = budget {
                if makers_touched >= b {
                    break; // fill budget exhausted (spec §5.4)
                }
            }
            let head = self
                .book
                .level_head(opposite, best)
                .expect("best level must be non-empty");

            let (maker_id, maker_account, maker_remaining) = {
                let node = self.book.node(head);
                (node.order.id, node.order.account, node.order.remaining)
            };

            // STP (spec §7): incoming would fill against a resting order of
            // the same account — apply the incoming order's policy.
            if maker_account == incoming.account {
                match incoming.stp {
                    StpPolicy::CancelNewest => {
                        // §7.1: cancel the taker's remainder; fills already
                        // executed stand; resting order untouched (keeps its
                        // queue position).
                        self.emit(EventBody::OrderCanceled {
                            order_id: incoming.id,
                            reason: CancelReason::StpCancelNewest,
                            remaining_at_cancel: incoming.remaining,
                        });
                        return (LoopExit::TakerStpTerminated, makers_touched);
                    }
                    StpPolicy::CancelOldest => {
                        // §7.2: cancel the resting maker; the loop continues
                        // with the next FIFO order / next level.
                        let maker = self.book.remove(opposite, best, head);
                        self.untrack(&maker);
                        self.emit(EventBody::OrderCanceled {
                            order_id: maker.id,
                            reason: CancelReason::StpCancelOldest,
                            remaining_at_cancel: maker.remaining,
                        });
                        makers_touched += 1; // maker consumed (module doc)
                        continue;
                    }
                    StpPolicy::Decrement => {
                        // §7.3: decrement both by d = min(t, m); no fill, no
                        // fee, no trade print, no position change.
                        let d = Qty(incoming.remaining.0.min(maker_remaining.0));
                        self.emit(EventBody::SelfTradeDecremented {
                            maker_order_id: maker_id,
                            taker_order_id: incoming.id,
                            qty: d,
                        });
                        incoming.remaining = Qty(incoming.remaining.0 - d.0);
                        if maker_remaining.0 == d.0 {
                            let maker = self.book.remove(opposite, best, head);
                            self.untrack(&maker);
                            self.emit(EventBody::OrderCanceled {
                                order_id: maker.id,
                                reason: CancelReason::StpDecrement,
                                remaining_at_cancel: Qty(0),
                            });
                        } else {
                            self.book.decrease_remaining(opposite, best, head, d);
                        }
                        makers_touched += 1; // maker (partially) consumed
                        if incoming.remaining.0 == 0 {
                            // Taker-terminal (module doc): remainder record
                            // canceled at 0; no residue rules afterwards.
                            self.emit(EventBody::OrderCanceled {
                                order_id: incoming.id,
                                reason: CancelReason::StpDecrement,
                                remaining_at_cancel: Qty(0),
                            });
                            return (LoopExit::TakerStpTerminated, makers_touched);
                        }
                        continue;
                    }
                }
            }
            let fill = Qty(incoming.remaining.0.min(maker_remaining.0));
            let maker_after = Qty(maker_remaining.0 - fill.0);
            incoming.remaining = Qty(incoming.remaining.0 - fill.0);

The Stylus contract

The on-chain entrypoint. account = msg::sender, interned to a compact handle; owner checks become handle equality. Every engine event is written as a single EVM log whose data is the canonical event encoding — so the on-chain log stream is byte-identical to the reference engine's, and off-chain replayers reconstruct the book exactly:

rust · clob-stylus/src/contract.rs
sol_storage! {
    #[entrypoint]
    pub struct Clob {
        /// Tick-bitmap book storage.
        BookStore book;
        /// Engine non-book ledgers + account interning.
        LedgerStore ledger;
        /// Market id (event envelope).
        uint64 market_id;
        /// --- packed MarketConfig (immutable after init) ---
        uint64 tick_size;
        uint64 max_tick;
        uint64 lot_size;
        uint64 min_order_size;
        uint32 fok_max_fill_levels;
        uint32 fill_budget;
        uint32 max_batch_len;
        uint32 max_open_orders_per_account;
        uint8 default_stp;
        /// Initialized flag (init() may run once).
        bool initialized;
    }
}


// -- public ABI -------------------------------------------------------------

#[public]
impl Clob {
    pub fn place_limit(
        &mut self,
        side: u8,
        price: u64,
        qty: u64,
        tif: u8,
        flags: u8,
        stp: u8,
        client_order_id: U256,
    ) -> Result<u64, Vec<u8>> {
        let account = self.caller_handle();
        let cfg = self.read_config();
        let mid = MarketId(self.market_id.get().low_u64());
        let mut sink = LogSink;
        let mut book = BitmapBook::new(&mut self.book);
        let mut ledger = StorageLedger::new(&mut self.ledger);
        let mut ctx = EngineCtx {
            market_id: mid,
            config: &cfg,
            book: &mut book,
            ledger: &mut ledger,
            sink: &mut sink,
        };
        // Standalone placement: a reject is an EVENT + consumed seq, NOT a tx
        // revert (Phase-1 semantics, spec E6). The OrderRejected log was already
        // emitted inside `place_limit`; we return the order id on accept, or 0
        // on reject (the auditable outcome is the log, not the return value).
        Ok(ctx
            .place_limit(
                account,
                side_from_u8(side),
                Price(price),
                Qty(qty),
                tif_from_u8(tif),
                flags_from_u8(flags),
                stp_from_u8(stp),
                ClientOrderId(client_order_id.low_u128()),
            )
            .map(|id| id.0)
            .unwrap_or(0))
    }

    // ... place_market, replace, continue_match, expire_suspended, batch ...
}

Canonical state hash

The whole point of an on-chain CLOB is verifiability. The engine serializes its entire state — book levels best-first, FIFO order within each level, signed positions, any pending continuation — into a canonical byte layout and hashes it with SHA-256. Two engines that reached the same observable state produce the same bytes regardless of how they got there. That single hash is what the differential tests assert on:

rust · matching-engine/src/state_hash.rs
pub fn canonical_state<B: BookIndex + Clone>(engine: &Engine<B>) -> Vec<u8> {
    let mut out = Vec::new();
    out.extend_from_slice(b"RAINBOOKv1");
    engine.market_id().encode(&mut out);
    out.extend_from_slice(&engine.seq_peek().to_be_bytes());

    for side in [Side::Buy, Side::Sell] {
        let book = engine.book();
        let levels = book.levels_best_first(side); // bids desc, asks asc
        out.extend_from_slice(&u32::try_from(levels.len()).unwrap().to_be_bytes());
        for price in levels {
            let agg = book
                .level_aggregates(side, price)
                .expect("listed level must exist");
            price.encode(&mut out);
            out.extend_from_slice(&agg.order_count.to_be_bytes());
            // FIFO head → tail (spec §3.1: seq order within level).
            let mut cursor = book.level_head(side, price);
            while let Some(handle) = cursor {
                encode_order(&book.node(handle).order, &mut out);
                cursor = book.next_in_level(handle);
            }
        }
    }

    let positions = engine.positions(); // BTreeMap: account ascending
    out.extend_from_slice(&u32::try_from(positions.len()).unwrap().to_be_bytes());
    for (account, pos) in positions {
        out.extend_from_slice(&account.to_be_bytes());
        out.extend_from_slice(&pos.to_be_bytes());
    }
    // ... suspended continuation, then SHA-256 of the bytes ...
}

Matching spec

The engine is tested against a normative specification. Any divergence between code and spec is a bug in one of them — and the spec wins until amended. It pins down primitive types and determinism rules, order types and time-in-force, strict price-time priority, the matching loop, thin-book and FOK semantics, bounded matching, batch atomicity, self-trade prevention, the event schema for log-only reconstruction, and an enumerated edge-case table.

Read it: the full public matching-rules specification is published here → RAIN CLOB Matching Rules Specification.

Source downloads

Curated, representative source from the reference engine and the Stylus contract:

FileWhat it contains
types.rsCore primitive newtypes, order enums, reject/cancel reasons, market configDownload
book.rsThe BookIndex trait + reference BTreeMap book with FIFO levelsDownload
engine_match_loop.rsPlacement API + the matching loop with inline STP handlingDownload
state_hash.rsCanonical state serialization + SHA-256 state hashDownload
clob_stylus_contract.rsStylus storage layout + the public contract ABIDownload

Want early access as a market maker or builder? Reach out via the official channels at rain.one.