Diffusal

DiffusalOptionsOrderBook

Event-based limit order book with dynamic tick precision

The DiffusalOptionsOrderBook contract provides event-based limit order trading for options. Orders are registered on-chain via events and indexed by an off-chain backend that maintains order book state.


Overview

The order book uses an event-based architecture with off-chain state management:

ComponentLocationDescription
Order registrationOn-chainregisterOrder() emits event (no on-chain storage)
Order storageOff-chainBackend indexes events, maintains order book
Order matchingOff-chainBackend matching engine finds crossed orders
SettlementOn-chainsettleMatch() executes with order data from backend

Key Features

  • Efficient registration — Orders registered via events
  • Dynamic tick precision — 2-12 decimals based on spot price from Oracle
  • Partial fills — Orders can be filled incrementally
  • Maker rebates — Negative maker fees incentivize liquidity providers
  • Margin enforcement — Post-trade health checks via CollateralVault
  • Lazy series creation — Series registered on first trade

Key Concepts

Tick-Based Pricing

Prices are represented as ticks with dynamic decimal precision. The tick precision (tickDecimals) is fetched from the Oracle at order registration time and included in the event.

priceWad=tick×10(18tickDecimals)\text{priceWad} = \text{tick} \times 10^{(18 - \text{tickDecimals})}
Spot Price RangeTick DecimalsTick Size Example
>= $10020.01
>= $1030.001
>= $140.0001
>= $0.150.00001
>= $0.0160.000001
< $0.017-12Finer precision

Order Types

Order DirectionisBuyBuyer GetsSeller GetsPremium Flow
BidtrueLong (+size)Short (-size)Buyer → Seller
AskfalseLong (+size)Short (-size)Buyer → Seller

Key insight: The buyer always gets long positions, the seller gets short. The isBuy flag determines which party is the buyer (maker for bids, taker for asks).

Fee Model

Fee TypeStorage TypeCan Be Negative?Description
makerFeeBpsint256Yes (rebates)Fee charged to order creator (negative = rebate)
takerFeeBpsuint256NoFee charged to order filler

Negative maker fees: When makerFeeBps < 0, the maker receives a rebate (incentive to provide liquidity). The protocol pays the maker from the taker fee proceeds.

Fee invariant: takerFeeBps + makerFeeBps > 0 ensures net positive protocol revenue even with maker rebates. This invariant prevents the protocol from paying out more in rebates than it collects in fees.


Storage & State

The contract uses minimal on-chain storage since orders are event-based. Key dependencies (position manager, series registry, oracle, collateral token) are immutables set at deployment.

Immutables

IERC20 public immutable COLLATERAL_TOKEN;        // USDC token
IDiffusalOptionsPositionManager public immutable POSITION_MANAGER;
IDiffusalOptionsSeriesRegistry public immutable SERIES_REGISTRY;
IDiffusalOracle public immutable ORACLE;         // For tick decimals

ERC-7201 Namespaced Storage

/// @custom:storage-location erc7201:diffusal.storage.OrderBook
struct OrderBookStorage {
    address owner;
    address pendingOwner;                        // For two-step ownership transfer
    mapping(address => bool) operators;          // Authorized operators for settleMatch
    mapping(address => uint256) userOrderNonce;  // For unique order ID generation
    int256 makerFeeBps;
    uint256 takerFeeBps;
    address feeRecipient;
    address collateralVault;                     // Vault for premium transfers
    address marginCalculator;                    // Margin calculator for health checks
    mapping(bytes32 => bool) cancelledOrders;    // On-chain cancellation tracking (struct hash)
    mapping(bytes32 => uint128) orderFilled;     // Order fill tracking
    mapping(bytes32 => uint256) orderPortfolioId; // Order to portfolio mapping
}

Order ID Generation

orderId = keccak256(abi.encodePacked(seriesId, maker, timestamp, nonce));

Series ID Generation

seriesId = keccak256(abi.encodePacked(pairId, strike, expiry, isCall));

