OrderBook is the on-chain FIFO spot book implementation. It matches native spot orders, manages resting-order reserves through ISpotBalanceAccount, emits packed trade/book-update events, and supports passive liquidity bands.
Contracts
SpotRouterdeploys spot market proxies, registers them withAccountCore, and administers market state.OrderBookstores canonical market configuration and handles matching, cancels, packed replace, post-fill hooks, swaps, passive liquidity, and execution state.AccountCoreowns balances, account ids, signer permissions, builder approvals, reserves, and settlement.SpotPeripheryis an optional, unprivileged facade for deposits, signed session-authorization forwarding, and multi-market swap estimates.
OrderBook is split by responsibility rather than by product inheritance. Permanent storage and shared constants live in OrderBookState; later modules layer admin, pricing, events, accounts, settlement, queues, passive markers, hooks, matching, placement, packed replacement, passive-liquidity accounting, swaps, and views. The final deployed contract is src/orderbook/OrderBook.sol.
Assets
Spot markets may use ERC20 tokens or native MON. Native MON is represented byaddress(0) in token configuration, market parameters, internal balances, reserves, settlement, and withdrawals. Because address(0) is a valid asset, market existence is tracked with explicit registration flags rather than by checking whether the stored base or quote token is nonzero.
Native/native markets are invalid: one side may be native MON, but base and quote must be different assets.
Quote token decimals are loaded from AccountCore during orderbook initialization, not supplied by the market deployer. ERC20 spot tokens must expose IERC20Metadata.decimals() when governance enables them; native MON is fixed at 18 decimals. The cached quote scale is then used for all quote-notional, reserve, passive-liquidity, and swap math.
Quote conversion is direction-aware. Resting-bid reserves, active-bid fills, and passive-bid fills floor in two deterministic stages: first price * size / sizePrecision, then quote-token scaling by 10 ** quoteTokenDecimals / pricePrecision. Passive asks ceil those two stages per fill. Active asks instead accumulate the exact full-precision numerator within one match and credit each FIFO maker the incremental change in the cumulative ceil. Active-ask credits therefore sum to ceil(sum(price * size * quoteScale) / (sizePrecision * pricePrecision)) without charging an unrelated fee recipient. pricePrecision remains the configured price-scale denominator; base sizing is handled by sizePrecision.
Native Orders
ISpotOrderBook.NativeOrder contains:
sidequantitypricetifexecutionInstructionminSizeAfterBlock
Exact-Input Swaps
swap is a min-out protected exact-input taker entrypoint for aggregator-style flows. isBuy = true spends quote for base, and isBuy = false spends base for quote. The returned SwapResult reports the input actually used and the output actually received after fees. minAmountOut is the swap slippage guard: the orderbook walks available active FIFO and passive liquidity, then reverts unless the final output is high enough.
Buy swaps treat amountIn as a fee-inclusive quote budget. The orderbook converts it once into the maximum raw quote principal. Passive asks consume their per-fill ceiled quote. Active asks consume incremental cumulative-ceil quote and carry the exact remainder across prices and FIFO heads in the match. Atomic budget can remain unused and a partial maker residual can be dust-cancelled. Passive fills debit raw input immediately but withhold output until finalization. The protocol taker fee is ceiling-rounded once on aggregate raw quote; an approved builder fee is ceiling-rounded once on aggregate base output.
estimateSwap is a view simulation of the same exact-input intent. It shadows passive-ask ceilings and the active-ask cumulative remainder across FIFO heads, including stale chunks and dust removal, without mutating queues, balances, or passive band inventory. It cannot predict liquidity or balance changes produced by stateful hooks or state changes between estimation and execution.
One swap is one fee match. A native batch or packed replacement may finalize several matches. Each match ceiling-rounds
taker and builder fees once across its active and passive totals; active maker fees remain per-fill floors.
Routers that only need quotes can call SpotPeriphery.estimateSwap or estimateSwaps instead of integrating with each market directly. The periphery does not execute swaps or bypass account permissions; actual swap calls must still come from the account or a signer authorized in AccountCore.
Reserve Model
Resting bids reserve quote plus the maker fee. Resting asks reserve base. Reserves are deducted from free balance when an order rests. For fills, the orderbook derives consumed reserve from the order’s old size and residual size. Full fills consume the full reserve. Partial fills leavespotReservedBalance equal to the residual order reserve. Cancels and reductions release only the unfilled residual reserve back to free balance.
Maker settlement consumes filled reserves directly from spotReservedBalance; filled maker funds are never temporarily credited to free balance before post-fill hooks run. Taker settlement still debits and credits free balances directly. Protocol fees accrue to the configured fee collector. Builder fees credit the builder’s free AccountCore balance and leave custody through withdraw.
Packed Events
An action emits oneBookUpdatesPacked or TradesPacked per contiguous run. Each cycle logs whichever run was appended first, then the other run. A nonempty
protocolCancelBySlots then emits ProtocolOrdersCancelled after those packed logs. Trade records are 64 bytes; the final record of
each nonempty match carries MATCH_END. Book-update records are 39 bytes. Active trade and live book records carry the
order’s maker-fee snapshot; passive trade and delete records carry zero. See
OrderBook indexing for the layouts and reconstruction rules.
Batch and packed-replace terminal outcomes reuse reserved bits in those same records. A zero-delta book sentinel represents
an otherwise silent IOC, POST_ONLY/ALO skip, empty packed cancel, or non-resting completion. The event signatures and record
widths do not change; indexers must mask base flag bits and skip sentinels for L2/L3.
Dust Handling
FIFO fills enforce the minimum base size that can settle to at least one atomic quote unit at the matched price. Resting orders below that executable unit are canceled as dust. Once a taker order has made a real fill, any remaining taker dust below the executable unit is treated as complete for IOC/FOK semantics rather than forcing a revert or leaving a meaningless unmatched tail. Partial fills that would leave an unexecutable maker residual cancel the residual instead of leaving it at the queue head.Passive Liquidity
Passive bands lock base and/or quote balances throughlockSpotPassiveLiquidity. Burns and fee claims release balances back through the spot balance interface. See Passive Liquidity Events for event indexing.
Passive activity changes visible depth and must be reconstructed from execution and band-update events; see OrderBook Events.
Callable reference
Signatures and parameter types below are generated from the reviewed SDK 0.2.0 ABI (contract revisioncdac5ae1). These are Solidity return values: SDK write methods return a transaction hash and require receipt reconciliation. Viem decodes small Solidity integers as numbers; the SDK normalizes account IDs to bigint on its account-client methods.
getMarketParams()
getMarketParams()
view. Use the account, permission, and execution rules described above and in the linked SDK guide.No parameters.Returns: uint32, uint96, uint32, uint96, uint96, uint256, uint256.baseToken()
baseToken()
view. Use the account, permission, and execution rules described above and in the linked SDK guide.No parameters.Returns: address.quoteToken()
quoteToken()
view. Use the account, permission, and execution rules described above and in the linked SDK guide.No parameters.Returns: address.baseSizeMultiplier()
baseSizeMultiplier()
view. Use the account, permission, and execution rules described above and in the linked SDK guide.No parameters.Returns: uint96.quoteTokenDecimals()
quoteTokenDecimals()
view. Use the account, permission, and execution rules described above and in the linked SDK guide.No parameters.Returns: uint8.passiveSpreadTicks()
passiveSpreadTicks()
view. Use the account, permission, and execution rules described above and in the linked SDK guide.No parameters.Returns: uint32.marketState()
marketState()
view. Use the account, permission, and execution rules described above and in the linked SDK guide.No parameters.Returns: uint8.bestBidAsk()
bestBidAsk()
view. Visible best prices. Empty bid is uint32 max; empty ask is zero.No parameters.Returns: uint32 bid, uint32 ask.getL2Book(uint256)
getL2Book(uint256)
view. Combined visible active and passive depth. Stale-order visibility caps are already applied.uint32[] bidPrices, uint96[] bidSizes, uint32[] askPrices, uint96[] askSizes.getOrderId(uint40, uint8)
getOrderId(uint40, uint8)
view. Use the account, permission, and execution rules described above and in the linked SDK guide.uint64 orderId.getOrderMinSizeAfterBlock(uint40, uint8)
getOrderMinSizeAfterBlock(uint40, uint8)
view. Use the account, permission, and execution rules described above and in the linked SDK guide.uint32 minSizeAfterBlock.batch(uint40, tuple[], uint8[])
batch(uint40, tuple[], uint8[])
nonpayable. Live TRADE authority over the target account is required. Cancels run before the native orders. Post-only skips are possible in successful transactions; other validation failures can revert the whole call.batch(uint40, tuple[], uint8[], bytes32, tuple)
batch(uint40, tuple[], uint8[], bytes32, tuple)
nonpayable. Live TRADE authority over the target account is required. Cancels run before the native orders. Post-only skips are possible in successful transactions; other validation failures can revert the whole call.batch(uint40, tuple[], uint8[], bytes32)
batch(uint40, tuple[], uint8[], bytes32)
nonpayable. Live TRADE authority over the target account is required. Cancels run before the native orders. Post-only skips are possible in successful transactions; other validation failures can revert the whole call.batch(uint40, tuple[], uint8[], tuple)
batch(uint40, tuple[], uint8[], tuple)
nonpayable. Live TRADE authority over the target account is required. Cancels run before the native orders. Post-only skips are possible in successful transactions; other validation failures can revert the whole call.replaceBySlotPacked(uint40, bytes, bytes32, tuple)
replaceBySlotPacked(uint40, bytes, bytes32, tuple)
nonpayable. Live TRADE authority required. Replacements lose queue priority. Slot indexes are reusable; use wallet expected-ID bindings for delayed signed requests.replaceBySlotPacked(uint40, bytes)
replaceBySlotPacked(uint40, bytes)
nonpayable. Live TRADE authority required. Replacements lose queue priority. Slot indexes are reusable; use wallet expected-ID bindings for delayed signed requests.replaceBySlotPacked(uint40, bytes, tuple)
replaceBySlotPacked(uint40, bytes, tuple)
nonpayable. Live TRADE authority required. Replacements lose queue priority. Slot indexes are reusable; use wallet expected-ID bindings for delayed signed requests.replaceBySlotPacked(uint40, bytes, bytes32)
replaceBySlotPacked(uint40, bytes, bytes32)
nonpayable. Live TRADE authority required. Replacements lose queue priority. Slot indexes are reusable; use wallet expected-ID bindings for delayed signed requests.cancelAllOrders(uint40)
cancelAllOrders(uint40)
nonpayable. Cancels the target account’s active orders in this market at execution. Does not remove passive positions or automation schedules.swap(uint40, bool, uint128, uint128, uint64, tuple)
swap(uint40, bool, uint128, uint128, uint64, tuple)
nonpayable. Exact-input amount and minimum output use raw token atoms. The caller needs TRADE permission and free funds. Reverts on self-trade, failed slippage/deadline checks, or other execution failures.(uint128 amountInUsed, uint128 amountOut) result.swap(uint40, bool, uint128, uint128, uint64)
swap(uint40, bool, uint128, uint128, uint64)
nonpayable. Exact-input amount and minimum output use raw token atoms. The caller needs TRADE permission and free funds. Reverts on self-trade, failed slippage/deadline checks, or other execution failures.(uint128 amountInUsed, uint128 amountOut) result.estimateSwap(bool, uint128, uint32)
estimateSwap(bool, uint128, uint32)
view. Returns an estimated result without changing state. It cannot predict stateful hook effects or later liquidity changes. SDK 0.2.0 exposes a loosely typed helper; a direct typed ABI read preserves result types.(uint128 amountInUsed, uint128 amountOut) result.estimateSwap(bool, uint128)
estimateSwap(bool, uint128)
view. Returns an estimated result without changing state. It cannot predict stateful hook effects or later liquidity changes. SDK 0.2.0 exposes a loosely typed helper; a direct typed ABI read preserves result types.(uint128 amountInUsed, uint128 amountOut) result.estimateSwap(uint40, bool, uint128, uint32)
estimateSwap(uint40, bool, uint128, uint32)
view. Returns an estimated result without changing state. It cannot predict stateful hook effects or later liquidity changes. SDK 0.2.0 exposes a loosely typed helper; a direct typed ABI read preserves result types.(uint128 amountInUsed, uint128 amountOut) result.estimateSwap(uint40, bool, uint128)
estimateSwap(uint40, bool, uint128)
view. Returns an estimated result without changing state. It cannot predict stateful hook effects or later liquidity changes. SDK 0.2.0 exposes a loosely typed helper; a direct typed ABI read preserves result types.(uint128 amountInUsed, uint128 amountOut) result.getPassiveBand(uint32)
getPassiveBand(uint32)
view. Use the account, permission, and execution rules described above and in the linked SDK guide.(uint32 lowPrice, uint32 highPrice, uint128 quoteAtLow, uint128 baseAtHigh, uint128 totalShares, uint256 feeGrowthBasePerShareX128, uint256 feeGrowthQuotePerShareX128) band.getPassivePosition(uint64)
getPassivePosition(uint64)
view. Use the account, permission, and execution rules described above and in the linked SDK guide.(uint40 ownerId, uint32 lowPrice, uint32 highPrice, uint128 shares, uint256 lastFeeGrowthBaseX128, uint256 lastFeeGrowthQuoteX128) position.getPostFillHook(uint40)
getPostFillHook(uint40)
view. Use the account, permission, and execution rules described above and in the linked SDK guide.address hook.setPostFillHook(uint40, address)
setPostFillHook(uint40, address)
nonpayable. Requires live TRADE authority; a nonzero hook also needs AccountCore eligibility and valid hook code.