Diffusal

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:

PhaseLocationDescription
Order RegistrationOn-chainMaker calls registerOrder() which emits an event
Order StorageOff-chainBackend indexes events and maintains order book state
Order MatchingOff-chainBackend matching engine finds crossing orders
SettlementOn-chainBackend submits settleMatch() transaction
Position UpdatesOn-chainoptionBalance 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

tickDecimals=clamp(log10(spotPrice/10000),2,12)\text{tickDecimals} = \text{clamp}(-\lfloor\log_{10}(\text{spotPrice} / 10000)\rfloor, 2, 12)

Tick Size Tiers

Spot PriceTick DecimalsTick Size
>= $10020.01 USDC
>= $1030.001 USDC
>= $140.0001 USDC
>= $0.150.00001 USDC
>= $0.0160.000001 USDC
< $0.017-12Finer 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

premium=tick×tickSize×size1018\text{premium} = \frac{\text{tick} \times \text{tickSize} \times \text{size}}{10^{18}}

where tickSize=106tickDecimals\text{tickSize} = 10^{6-\text{tickDecimals}} for USDC (6 decimals).

Examples (tick 150, 10 contracts):

Tick DecimalsTick SizePremium
20.01 USDC$15.00
30.001 USDC$1.50
40.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:

FieldDescription
seriesIdOption series identifier
orderIdUnique order identifier
makerAddress that created the order
isBuyDirection: true = bid (maker buys), false = ask (maker sells)
tickPrice tick
sizeOrder size in contracts (WAD, 18 decimals)
expiryOrder validity deadline (unix timestamp)
timestampBlock timestamp when registered (for FIFO priority)
tickDecimalsTick 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 TypeBuyer (Long)Seller (Short)Premium Positions
Bid (isBuy=true)Maker: +optionBalanceTaker: -optionBalanceMaker: -premiumBalance (payer), Taker: +premiumBalance (receiver)
Ask (isBuy=false)Taker: +optionBalanceMaker: -optionBalanceTaker: -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 PositionOrder TypeWhen 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

  1. Maker calls registerOrder(seriesId, portfolioId, isBuy, tick, size, expiry)
  2. Contract validates: size > 0, tick > 0, expiry in future, series tradeable
  3. Margin simulation: contract checks that the maker's portfolio would remain healthy after the hypothetical trade via MarginCalculator.simulatePostTradeHealth()
  4. Unique orderId generated from seriesId, maker, timestamp, and nonce
  5. OrderRegistered event emitted (no on-chain storage)
  6. 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 ─│
  1. Backend watches OrderRegistered events
  2. Maintains off-chain order book state
  3. Finds crossing orders where bid.tick >= ask.tick
  4. Determines fill amount as min(bid.size, ask.size)
  5. Submits settleMatch() transaction with order data

Settling a Match

  1. Backend calls settleMatch(bidOrder, askOrder, fillAmount) with full order data
  2. Contract validates: orders not expired, prices cross, signatures valid
  3. Trade executes at ask price (price improvement for buyer)
  4. Maker/taker determined by timestamp (see below)
  5. Positions updated for both parties
  6. MatchSettled event emitted

Maker vs Taker

When two limit orders are matched via settleMatch(), maker/taker is determined by timestamp (who placed their order first):

RoleDetermined ByFee Charged
MakerEarlier timestamp (first order in book)makerFeeBps (can be negative for rebates)
TakerLater 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:

CheckRequirement
Order expiryblock.timestamp < order.expiry
Option expiryblock.timestamp < optionExpiry
Price crossingbid.tick >= ask.tick
Fill capacityfillAmount <= order.size for both orders
Series matchBoth orders for same seriesId
Margin checkBoth 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:

premiumDelta=tick×tickSize×fillAmount1018\text{premiumDelta} = \frac{\text{tick} \times \text{tickSize} \times \text{fillAmount}}{10^{18}}

where tickSize=106tickDecimals\text{tickSize} = 10^{6-\text{tickDecimals}} for USDC (6 decimals).

DirectionBuyer GetsSeller Gets
Any tradeLong, -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

MethodWho Can CallEffect
Wait for expiryN/AOrder naturally expires
cancelOrder(order, orderId)Order makerCancel own order using full Order struct + orderId
invalidateOrder()AnyoneCancel stale order whose maker is now unhealthy

Cancelling an Order

To cancel an order:

  1. Call cancelOrder(order, orderId) with the full Order struct and the orderId from the OrderRegistered event
  2. Contract verifies caller is the order maker (order.maker == msg.sender)
  3. Computes the struct hash (same hash used in settlement verification)
  4. Marks the order as cancelled on-chain (cancelledOrders[structHash] = true)
  5. Emits OrderCancelled(orderId, maker, timestamp, structHash) event
  6. 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:

  1. Call invalidateOrder(order, orderId) with the Order struct and order ID
  2. Contract re-runs margin simulation for the maker's current portfolio
  3. If the maker's portfolio would be unhealthy after the hypothetical trade: order is cancelled on-chain
  4. 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

AspectOrder BookRFQ
CounterpartyAny userMain Market Maker only
Price DiscoveryMarket-driven via bids/asksMMM quotes
Order RegistrationOn-chain event (registerOrder())N/A (MMM creates quotes)
MatchingOff-chain backendDirect fill
ExecutionsettleMatch() by backendFill MMM's signed quote
Partial FillsSupportedSupported
Maker FeesCan be negative (rebates)N/A
Best ForPrice discovery, liquidity provisionLarge 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

ComponentDescription
OrderEvent-based registration (no on-chain storage)
ExecutionOff-chain matching + on-chain settlement via settleMatch()
DirectionBuyer gets long + premium payer; Seller gets short + premium receiver
Premium BalanceUpdated at trade time; only fees transferred (premium settles at expiry)
FeesMaker fee (can be negative rebate) + taker fee; collected per fill
CancellationVia cancelOrder(order, orderId) with full Order struct + orderId, or invalidateOrder() by keepers
Partial fillsSupported via off-chain order size tracking
PricingDynamic 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

ContractRole
DiffusalOptionsOrderBookOrder registration, cancellation, and settlement
DiffusalOptionsPositionManagerPosition updates via updatePosition()
DiffusalOptionsSeriesRegistryLazy series registration and validation
DiffusalCollateralVaultMargin checks and collateral requirements

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

On this page