Skip to content
On this page

L3-5: Order Book (On-Chain Order Book)

1. Problem

The order book is the core infrastructure of traditional exchanges (NASDAQ, NYSE, Binance), but implementing one directly on Ethereum mainnet faces severe gas obstacles. An active order book handles thousands of orders, cancellations, and matches per second — if every operation required an L1 transaction, gas costs would make small-ticket trading impossible. AMMs (Automated Market Makers) cleverly sidestep this problem with the constant product formula, but AMMs suffer from slippage on large trades and are less efficient at price discovery than order books.

How can you implement a limit order book on-chain while controlling gas costs? The core challenges are: makers need to lock tokens to prevent spoof orders, takers need to find the best-priced counterparty, and the matching process must execute atomically to protect both parties.

2. Rationale

As L2s mature (gas fees as low as $0.01 on Base, Arbitrum, and Optimism), on-chain order books have become viable again. Compared to AMMs, order books offer more precise price control (limit orders), lower slippage (especially for large trades), and more flexible order types (stop-loss orders, iceberg orders, etc.). Understanding on-chain order book design patterns is foundational for interacting with centralized exchanges, building aggregators, and on-chain derivatives.

The hybrid model of order book + AMM is becoming a trend in the industry: Uniswap V4 introduced hooks to support limit orders, and CoWSwap uses off-chain order books + on-chain batch settlement. Mastering order book patterns is key to understanding next-generation DEX architecture.

3. Solution

OnChainOrderBook.sol implements a limit order book supporting partial fills.

Data Structure: the Order struct records the maker, baseToken/quoteToken (trading pair), side (BUY/SELL), price (precision 1e18), amount (total quantity), filled (already filled quantity), and status (ACTIVE/PARTIALLY_FILLED/FILLED/CANCELLED).

Place Order: placeOrder() is called by the maker. When placing a sell order, the maker locks baseToken; when placing a buy order, the maker locks quoteToken (total buy cost = amount * price / 1e18). Tokens are held in escrow by the contract until filled or cancelled.

Fill Order: fillOrder() is called by the taker, specifying the order ID and amount to fill. Partial fills are supported — if fillAmount < remaining, the order status changes to PARTIALLY_FILLED. Assets are exchanged atomically:

  • Filling a buy order: taker sends baseToken to maker, receives quoteToken from escrow
  • Filling a sell order: taker sends quoteToken to maker, receives baseToken from escrow

Cancel Order: cancelOrder() is called by the maker, valid only for orders in ACTIVE or PARTIALLY_FILLED status. The unfilled portion of escrowed tokens is returned to the maker.

Reference source file: src/level3/OnChainOrderBook.sol

4. Pitfalls Encountered

  • Lack of price-time priority: the contract itself does not guarantee price-time priority; anyone can fill orders in any order, potentially causing the best-priced order to be skipped
  • Refund calculation after partial fill: when cancelling an order, the unfilled amount of tokens is refunded, but the buy order refund needs proportional calculation (unfilled * price) / 1e18; integer division can cause a 1-wei rounding discrepancy
  • Self-trading risk: the fill order check does not explicitly prohibit maker == taker, which can lead to meaningless self-trades wasting gas
  • Orders can be front-run: malicious actors can monitor the mempool for fill transactions and submit a same-price fill ahead of the taker (priority gas auction)
  • Order book has no sorting mechanism: there is no efficient way to sort orders by price on-chain; takers need to scan all orders off-chain to find the best price

5. Root Causes of Pitfalls

The price-time priority problem is the fundamental challenge of on-chain order books. On centralized exchanges, the matching engine runs in memory and can sort and match the order book in milliseconds. On-chain, all data is stored in EVM state, with no native sorted data structures (no heaps, priority queues, etc.). This means best-price discovery must happen off-chain — an off-chain service scans the order book, finds the best-priced order, and then guides the user to execute on-chain.

This introduces a trust assumption: users need to trust that the off-chain service will not provide incorrect or suboptimal prices. In OnChainOrderBook.sol, fillOrder() accepts any _orderId without any price verification. In practice, a production implementation should allow the taker to specify an expected price range, and the contract should verify that the filled order's price is indeed within that range.

The front-running problem also exists in AMMs but is more severe in order books — because orders are public, attackers can scan all resting orders, identify profitable fill opportunities, and submit ahead. Mitigations include: using a batch auction model, where multiple orders are aggregated and settled uniformly within a single block; or using sealed orders to prevent mempool visibility.

6. How to Resolve the Pitfalls

Off-chain matching + on-chain verification: build an off-chain matching service that sorts orders by price and guides users to fill. Add price range verification in the contract:

solidity
function fillOrder(uint256 _orderId, uint256 _fillAmount, uint256 _maxPrice, uint256 _minPrice) external {
    Order storage order = orders[_orderId];
    require(order.price >= _minPrice && order.price <= _maxPrice, "Price out of bounds");
    // ... rest of logic
}

Prevent self-trading:

solidity
require(order.maker != msg.sender, "Cannot fill own order");

Batch auctions: for high-activity trading pairs, consider periodic batch settlement — collect all fill and place demand over a time window, and compute the optimal matching and clearing price within a single block. CoWSwap's solution follows this model.

L2 optimization: deploy the order book on L2 to benefit from sub-second block times and sub-cent gas fees, making on-chain order books economically viable. Additionally, you can leverage pre-confirmations provided by L2 sequencers for a better user experience.

7. Technical Highlights

TechniqueDescription
Maker/Taker ModelMaker provides liquidity (places orders), Taker consumes liquidity (fills orders)
Escrow MechanismTokens locked when placing orders, preventing spoof orders
Partial Fillsamount - filled tracks remaining quantity
Four StatesACTIVE → PARTIALLY_FILLED → FILLED/CANCELLED
Dual Token PairbaseToken (underlying) + quoteToken (pricing)
Price Precision1e18 scaling, similar to Uniswap's Q64.96 format
User TrackinguserOrders mapping maintains each user's order list
Price DiscoveryOff-chain scanning + on-chain verification (recommended for production)

Built with AiAda