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:
| Component | Location | Description |
|---|---|---|
| Order registration | On-chain | registerOrder() emits event (no on-chain storage) |
| Order storage | Off-chain | Backend indexes events, maintains order book |
| Order matching | Off-chain | Backend matching engine finds crossed orders |
| Settlement | On-chain | settleMatch() 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.
| Spot Price Range | Tick Decimals | Tick Size Example |
|---|---|---|
| >= $100 | 2 | 0.01 |
| >= $10 | 3 | 0.001 |
| >= $1 | 4 | 0.0001 |
| >= $0.1 | 5 | 0.00001 |
| >= $0.01 | 6 | 0.000001 |
| < $0.01 | 7-12 | Finer precision |
Order Types
| Order Direction | isBuy | Buyer Gets | Seller Gets | Premium Flow |
|---|---|---|---|---|
| Bid | true | Long (+size) | Short (-size) | Buyer → Seller |
| Ask | false | Long (+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 Type | Storage Type | Can Be Negative? | Description |
|---|---|---|---|
makerFeeBps | int256 | Yes (rebates) | Fee charged to order creator (negative = rebate) |
takerFeeBps | uint256 | No | Fee 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 decimalsERC-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)| Parameter | Type | Description |
|---|---|---|
seriesId | bytes32 | Option series identifier |
portfolioId | uint256 | Portfolio ID (0 = default portfolio) |
isBuy | bool | true for bid, false for ask |
tick | uint32 | Price in ticks |
size | uint128 | Number of contracts (WAD) |
expiry | uint32 | Order validity deadline (unix timestamp) |
Returns: orderId — Unique identifier for the registered order.
Validation checks:
size > 0tick > 0expiry > block.timestamp- Series is tradeable (not expired, not settled)
- Margin simulation passes (portfolio remains healthy after hypothetical trade)
Effect:
- Validates input parameters
- Fetches
tickDecimalsfrom Oracle based on current spot price - Simulates post-trade margin health via
MarginCalculator.simulatePostTradeHealth() - Generates unique orderId from series, maker, timestamp, and nonce
- Increments user's order nonce
- Emits
OrderRegisteredevent
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| Parameter | Type | Description |
|---|---|---|
order | OrderTypes.Order | The order struct to cancel |
orderId | bytes32 | The 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:
- Computes the struct hash of the order
- Verifies the caller is the order maker
- Marks the order as cancelled on-chain (
cancelledOrders[structHash] = true) - Emits
OrderCancelledevent 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| Parameter | Type | Description |
|---|---|---|
order | OrderTypes.Order | The order struct to invalidate |
orderId | bytes32 | The 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:
- Re-runs the margin simulation for the order maker's current portfolio state
- If the portfolio would be unhealthy after the hypothetical trade: marks the order as cancelled and emits
OrderCancelled - 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
) externalOrder 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
}| Parameter | Type | Description |
|---|---|---|
makerOrder | OrderTypes.Order | The maker order struct |
makerSignature | bytes | EIP-712 signature from maker |
takerOrder | OrderTypes.Order | The taker order struct |
takerSignature | bytes | EIP-712 signature from taker |
fillAmount | uint128 | Amount of contracts to fill |
makerIsBid | bool | true 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:
- Validates order data
- Uses
makerIsBidto determine maker/taker for fee calculation - Calculates premium using ask tick and tick decimals
- Updates positions via PositionManager
- Collects fees (maker fee from maker, taker fee from taker)
- 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) externalConstraint: takerFeeBps + makerFeeBps > 0 (net positive fees)
Emits: FeesUpdated
setFeeRecipient
Sets the address that receives protocol fees.
function setFeeRecipient(address _feeRecipient) externalEmits: FeeRecipientUpdated
setCollateralVault
Sets the collateral vault for margin checks.
function setCollateralVault(address _collateralVault) externalEmits: CollateralVaultUpdated
setMarginCalculator
Sets the margin calculator address (for health checks after trades).
function setMarginCalculator(address _marginCalculator) externalEmits: MarginCalculatorUpdated
transferOwnership
Initiates ownership transfer to a new address (two-step pattern).
function transferOwnership(address newOwner) externalNote: Setting newOwner to zero cancels any pending transfer.
Emits: OwnershipTransferStarted
acceptOwnership
Accepts pending ownership transfer (called by pending owner).
function acceptOwnership() externalEmits: OwnershipTransferred
setOperator
Sets operator authorization status.
function setOperator(address operator, bool authorized) externalEvents
| Event | Parameters | Description |
|---|---|---|
OrderRegistered | seriesId, orderId, maker, isBuy, tick, size, expiry, timestamp, tickDecimals, portfolioId, nonce | Order registered (indexed by backend) |
OrderCancelled | orderId, maker, timestamp, structHash | Order cancelled (on-chain + indexed by backend) |
MinOrderNonceIncremented | maker, newMinNonce | Mass cancellation via nonce increment |
MatchSettled | seriesId, bidOrderId, askOrderId, matchTick, fillAmount | Match settled (indexed by backend) |
Trade | seriesId, buyer, seller, tick, size, premium | Trade executed |
FeesCollected | seriesId, maker, taker, makerFeeAmount, takerFeeAmount, feeRecipient | Fees collected |
FeesUpdated | makerFeeBps, takerFeeBps | Fee rates changed |
FeeRecipientUpdated | oldRecipient, newRecipient | Fee recipient changed |
CollateralVaultUpdated | oldVault, newVault | Collateral vault changed |
MarginCalculatorUpdated | oldCalculator, newCalculator | Margin calculator changed |
OwnershipTransferred | previousOwner, newOwner | Ownership changed |
OwnershipTransferStarted | previousOwner, newOwner | Ownership transfer started (two-step pattern) |
RebateCapped | maker, expectedRebate, actualRebate | Maker 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
| Contract | Purpose |
|---|---|
| DiffusalOracle | getTickDecimals() for dynamic tick precision |
| DiffusalOptionsPositionManager | Position updates |
| DiffusalOptionsSeriesRegistry | Series validation/creation |
| DiffusalCollateralVault | Premium transfers |
| DiffusalMarginCalculator | Post-trade margin health checks |
| USDC (ERC20) | Collateral token |
Used By
| Component | Purpose |
|---|---|
| Backend Indexer | Indexes OrderRegistered, OrderCancelled, MatchSettled events |
| Backend Matching Engine | Finds crossed orders, calls settleMatch() |
| DiffusalInsuranceFund | Fee recipient |
Security Considerations
Reentrancy Protection
All state-changing operations use nonReentrant modifier to prevent callback attacks.
Access Control
| Function | Access Level |
|---|---|
registerOrder, registerOrderWithSeriesParams | Public (any user, margin simulation enforced) |
cancelOrder | Order maker only (verified via order.maker) |
invalidateOrder | Public (anyone can invalidate unhealthy orders) |
settleMatch | Authorized operators only (onlyOperator modifier) |
| Owner functions | Owner 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);
}Related
- DiffusalOracle — Dynamic tick decimals source
- DiffusalOptionsRFQ — Alternative trading via MMM quotes
- DiffusalCollateralVault — Margin enforcement
- Order Book (Protocol) — High-level order book mechanics