Diffusal

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):

StepDescription
1Series expires (block.timestamp ≥ expiry)
2Series is settled with TWAP price
3Settlement engine processes individual positions
4Net settlement = (intrinsic × optionBalance) + premiumBalance

Four-Instrument Model

The settlement engine uses the Four-Instrument Model where each position has two components:

ComponentSignMeaning
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 TypeITM When
CallSpot > Strike
PutStrike > 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

Net Settlement=(Intrinsic Value×optionBalance)+premiumBalance\text{Net Settlement} = (\text{Intrinsic Value} \times \text{optionBalance}) + \text{premiumBalance}

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)
ParameterTypeDescription
seriesIdbytes32Option series identifier
optionBalanceint256Option balance (+ = long, - = short)
premiumBalanceint256Premium 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:

  1. Check if already settled → return early if yes
  2. Get optionBalance and premiumBalance from PositionManager
  3. If both balances = 0 → mark settled, return
  4. Verify series is settled
  5. Calculate intrinsic value and Four-Instrument settlement
  6. Credit (positive settlement) / debit (negative settlement) via Vault
  7. Zero out BOTH optionBalance and premiumBalance
  8. 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().

ScenarioAction
Payer collection covers all receiversNo insurance needed
Partial shortfall, insurance has fundsInsurance covers shortfall
Shortfall > insurance balancePartial coverage, receivers prorated
No insurance fund configuredReceivers 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 onlyOwner

Emits: 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 onlyOwner

transferOwnership / 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 pendingOwner

Step 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

EventParametersDescription
PositionSettleduser, seriesId, portfolioId, optionBalance, premiumBalance, intrinsicValue, settlementAmountIndividual position settled
OperatorUpdatedoperator, authorizedOperator status changed
OwnershipTransferredpreviousOwner, newOwnerOwnership changed
OwnershipTransferStartedpreviousOwner, newOwnerTwo-step ownership transfer initiated
PositionManagerUpdatedoldManager, newManagerPosition manager address changed
SeriesRegistryUpdatedoldRegistry, newRegistrySeries registry address changed
CollateralVaultUpdatedoldVault, newVaultCollateral vault address changed
InsuranceFundUpdatedoldFund, newFundInsurance 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) = $4850

Result: 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 = $100

Result: 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 = -$1800

Result: 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 = $500

Result: User receives $500 credit.


Integration Points

Depends On

ContractPurpose
DiffusalOptionsPositionManagerPosition queries and zeroing
DiffusalOptionsSeriesRegistrySettlement price lookup
DiffusalCollateralVaultCredit/debit operations
DiffusalInsuranceFundShortfall coverage for batch settlement

Used By

ContractPurpose
Keepers/AdminTrigger 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 conversion

Protocol Documentation

Contract Documentation

On this page