External Functions

Order Registration

registerOrderInPortfolio

Registers a new limit order in a specific portfolio by emitting an event (no on-chain storage).

function registerOrderInPortfolio(
    bytes32 seriesId,
    uint256 portfolioId,
    bool isBuy,
    uint32 tick,
    uint128 size,
    uint32 expiry
) external returns (bytes32 orderId)
ParameterTypeDescription
seriesIdbytes32Option series identifier
portfolioIduint256Portfolio ID (0 = default portfolio)
isBuybooltrue for bid, false for ask
tickuint32Price in ticks
sizeuint128Number of contracts (WAD)
expiryuint32Order validity deadline (unix timestamp)

Returns: orderId — Unique identifier for the registered order.

Validation checks:

  • size > 0
  • tick > 0
  • expiry > block.timestamp
  • Series is tradeable (not expired, not settled)
  • Margin simulation passes (portfolio remains healthy after hypothetical trade)

Effect:

  1. Validates input parameters
  2. Fetches tickDecimals from Oracle based on current spot price
  3. Simulates post-trade margin health via MarginCalculator.simulatePostTradeHealth()
  4. Generates unique orderId from series, maker, timestamp, and nonce
  5. Increments user's order nonce
  6. Emits OrderRegistered event

Note: No order data is stored on-chain. The backend indexes the event and maintains order book state. The margin simulation checks that the maker's portfolio would remain healthy if this order were filled at the specified price.

Emits: OrderRegistered(seriesId, orderId, maker, isBuy, tick, size, expiry, timestamp, tickDecimals, portfolioId, nonce)


registerOrderInPortfolioWithSeriesParams

Registers an order in a portfolio with series parameters for lazy series creation.

function registerOrderInPortfolioWithSeriesParams(
    bytes32 seriesId,
    uint256 portfolioId,
    bool isBuy,
    uint32 tick,
    uint128 size,
    uint32 expiry,
    IDiffusalOptionsSeriesRegistry.SeriesParams calldata seriesParams
) external returns (bytes32 orderId)

Additional parameters:

  • portfolioId — Portfolio ID (0 = default portfolio)
  • seriesParams — Parameters for series validation/creation if series doesn't exist

Order Cancellation

cancelOrder

Cancels an order using the full order struct and the orderId from the OrderRegistered event. Uses the struct hash for on-chain cancellation tracking, and emits both the orderId (for indexer correlation) and structHash (for on-chain verification).

function cancelOrder(OrderTypes.Order calldata order, bytes32 orderId) external
ParameterTypeDescription
orderOrderTypes.OrderThe order struct to cancel
orderIdbytes32The order ID from OrderRegistered (for indexer lookup)

Validation checks:

  • Caller must be the order maker (order.maker == msg.sender)
  • Order must not already be cancelled

Effect:

  1. Computes the struct hash of the order
  2. Verifies the caller is the order maker
  3. Marks the order as cancelled on-chain (cancelledOrders[structHash] = true)
  4. Emits OrderCancelled event with both orderId and structHash

On-chain verification: The cancelledOrders mapping (keyed by struct hash) is checked during settleMatch() to prevent settlement of cancelled orders.

Emits: OrderCancelled(orderId, maker, timestamp, structHash)


Order Invalidation

invalidateOrder

Allows anyone to cancel an order whose maker's portfolio can no longer support the trade. This is a keeper function for cleaning stale orders from the book.

function invalidateOrder(OrderTypes.Order calldata order, bytes32 orderId) external
ParameterTypeDescription
orderOrderTypes.OrderThe order struct to invalidate
orderIdbytes32The order ID (for portfolio lookup)

Validation checks:

  • Order must not already be cancelled
  • Order must not be expired
  • Margin calculator must be set
  • Maker's portfolio must fail the margin simulation (order is unhealthy)

