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.
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.
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 camelCase —
placeLimit,bestBid,bestAsk(initis unchanged). snake_case calls (e.g.place_limit,best_bid) hit no selector and revert. - Tick range: the bitmap book supports ticks
0..1023. Usemax_tick ≤ 1023and keep prices< 1024.
# 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
| Capability | What it does |
|---|---|
| On-chain matching | Placement, cancel, and matching are all on-chain state transitions. The full book is reconstructable from event logs alone. |
| Price-time priority | Strict: better price first, then FIFO by arrival sequence within a level. No pro-rata, ever. Queue position is exactly modelable. |
| Order types | limit and market. |
| Time-in-force | GTC, IOC (immediate-or-cancel), FOK (fill-or-kill, atomic feasibility). |
| Flags | post-only (reject if it would take) and reduce-only (may only decrease net position; auto-clamped). |
| Self-trade prevention | Three switchable policies per order: CancelNewest, CancelOldest, Decrement. |
| Atomic replace | Cancel + place in one op. A pure quantity decrease preserves queue position; price change or quantity increase re-queues. |
| Atomic batch | All-or-nothing place / cancel / replace — built for market-maker quote updates. Any failing op reverts the whole batch. |
| Bounded matching | A 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 hash | A deterministic serialization of the entire book + positions, hashed with SHA-256 — the single value that proves two engines are in identical states. |
| Deterministic replay | Replaying 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.
🎲 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:
/// 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:
/// 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:
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:
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:
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.
Source downloads
Curated, representative source from the reference engine and the Stylus contract:
| File | What it contains | |
|---|---|---|
types.rs | Core primitive newtypes, order enums, reject/cancel reasons, market config | Download |
book.rs | The BookIndex trait + reference BTreeMap book with FIFO levels | Download |
engine_match_loop.rs | Placement API + the matching loop with inline STP handling | Download |
state_hash.rs | Canonical state serialization + SHA-256 state hash | Download |
clob_stylus_contract.rs | Stylus storage layout + the public contract ABI | Download |
Want early access as a market maker or builder? Reach out via the official channels at rain.one.