DiffusalSettlementEngine
Handles Four-Instrument Model settlement of expired options
The DiffusalSettlementEngine contract settles expired option positions using the Four-Instrument Model. Settlement combines option intrinsic value with premium balances to calculate the net settlement for each user. This is a high-risk contract as it handles the final distribution of user funds at option expiry.
Overview
Settlement occurs after an option series expires and has been settled (via SeriesRegistry.settle):
| Step | Description |
|---|---|
| 1 | Series expires (block.timestamp ≥ expiry) |
| 2 | Series is settled with TWAP price |
| 3 | Settlement engine processes individual positions |
| 4 | Net settlement = (intrinsic × optionBalance) + premiumBalance |
Four-Instrument Model
The settlement engine uses the Four-Instrument Model where each position has two components:
| Component | Sign | Meaning |
|---|---|---|
optionBalance | + (long) | Receives intrinsic value |
optionBalance | - (short) | Pays intrinsic value |
premiumBalance | + (receivable) | Receives premium |
premiumBalance | - (payable) | Pays premium |
Intrinsic Value
At expiry, options have only intrinsic value (no time value). See Options Settlement: Intrinsic Value for the complete formulas.
| Option Type | ITM When |
|---|---|
| Call | Spot > Strike |
| Put | Strike > Spot |
Key Concepts
Settlement Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ 1. Verify series is settled (isSettled = true) │
└─────────────────────────────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 2. Get user's optionBalance and premiumBalance from PositionManager │
└─────────────────────────────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 3. Calculate intrinsic value │
└─────────────────────────────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 4. Calculate Four-Instrument settlement: │
│ netSettlement = (intrinsic x optionBalance) + premiumBalance │
└─────────────────────────────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 5. Process collateral via Vault │
└─────────────────────┬───────────────────────────────────┬───────────────────┘
│ │
▼ ▼
┌───────────────────────────────────┐ ┌───────────────────────────────────┐
│ Positive settlement: │ │ Negative settlement: │
│ vault.creditCollateral(user, amt) │ │ vault.debitCollateral(user, amt) │
└─────────────────┬─────────────────┘ └─────────────────┬─────────────────┘
│ │
└───────────────────┬───────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 6. Zero out BOTH optionBalance and premiumBalance via PositionManager │
└─────────────────────────────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 7. Mark position as settled (prevents double-settlement) │
└─────────────────────────────────────────────────────────────────────────────┘Four-Instrument Settlement Formula
Option Settlement:
- Long (optionBalance > 0): RECEIVES intrinsic × size (always ≥ 0)
- Short (optionBalance < 0): PAYS intrinsic × |size| (always ≤ 0)
Premium Settlement:
- Receiver (premiumBalance > 0): RECEIVES fixed amount
- Payer (premiumBalance < 0): PAYS fixed amount
These directions are deterministic and never reverse.
Double-Settlement Prevention
Each user-series-portfolio combination can only be settled once:
mapping(address => mapping(uint256 => mapping(bytes32 => bool))) settledPortfolioPositions;Storage & State
The contract uses ERC-7201 namespaced storage for upgradeability:
/// @custom:storage-location erc7201:diffusal.storage.SettlementEngine
struct SettlementEngineStorage {
address owner;
address pendingOwner;
mapping(address => bool) operators;
address positionManager;
address seriesRegistry;
address collateralVault;
address insuranceFund;
mapping(address => mapping(bytes32 => bool)) settledPositions;
mapping(address => mapping(uint256 => mapping(bytes32 => bool))) settledPortfolioPositions;
}SettlementResult Struct
struct SettlementResult {
address user; // User whose position was settled
bool settled; // Whether settlement was successful
bytes32 seriesId; // Option series
uint256 portfolioId; // Portfolio ID (0 for legacy/default)
int256 optionBalance; // Original option balance (+ = long, - = short)
int256 premiumBalance; // Premium balance (+ = receivable, - = payable)
uint256 intrinsicValue; // Intrinsic value per contract (WAD)
int256 settlementAmount; // Net settlement = (intrinsic × optionBalance) + premiumBalance (USDC, 6 decimals)
}View Functions
calculateIntrinsicValue
Calculates the intrinsic value per contract.
function calculateIntrinsicValue(bytes32 seriesId) public view returns (uint256 intrinsicValue)Returns: Intrinsic value in WAD (1e18).
Reverts: OptionNotSettled if series hasn't been settled.
Logic:
if (isCall) {
// Call: max(0, settlementPrice - strike)
if (settlementPrice > strike) {
intrinsicValue = settlementPrice - strike;
}
} else {
// Put: max(0, strike - settlementPrice)
if (strike > settlementPrice) {
intrinsicValue = strike - settlementPrice;
}
}insuranceFund
Returns the insurance fund address.
function insuranceFund() external view returns (address)calculateSettlementAmount
Calculates the Four-Instrument settlement amount for a position.
function calculateSettlementAmount(
bytes32 seriesId,
int256 optionBalance,
int256 premiumBalance
) public view returns (int256 settlementAmount)| Parameter | Type | Description |
|---|---|---|
seriesId | bytes32 | Option series identifier |
optionBalance | int256 | Option balance (+ = long, - = short) |
premiumBalance | int256 | Premium balance (+ = receivable, - = payable) |
Returns: Net settlement amount in USDC (6 decimals), signed:
- Positive = user receives (long ITM or premium receivable)
- Negative = user pays (short ITM or premium payable)
Formula: netSettlement = (intrinsicValue × optionBalance) + premiumBalance
isPortfolioPositionSettled
Checks if a portfolio position has been settled.
function isPortfolioPositionSettled(address user, uint256 portfolioId, bytes32 seriesId)
external view returns (bool)Settlement Functions
These functions require operator authorization.
Portfolio-Aware Only
All settlement functions are portfolio-aware. You must always specify a
portfolioId.
settlePortfolioPosition
Settles a user's position in a specific portfolio.
function settlePortfolioPosition(address user, uint256 portfolioId, bytes32 seriesId)
external onlyOperator returns (SettlementResult memory result)Requirements:
- Series must be settled (via SeriesRegistry)
- Position not already settled
Process:
- Check if already settled → return early if yes
- Get optionBalance and premiumBalance from PositionManager
- If both balances = 0 → mark settled, return
- Verify series is settled
- Calculate intrinsic value and Four-Instrument settlement
- Credit (positive settlement) / debit (negative settlement) via Vault
- Zero out BOTH optionBalance and premiumBalance
- Mark as settled
Emits: PositionSettled
settlePortfolioPositionBatch
Settles multiple portfolio positions for a single series.
function settlePortfolioPositionBatch(
address[] calldata users,
uint256[] calldata portfolioIds,
bytes32 seriesId
) external onlyOperator returns (SettlementResult[] memory results)Note: Each position is settled individually with per-position insurance coverage.
Emits: PositionSettled for each position.
Batch Settlement Flow
The batch settlement process ensures fair distribution even when some payers can't cover their full obligation:
┌─────────────────────────────────────────────────────────────────────────────┐
│ 1. Calculate Four-Instrument Settlements │
│ │
│ For each user: │
│ netSettlement = (intrinsic x optionBalance) + premiumBalance │
│ │
│ totalReceiversEntitlement = sum(positive settlements) │
│ totalPayersObligation = sum(|negative settlements|) │
└─────────────────────────────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 2. Collect from Payers (negative settlement) │
│ │
│ For each payer: │
│ actualCollection = min(obligation, userDeposit) │
│ │ │
│ ▼ │
│ Accumulate into payout pool │
└─────────────────────────────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 3. Cover Shortfall from Insurance │
│ │
│ shortfall = totalReceiversEntitlement - actualCollection │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ shortfall > 0?│ │
│ └───────┬───────┘ │
│ │ Yes │
│ ┌─────────────┴─────────────┐ │
│ ▼ ▼ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │shortfall <= balance │ │shortfall > balance │ │
│ │ cover full shortfall│ │cover what's available│ │
│ └─────────────────────┘ └─────────────────────┘ │
└─────────────────────────────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 4. Pay Receivers (positive settlement) │
│ │
│ payoutPool = actualCollection + insuranceCoverage │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ payoutPool < totalReceiversEntitlement? │ │
│ └──────────────────┬───────────────────┘ │
│ │ │
│ ┌───────────────────┴───────────────────┐ │
│ │ Partial coverage │ │
│ ▼ │ Full coverage │
│ actualPayout = expectedPayout x │ │
│ (payoutPool / totalReceiversEntitlement) │ │
│ │ │ │
│ └───────────────────┬────────────────────┘ │
│ ▼ │
│ Distribute rounding dust to last receiver │
└─────────────────────────────────────────────────────────────────────────────┘Implementation Details
Step 1: Calculate settlements (_calculateBatchSettlements)
for (uint256 i = 0; i < users.length; ++i) {
// Four-Instrument settlement
int256 settlementAmount = calculateSettlementAmount(
seriesId,
posInfo.optionBalance,
posInfo.premiumBalance
);
if (settlementAmount > 0) {
totalReceiversEntitlement += settlementAmount;
} else if (settlementAmount < 0) {
totalPayersObligation += abs(settlementAmount);
}
}Step 2: Collect from payers (_collectFromPayers)
for (uint256 i = 0; i < results.length; ++i) {
if (results[i].settlementAmount < 0) {
uint256 obligation = abs(results[i].settlementAmount);
// Vault caps at available deposit
actualCollection += vault.debitCollateral(user, obligation);
// Zero out BOTH optionBalance and premiumBalance
pm.updatePosition(user, seriesId, -optionBalance, -premiumBalance);
}
}Step 3: Cover shortfall (_coverShortfallFromInsurance)
if (actualCollection < totalReceiversEntitlement && insuranceFund != address(0)) {
uint256 shortfall = totalReceiversEntitlement - actualCollection;
uint256 coverage = min(shortfall, insuranceFund.getBalance());
if (coverage > 0) {
// Each user's shortfall covered individually
insuranceFund.cover(coverage, CoverageSource.SETTLEMENT, user);
payoutPool += coverage;
}
}Step 4: Pay receivers (_payReceivers)
for (uint256 i = 0; i < results.length; ++i) {
if (results[i].settlementAmount > 0) {
uint256 expectedPayout = results[i].settlementAmount;
// Prorate if insufficient funds
uint256 actualPayout = (payoutPool < totalReceiversEntitlement)
? (expectedPayout * payoutPool) / totalReceiversEntitlement
: expectedPayout;
vault.creditCollateral(user, actualPayout);
// Zero out BOTH optionBalance and premiumBalance
pm.updatePosition(user, seriesId, -optionBalance, -premiumBalance);
}
}Insurance Fund Integration
The settlement engine is authorized to call InsuranceFund.cover() for shortfall coverage. The insurance fund must be configured via setInsuranceFund().
| Scenario | Action |
|---|---|
| Payer collection covers all receivers | No insurance needed |
| Partial shortfall, insurance has funds | Insurance covers shortfall |
| Shortfall > insurance balance | Partial coverage, receivers prorated |
| No insurance fund configured | Receivers prorated to payer collection |
See DiffusalInsuranceFund for details on the coverage mechanism.
Owner Functions
setOperator
Authorizes or deauthorizes an operator.
function setOperator(address operator, bool authorized) external onlyOwnerEmits: OperatorUpdated
setPositionManager / setSeriesRegistry / setCollateralVault / setInsuranceFund
Updates contract references.
function setPositionManager(address positionManager_) external onlyOwner
function setSeriesRegistry(address seriesRegistry_) external onlyOwner
function setCollateralVault(address collateralVault_) external onlyOwner
function setInsuranceFund(address insuranceFund_) external onlyOwnertransferOwnership / acceptOwnership
Two-step ownership transfer pattern for security:
// Step 1: Owner initiates transfer
function transferOwnership(address newOwner) external onlyOwner
// Step 2: New owner accepts
function acceptOwnership() external // Only callable by pendingOwnerStep 1: Current owner calls transferOwnership(newOwner). Sets pendingOwner and emits OwnershipTransferStarted.
Step 2: New owner calls acceptOwnership(). Transfers ownership and emits OwnershipTransferred.
Cancel: Call transferOwnership(address(0)) to cancel pending transfer.
Events
| Event | Parameters | Description |
|---|---|---|
PositionSettled | user, seriesId, portfolioId, optionBalance, premiumBalance, intrinsicValue, settlementAmount | Individual position settled |
OperatorUpdated | operator, authorized | Operator status changed |
OwnershipTransferred | previousOwner, newOwner | Ownership changed |
OwnershipTransferStarted | previousOwner, newOwner | Two-step ownership transfer initiated |
PositionManagerUpdated | oldManager, newManager | Position manager address changed |
SeriesRegistryUpdated | oldRegistry, newRegistry | Series registry address changed |
CollateralVaultUpdated | oldVault, newVault | Collateral vault address changed |
InsuranceFundUpdated | oldFund, newFund | Insurance fund address changed |
Settlement Examples
Example 1: ITM Call (Long with Premium Payable)
Setup:
- ETH-USDC Call, Strike = $3000
- Settlement price = $3500
- User: optionBalance = +10 (long), premiumBalance = -150e18 (paid $150 premium)
Calculation:
Intrinsic value = max(0, 3500 - 3000) = $500 per contract
Option settlement = $500 × 10 = $5000
Premium settlement = -$150 (already owed)
Net settlement = $5000 + (-$150) = $4850Result: User receives $4850 credit.
Example 2: OTM Put (Short with Premium Receivable)
Setup:
- ETH-USDC Put, Strike = $2800
- Settlement price = $3000
- User: optionBalance = -5 (short), premiumBalance = +100e18 (owed $100 premium)
Calculation:
Intrinsic value = max(0, 2800 - 3000) = $0 per contract
Option settlement = $0 × -5 = $0
Premium settlement = +$100 (owed to user)
Net settlement = $0 + $100 = $100Result: User receives $100 credit (premium owed to them).
Example 3: ITM Put (Short with Premium Receivable)
Setup:
- ETH-USDC Put, Strike = $3200
- Settlement price = $3000
- User: optionBalance = -10 (short), premiumBalance = +200e18 (owed $200 premium)
Calculation:
Intrinsic value = max(0, 3200 - 3000) = $200 per contract
Option settlement = $200 × -10 = -$2000 (short pays)
Premium settlement = +$200 (owed to user)
Net settlement = -$2000 + $200 = -$1800Result: User's deposit debited by $1800.
Example 4: Pure Premium Position (No Options)
Setup:
- User: optionBalance = 0, premiumBalance = +500e18 (owed $500 premium)
Calculation:
Option settlement = $0 (no option position)
Premium settlement = +$500
Net settlement = $500Result: User receives $500 credit.
Integration Points
Depends On
| Contract | Purpose |
|---|---|
| DiffusalOptionsPositionManager | Position queries and zeroing |
| DiffusalOptionsSeriesRegistry | Settlement price lookup |
| DiffusalCollateralVault | Credit/debit operations |
| DiffusalInsuranceFund | Shortfall coverage for batch settlement |
Used By
| Contract | Purpose |
|---|---|
| Keepers/Admin | Trigger settlement after expiry |
Security Considerations
Operator-Only Access
Settlement can only be triggered by authorized operators:
modifier onlyOperator() {
if (!_operators[msg.sender]) revert Errors.NotOperator();
_;
}Double-Settlement Prevention
Each position can only be settled once:
if (settledPortfolioPositions[user][portfolioId][seriesId]) {
return result; // Already settled
}
// ... process settlement ...
settledPortfolioPositions[user][portfolioId][seriesId] = true;Series Settlement Verification
Positions can only be settled after the series itself is settled:
if (!sr.isSettled(seriesId)) revert Errors.OptionNotSettled();Debit Capping
When debiting shorts, the vault caps at available deposit:
uint256 actualDebit = amount > deposits[user] ? deposits[user] : amount;This prevents underflow but may leave protocol with bad debt (covered by insurance fund).
Code Reference
Source: packages/contracts/src/DiffusalSettlementEngine.sol
Interface: packages/contracts/src/interfaces/IDiffusalSettlementEngine.sol
Key Constants
// From Constants.sol
uint256 constant WAD = 1e18;
uint256 constant WAD_TO_USDC = 1e12; // WAD to USDC conversionRelated
Protocol Documentation
- Options Settlement (Protocol) — High-level settlement mechanics, TWAP pricing, keeper operations
- Liquidation (Protocol) — Settlement readiness liquidation (why users need cash at expiry)
Contract Documentation
- DiffusalOptionsSeriesRegistry — Series settlement with TWAP,
settle()function - DiffusalPriceHistory — Rolling price snapshots, TWAP calculation source
- DiffusalCollateralVault —
creditCollateral()anddebitCollateral()functions - DiffusalInsuranceFund —
cover()function for shortfall coverage - DiffusalLiquidationEngine — Settlement readiness liquidation (related pre-expiry mechanism)