Effect:

  1. Re-runs the margin simulation for the order maker's current portfolio state
  2. If the portfolio would be unhealthy after the hypothetical trade: marks the order as cancelled and emits OrderCancelled
  3. If the portfolio is still healthy: reverts with OrderStillValid

Use case: Keepers call this to prune orders that have become unfillable due to market moves, other trades consuming margin, or partial liquidations.

Emits: OrderCancelled(orderId, maker, timestamp, structHash)


Order Settlement

settleMatch

Settles a matched order pair (called by backend matcher with signed orders).

function settleMatch(
    OrderTypes.Order calldata makerOrder,
    bytes calldata makerSignature,
    OrderTypes.Order calldata takerOrder,
    bytes calldata takerSignature,
    uint128 fillAmount,
    bool makerIsBid,
    uint256 makerPortfolioId,
    uint256 takerPortfolioId
) external

Order struct:

struct Order {
    address maker;     // Order creator (signer)
    bytes32 seriesId;  // Option series identifier
    bool isBuy;        // true = maker wants to buy, false = maker wants to sell
    uint256 price;     // Price per contract in WAD (1e18)
    uint256 size;      // Number of contracts in WAD (1e18)
    uint256 nonce;     // Maker's current nonce (for invalidation)
    uint256 expiry;    // Order expiry timestamp
}
ParameterTypeDescription
makerOrderOrderTypes.OrderThe maker order struct
makerSignaturebytesEIP-712 signature from maker
takerOrderOrderTypes.OrderThe taker order struct
takerSignaturebytesEIP-712 signature from taker
fillAmountuint128Amount of contracts to fill
makerIsBidbooltrue if bid order was placed first (maker), false if ask was first

Validation checks:

  • fillAmount > 0
  • Valid EIP-712 signatures for both orders
  • Both orders not expired (expiry > block.timestamp)
  • Orders are for same series
  • Prices cross (bid price >= ask price)
  • Nonces match current user nonces
  • Post-trade: both parties pass margin health check

Effect:

  1. Validates order data
  2. Uses makerIsBid to determine maker/taker for fee calculation
  3. Calculates premium using ask tick and tick decimals
  4. Updates positions via PositionManager
  5. Collects fees (maker fee from maker, taker fee from taker)
  6. Performs post-trade margin checks

Emits: MatchSettled(seriesId, bidOrderId, askOrderId, tick, fillAmount), Trade(...)


View Functions

userOrderNonce

Returns a user's current order nonce (for order ID prediction).

function userOrderNonce(address user) external view returns (uint256)

owner

Returns the contract owner address.

function owner() external view returns (address)

makerFeeBps

Returns the maker fee rate in BPS (can be negative for rebates).

function makerFeeBps() external view returns (int256)

takerFeeBps

Returns the taker fee rate in BPS.

function takerFeeBps() external view returns (uint256)

feeRecipient

Returns the address that receives protocol fees.

function feeRecipient() external view returns (address)

collateralVault

Returns the collateral vault address.

function collateralVault() external view returns (address)

seriesRegistry

Returns the series registry contract.

function seriesRegistry() external view returns (IDiffusalOptionsSeriesRegistry)

orderFilled

Returns the filled amount for a specific order.

function orderFilled(bytes32 orderId) external view returns (uint128)

orderPortfolioId

Returns the portfolio ID for a specific order.

function orderPortfolioId(bytes32 orderId) external view returns (uint256)

pendingOwner

Returns the pending owner address (for two-step ownership transfer).

function pendingOwner() external view returns (address)

isOperator

Returns whether an address is an authorized operator.

function isOperator(address operator) external view returns (bool)

marginCalculator

Returns the margin calculator address.

function marginCalculator() external view returns (address)

Owner Functions

setFees

Configures maker and taker fees.

function setFees(int256 _makerFeeBps, uint256 _takerFeeBps) external

Constraint: takerFeeBps + makerFeeBps > 0 (net positive fees)

Emits: FeesUpdated


setFeeRecipient

Sets the address that receives protocol fees.

