Order Book
How the limit order book works in the Diffusal protocol
The order book enables peer-to-peer limit order trading for options. Unlike the RFQ system where users trade against the Main Market Maker, the order book allows any user to place and fill orders against each other.
Overview
The order book uses an event-based architecture with off-chain state management:
| Phase | Location | Description |
|---|---|---|
| Order Registration | On-chain | Maker calls registerOrder() which emits an event |
| Order Storage | Off-chain | Backend indexes events and maintains order book state |
| Order Matching | Off-chain | Backend matching engine finds crossing orders |
| Settlement | On-chain | Backend submits settleMatch() transaction |
| Position Updates | On-chain | optionBalance and premiumBalance updated |
Key Benefits:
- Efficient registration — Order registration only emits events
- Dynamic tick-based pricing — Tick size automatically scales with spot price (2-12 decimals)
- Off-chain matching — Fast matching with no on-chain overhead per match attempt
- Partial fills — Orders can be filled incrementally
- Maker incentives — Negative fees (rebates) possible for liquidity providers
Dynamic Tick-Based Pricing
The order book uses discrete ticks with dynamic precision based on the underlying asset's spot price. Higher-priced assets use larger tick sizes (fewer decimals), while lower-priced assets use smaller tick sizes (more decimals).
Tick Size Formula
Tick Size Tiers
| Spot Price | Tick Decimals | Tick Size |
|---|---|---|
| >= $100 | 2 | 0.01 USDC |
| >= $10 | 3 | 0.001 USDC |
| >= $1 | 4 | 0.0001 USDC |
| >= $0.1 | 5 | 0.00001 USDC |
| >= $0.01 | 6 | 0.000001 USDC |
| < $0.01 | 7-12 | Finer granularity |
Key points:
- Tick decimals range from 2 (minimum, 0.01 USDC) to 12 (maximum, 0.000000000001 USDC)
- Every 10x decrease in spot price → +1 decimal (smaller tick size)
- Tick decimals are computed at order registration time from the current spot price
- Each order stores its tick decimals to handle price tier changes
Premium Calculation
where for USDC (6 decimals).
Examples (tick 150, 10 contracts):
| Tick Decimals | Tick Size | Premium |
|---|---|---|
| 2 | 0.01 USDC | $15.00 |
| 3 | 0.001 USDC | $1.50 |
| 4 | 0.0001 USDC | $0.15 |
Cross-Tier Matching
Orders from different tick decimal tiers can match. When an order is placed at one price tier and later matches with an order from a different tier:
- Each order uses its own tick decimals for premium calculation
- Example: An order placed when spot was $100 (2 decimals) can match with an order placed when spot was $9 (3 decimals)
Order Structure
When an order is registered, the following data is emitted in the OrderRegistered event:
| Field | Description |
|---|---|
seriesId | Option series identifier |
orderId | Unique order identifier |
maker | Address that created the order |
isBuy | Direction: true = bid (maker buys), false = ask (maker sells) |
tick | Price tick |
size | Order size in contracts (WAD, 18 decimals) |
expiry | Order validity deadline (unix timestamp) |
timestamp | Block timestamp when registered (for FIFO priority) |
tickDecimals | Tick decimals (2-12) computed from spot price at registration |
Orders are identified by a unique orderId generated from the series, maker, timestamp, and a per-user nonce. The tickDecimals field stores the tick precision at the time of order creation, ensuring deterministic premium calculation even if the spot price crosses tier boundaries later.
Order Types
Bid Order (isBuy = true)
The maker wants to buy options. They are placing a bid. For example, if Alice places a bid to buy 10 contracts at tick 15000 ($1.50) and it matches with Bob's ask:
- Alice (buyer) receives +10 optionBalance (long)
- Bob (seller) receives -10 optionBalance (short)
- Premium balances updated: Alice -$15.00 (payer), Bob +$15.00 (receiver)
- No premium changes hands (only fees; settlement at expiry determines actual cash flow)
Ask Order (isBuy = false)
The maker wants to sell options. They are placing an ask. For example, if Carol places an ask to sell 10 contracts at tick 16000 ($1.60) and it matches with Dave's bid:
- Carol (seller) receives -10 optionBalance (short)
- Dave (buyer) receives +10 optionBalance (long)
- Premium balances updated: Dave -$16.00 (payer), Carol +$16.00 (receiver)
- No premium changes hands (only fees; settlement at expiry determines actual cash flow)
Position Flow Summary (Four-Instrument Model)
Each trade creates four positions (two pairs):
| Order Type | Buyer (Long) | Seller (Short) | Premium Positions |
|---|---|---|---|
Bid (isBuy=true) | Maker: +optionBalance | Taker: -optionBalance | Maker: -premiumBalance (payer), Taker: +premiumBalance (receiver) |
Ask (isBuy=false) | Taker: +optionBalance | Maker: -optionBalance | Taker: -premiumBalance (payer), Maker: +premiumBalance (receiver) |
Key insight: The buyer always gets long (+optionBalance) and becomes a premium payer (-premiumBalance). The seller always gets short (-optionBalance) and becomes a premium receiver (+premiumBalance). No USDC is exchanged at trade time except for fees—premium obligations settle at expiry via the net settlement formula.
Acquiring Long and Short Positions
To acquire a position, place the appropriate order type:
| Desired Position | Order Type | When Filled |
|---|---|---|
| Long (+optionBalance) | Buy order (bid, isBuy=true) | You receive the option |
| Short (-optionBalance) | Sell order (ask, isBuy=false) | You write the option |
You don't need to own an option before selling it. Selling an option you don't have creates a short position (negative optionBalance), representing your obligation to pay the intrinsic value at settlement if the option expires in-the-money. This is sometimes called "writing" or "naked selling" an option.
Execution Flow
Registering an Order
- Maker calls
registerOrder(seriesId, portfolioId, isBuy, tick, size, expiry) - Contract validates: size > 0, tick > 0, expiry in future, series tradeable
- Margin simulation: contract checks that the maker's portfolio would remain healthy after the hypothetical trade via
MarginCalculator.simulatePostTradeHealth() - Unique orderId generated from seriesId, maker, timestamp, and nonce
OrderRegisteredevent emitted (no on-chain storage)- Backend indexes the event and adds order to off-chain order book
Margin simulation at order time ensures that only fillable orders appear on the book. If the maker cannot afford the trade (buyer without sufficient collateral, seller whose portfolio would become unhealthy), the order is rejected.
Off-Chain Matching
MAKER (Alice) BACKEND TAKER (Bob) CONTRACT
│ │ │ │
├── registerOrder(bid @ $1.50) ──────────────────────────────────►│
│ │ │ │
│ │◄── OrderRegistered event ─────────────────│
│ │ │ │
│ │ Index order │ │
│ │ Add to book │ │
│ │ │ │
│ │ ├── registerOrder(ask @ $1.45) ─►│
│ │ │ │
│ │◄── OrderRegistered event ─────────────────│
│ │ │ │
│ │ Index order │ │
│ │ Detect crossing: │ │
│ │ bid.tick >= ask.tick│ │
│ │ fillAmount = min() │ │
│ │ │ │
│ ├── settleMatch(bidOrder, askOrder, fillAmount) ───►│
│ │ │ │
│ │ │ Validate orders │
│ │ │ Update positions │
│ │ │ │
│ │◄── MatchSettled event ────────────────────│
│◄── +optionBalance, -premiumBalance ────────────────────────────│
│ │ │◄── -optionBalance, +premiumBalance ─│- Backend watches
OrderRegisteredevents - Maintains off-chain order book state
- Finds crossing orders where
bid.tick >= ask.tick - Determines fill amount as
min(bid.size, ask.size) - Submits
settleMatch()transaction with order data
Settling a Match
- Backend calls
settleMatch(bidOrder, askOrder, fillAmount)with full order data - Contract validates: orders not expired, prices cross, signatures valid
- Trade executes at ask price (price improvement for buyer)
- Maker/taker determined by timestamp (see below)
- Positions updated for both parties
MatchSettledevent emitted
Maker vs Taker
When two limit orders are matched via settleMatch(), maker/taker is determined by timestamp (who placed their order first):
| Role | Determined By | Fee Charged |
|---|---|---|
| Maker | Earlier timestamp (first order in book) | makerFeeBps (can be negative for rebates) |
| Taker | Later timestamp (matching order) | takerFeeBps (always positive) |
Tiebreaker: If both orders have the same timestamp (placed in same block), the bid order is treated as the maker.
Example: Alice registers an ask at $1.50 at t=100. Bob registers a bid at $1.51 at t=200. When matched:
- Alice (earlier timestamp) = maker, pays maker fee (may get rebate)
- Bob (later timestamp) = taker, pays taker fee
Validation Checks
The contract performs these checks before executing any match:
| Check | Requirement |
|---|---|
| Order expiry | block.timestamp < order.expiry |
| Option expiry | block.timestamp < optionExpiry |
| Price crossing | bid.tick >= ask.tick |
| Fill capacity | fillAmount <= order.size for both orders |
| Series match | Both orders for same seriesId |
| Margin check | Both parties pass post-trade margin check |
Trade Examples
Example 1: Matched Orders
Alice registers a bid at tick 15000 ($1.50) for 10 contracts. Bob registers an ask at tick 14500 ($1.45) for 10 contracts.
The backend detects the crossing (15000 >= 14500) and calls settleMatch():
- Trade executes at ask price (tick 14500 = $1.45)
- Alice gets price improvement: pays $1.45 instead of $1.50
- Alice: optionBalance = +10 (long), premiumBalance = -$14.50 (payer)
- Bob: optionBalance = -10 (short), premiumBalance = +$14.50 (receiver)
- Deposits unchanged (no USDC transferred except fees)
Example 2: Partial Fill
Alice has a bid for 100 contracts. Bob has an ask for only 30 contracts.
Backend settles with fillAmount = 30:
- Alice: optionBalance = +30, order has 70 remaining
- Bob: optionBalance = -30, order fully filled
- Alice's order stays in the off-chain order book for future matches
Premium Balance Recording
No premium changes hands at trade time—only fees are transferred. Premium balances are updated using each order's tick decimals:
where for USDC (6 decimals).
| Direction | Buyer Gets | Seller Gets |
|---|---|---|
| Any trade | Long, -premiumDelta (payer) | Short, +premiumDelta (receiver) |
At expiry, positions settle using the net settlement formula.
Fee Structure
The order book uses a maker/taker fee model with possible maker rebates. See Fee Structure for fee calculations, maker rebates, and examples.
Order Cancellation
Cancellation Methods
| Method | Who Can Call | Effect |
|---|---|---|
| Wait for expiry | N/A | Order naturally expires |
| cancelOrder(order, orderId) | Order maker | Cancel own order using full Order struct + orderId |
| invalidateOrder() | Anyone | Cancel stale order whose maker is now unhealthy |
Cancelling an Order
To cancel an order:
- Call
cancelOrder(order, orderId)with the fullOrderstruct and the orderId from theOrderRegisteredevent - Contract verifies caller is the order maker (
order.maker == msg.sender) - Computes the struct hash (same hash used in settlement verification)
- Marks the order as cancelled on-chain (
cancelledOrders[structHash] = true) - Emits
OrderCancelled(orderId, maker, timestamp, structHash)event - Backend indexes event and removes order from off-chain order book
Order Invalidation (Keeper Function)
Orders can become unfillable after placement due to market moves, other trades consuming margin, or partial liquidations. The invalidateOrder function allows anyone to prune these stale orders:
- Call
invalidateOrder(order, orderId)with the Order struct and order ID - Contract re-runs margin simulation for the maker's current portfolio
- If the maker's portfolio would be unhealthy after the hypothetical trade: order is cancelled on-chain
- If the portfolio is still healthy: transaction reverts with
OrderStillValid
This is designed for keeper bots that maintain order book health by removing orders that can no longer be filled.
Partial Fills
Orders support partial fills—each match can fill less than the full order size.
Fill Tracking
The backend tracks remaining order size. When an order is fully filled (remainingSize == 0), it is removed from the off-chain order book.
An order can be filled multiple times until fully consumed.
Lazy Series Registration
The order book supports lazy series registration—if the series doesn't exist when an order is registered, it's created automatically. See Options Creation for validation rules and the creation flow.
Security Considerations
Order ID Uniqueness
Order IDs are generated from seriesId, maker address, timestamp, and a per-user nonce. This ensures unique IDs even for multiple orders in the same block.
Price Crossing Validation
The contract validates that bid.tick >= ask.tick before executing matches, preventing invalid trades.
Reentrancy Protection
All order operations use reentrancy guards to prevent callback attacks.
Expiry Checks
Both order expiry and option expiry are validated before settlement.
Contract Integration
Dependencies
The DiffusalOptionsOrderBook contract integrates with:
- DiffusalOptionsPositionManager: For
updatePosition()to update optionBalance and premiumBalance - DiffusalCollateralVault: For
isHealthy()margin checks - DiffusalOptionsSeriesRegistry: For series validation and lazy registration
- IERC20 (Collateral Token): For fee collection to feeRecipient
Order Book vs RFQ
| Aspect | Order Book | RFQ |
|---|---|---|
| Counterparty | Any user | Main Market Maker only |
| Price Discovery | Market-driven via bids/asks | MMM quotes |
| Order Registration | On-chain event (registerOrder()) | N/A (MMM creates quotes) |
| Matching | Off-chain backend | Direct fill |
| Execution | settleMatch() by backend | Fill MMM's signed quote |
| Partial Fills | Supported | Supported |
| Maker Fees | Can be negative (rebates) | N/A |
| Best For | Price discovery, liquidity provision | Large orders, instant execution |
Order Book Queries
Since the order book state is maintained off-chain, queries for order book data are handled by the backend API:
- Best Bid/Ask — Get the top of book prices and sizes
- Order Book Depth — Get multiple price levels with aggregated sizes
- User Orders — Get all active orders for a specific user
- Order Status — Check remaining size and fill history for an order
The backend indexes OrderRegistered, OrderCancelled, and MatchSettled events to maintain an accurate off-chain order book state.
Summary
| Component | Description |
|---|---|
| Order | Event-based registration (no on-chain storage) |
| Execution | Off-chain matching + on-chain settlement via settleMatch() |
| Direction | Buyer gets long + premium payer; Seller gets short + premium receiver |
| Premium Balance | Updated at trade time; only fees transferred (premium settles at expiry) |
| Fees | Maker fee (can be negative rebate) + taker fee; collected per fill |
| Cancellation | Via cancelOrder(order, orderId) with full Order struct + orderId, or invalidateOrder() by keepers |
| Partial fills | Supported via off-chain order size tracking |
| Pricing | Dynamic tick-based (2-12 decimals based on spot price) |
The order book provides:
- Event-based registration — Efficient order registration
- Dynamic tick-based pricing — Tick size automatically scales with spot price (2-12 decimals)
- Cross-tier matching — Orders from different tick decimal tiers can match
- Off-chain matching — Fast matching with no on-chain overhead per attempt
- Atomic execution — Single transaction for settlement, position updates, fees
- Flexible fills — Partial fills, batch operations supported
- Maker incentives — Rebates possible via negative maker fees
- Security — Order ID uniqueness, margin checks, expiry validation
Contract Implementation
| Contract | Role |
|---|---|
| DiffusalOptionsOrderBook | Order registration, cancellation, and settlement |
| DiffusalOptionsPositionManager | Position updates via updatePosition() |
| DiffusalOptionsSeriesRegistry | Lazy series registration and validation |
| DiffusalCollateralVault | Margin checks and collateral requirements |
Related
Protocol Documentation
- RFQ Flow — Professional quotes from market makers with instant execution
- Options Creation — How series are lazily registered on first trade
- Margin System — Collateral requirements for positions
- Fees — Maker/taker fee structure
Contract Documentation
- DiffusalOptionsOrderBook — Core order book logic and settlement
- DiffusalOptionsPositionManager — Position updates via
updatePosition() - DiffusalOptionsSeriesRegistry — Lazy series registration and validation
- DiffusalCollateralVault — Margin checks and collateral requirements