function setFeeRecipient(address _feeRecipient) external

Emits: FeeRecipientUpdated


setCollateralVault

Sets the collateral vault for margin checks.

function setCollateralVault(address _collateralVault) external

Emits: CollateralVaultUpdated


setMarginCalculator

Sets the margin calculator address (for health checks after trades).

function setMarginCalculator(address _marginCalculator) external

Emits: MarginCalculatorUpdated


transferOwnership

Initiates ownership transfer to a new address (two-step pattern).

function transferOwnership(address newOwner) external

Note: Setting newOwner to zero cancels any pending transfer.

Emits: OwnershipTransferStarted


acceptOwnership

Accepts pending ownership transfer (called by pending owner).

function acceptOwnership() external

Emits: OwnershipTransferred


setOperator

Sets operator authorization status.

function setOperator(address operator, bool authorized) external

Events

EventParametersDescription
OrderRegisteredseriesId, orderId, maker, isBuy, tick, size, expiry, timestamp, tickDecimals, portfolioId, nonceOrder registered (indexed by backend)
OrderCancelledorderId, maker, timestamp, structHashOrder cancelled (on-chain + indexed by backend)
MinOrderNonceIncrementedmaker, newMinNonceMass cancellation via nonce increment
MatchSettledseriesId, bidOrderId, askOrderId, matchTick, fillAmountMatch settled (indexed by backend)
TradeseriesId, buyer, seller, tick, size, premiumTrade executed
FeesCollectedseriesId, maker, taker, makerFeeAmount, takerFeeAmount, feeRecipientFees collected
FeesUpdatedmakerFeeBps, takerFeeBpsFee rates changed
FeeRecipientUpdatedoldRecipient, newRecipientFee recipient changed
CollateralVaultUpdatedoldVault, newVaultCollateral vault changed
MarginCalculatorUpdatedoldCalculator, newCalculatorMargin calculator changed
OwnershipTransferredpreviousOwner, newOwnerOwnership changed
OwnershipTransferStartedpreviousOwner, newOwnerOwnership transfer started (two-step pattern)
RebateCappedmaker, expectedRebate, actualRebateMaker rebate capped due to insufficient fee balance

Execution Flow

Register Order Sequence

┌─────────────────────────────────────────────────────────────────────────────┐
│ 1. User calls registerOrder(seriesId, portfolioId, isBuy, tick, size,       │
│    expiry)                                                                  │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 2. Validation                                                               │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ • Check size > 0, tick > 0                                              │ │
│ │ • Check expiry > block.timestamp                                        │ │
│ │ • Validate series is tradeable                                          │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 3. Get tick decimals                                                        │
│    Oracle.getTickDecimals(pairId) → dynamic based on spot price             │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 4. Margin simulation                                                        │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ • Compute hypothetical optionDelta and premiumDelta                     │ │
│ │ • MarginCalculator.simulatePostTradeHealth() → revert if unhealthy      │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 5. Generate orderId                                                         │
│    Hash(seriesId, maker, timestamp, nonce)                                  │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 6. Emit OrderRegistered event                                               │
│    Backend indexes and adds to off-chain order book                         │
└─────────────────────────────────────────────────────────────────────────────┘

Settle Match Sequence

┌─────────────────────────────────────────────────────────────────────────────┐
│ 1. Backend calls settleMatch(makerOrder, makerSignature, takerOrder,        │
│    takerSignature, fillAmount, makerIsBid)                                  │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 2. Validation                                                               │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ • Verify EIP-712 signatures for both orders                             │ │
│ │ • Check orders not expired                                              │ │
│ │ • Check same series                                                     │ │
│ │ • Check bid price >= ask price (prices cross)                           │ │
│ │ • Check nonces match current user nonces                                │ │
│ │ • Check fillAmount > 0                                                  │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 3. Determine maker/taker                                                    │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │                         makerIsBid?                                     │ │
│ │                             │                                           │ │
│ │              ┌──────────────┴──────────────┐                            │ │
│ │              ▼                             ▼                            │ │
│ │         [true]                        [false]                           │ │
│ │   bid maker is maker            ask maker is maker                      │ │
│ │   ask maker is taker            bid maker is taker                      │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 4. Calculate premium                                                        │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ • Use execution price from order (lower of bid/ask)                     │ │
│ │ • premium = (price × fillAmount) / 1e18                                 │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 5. Update positions                                                         │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ • Buyer: optionBalance += fillAmount, premiumBalance -= premium         │ │
│ │ • Seller: optionBalance -= fillAmount, premiumBalance += premium        │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 6. Collect fees                                                             │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ • Taker fee: taker → feeRecipient                                       │ │
│ │ • Maker fee: maker → feeRecipient (or reverse if rebate)                │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 7. Margin check (via MarginCalculator)                                      │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ • Check buyer isHealthy() / isPortfolioHealthy()                        │ │
│ │ • Check seller isHealthy() / isPortfolioHealthy()                       │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 8. Emit events                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Integration Points

Depends On

ContractPurpose
DiffusalOraclegetTickDecimals() for dynamic tick precision
DiffusalOptionsPositionManagerPosition updates
DiffusalOptionsSeriesRegistrySeries validation/creation
DiffusalCollateralVaultPremium transfers
DiffusalMarginCalculatorPost-trade margin health checks
USDC (ERC20)Collateral token

Used By

ComponentPurpose
Backend IndexerIndexes OrderRegistered, OrderCancelled, MatchSettled events
Backend Matching EngineFinds crossed orders, calls settleMatch()
DiffusalInsuranceFundFee recipient

Security Considerations

Reentrancy Protection

All state-changing operations use nonReentrant modifier to prevent callback attacks.

Access Control

FunctionAccess Level
registerOrder, registerOrderWithSeriesParamsPublic (any user, margin simulation enforced)
cancelOrderOrder maker only (verified via order.maker)
invalidateOrderPublic (anyone can invalidate unhealthy orders)
settleMatchAuthorized operators only (onlyOperator modifier)
Owner functionsOwner only

Margin Enforcement (Three-Layer Defense)

Layer 1 — Margin simulation at order time: When an order is registered, the contract simulates the post-trade portfolio state and checks that the maker would remain healthy (equity >= maintenance margin). This prevents obviously unfillable orders from appearing on the book.

// In registerOrder: simulate hypothetical trade
if (!marginCalculator.simulatePostTradeHealth(maker, portfolioId, seriesId, optionDelta, premiumDelta)) {
    revert Errors.InsufficientMargin();
}

Layer 2 — Order invalidation (keeper function): The invalidateOrder function allows anyone to cancel orders whose makers can no longer support them. This handles portfolio deterioration after order placement (market moves, other trades consuming margin, partial liquidations).

Layer 3 — Post-trade health check at settlement: The existing settlement flow checks both parties' margin health after position updates:

if (!marginCalculator.isHealthy(buyer)) revert Errors.InsufficientMargin();
if (!marginCalculator.isHealthy(seller)) revert Errors.InsufficientMargin();

Fee Invariant

The constraint takerFeeBps + makerFeeBps > 0 prevents protocol from paying out more in rebates than it collects:

if (int256(_takerFeeBps) + _makerFeeBps < 1) revert Errors.InvalidFeeConfiguration();

Order Data Validation

Since order data is provided by the backend in settleMatch(), the contract validates all order parameters including expiry and price crossing.


Code Reference

Source: packages/contracts/src/DiffusalOptionsOrderBook.sol

Interface: packages/contracts/src/interfaces/IDiffusalOptionsOrderBook.sol

Order Types: packages/contracts/src/utils/OptionsOrderTypes.sol

Tick to WAD Conversion

function tickToWad(uint32 tick, uint8 tickDecimals) internal pure returns (uint256 priceWad) {
    return uint256(tick) * 10 ** (18 - tickDecimals);
}

On this page