Diffusal

DiffusalLiquidationEngine

Handles liquidation of undercollateralized users

The DiffusalLiquidationEngine contract liquidates users whose equity falls below the maintenance margin threshold. It closes positions at penalized prices, pays a bounty to liquidators, and draws from the insurance fund to cover shortfalls. This is a high-risk contract with significant impact on user funds.

Note: For settlement readiness liquidation (when users lack cash for expiring obligations), see DiffusalSettlementReadinessLiquidator.

Architecture Note: For contract size optimization, complex calculation logic and settlement logic have been extracted to separate contracts:

The engine retains basic view functions like getLiquidationInfo that call into the calculator.


Overview

Liquidation is triggered when a user's equity falls below their maintenance margin:

Liquidatable=Equity<Maintenance Margin\text{Liquidatable} = \text{Equity} < \text{Maintenance Margin}

Important: Users with long-only option positions have bounded risk. Their maximum loss is determined by their Premium Payer obligation (a separate instrument from the Option Long). With sufficient cash to cover their premium obligations (deposit ≥ premium paid), they can never be liquidated. However, if their deposit is less than the premium paid, they can still be liquidated. See Margin System: Example D - Unliquidatable Long Position for details.

Liquidation Process

┌─────────────────────────────────────────────────────────────────────────────┐
│ 1. Verify user is liquidatable (equity < MM, not MMM)                       │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 2. Calculate debt (IM - equity)                                             │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 3. Close positions at penalized mark prices                                 │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 4. Calculate liquidator bounty (5% of debt, capped at proceeds)             │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 5. Pay bounty to liquidator first (always)                                  │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 6. Apply remaining proceeds to debt                                         │
└─────────────────────────────────────────────────────────────────────────────┘


                 ┌────────────────────────────────────────┐
                 │   Proceeds after bounty >= debt?       │
                 └────────────────────────────────────────┘

           ┌──────────────────┴──────────────────┐
           ▼                                     ▼
┌─────────────────────────┐         ┌─────────────────────────┐
│ [Yes]                   │         │ [No - shortfall]        │
│ 7. Credit surplus to    │         │ 8. Draw from insurance  │
│    user                 │         │    fund                 │
└─────────────────────────┘         └─────────────────────────┘

Partial vs Full Liquidation

TypePositions ClosedUse Case
FullAll positionsUser is deeply underwater
PartialProportional (debt/IM fraction)Often restores health without full wipeout

Proportional Liquidation: Partial liquidation closes a fraction of positions equal to debt / initialMargin, ensuring just enough positions are liquidated to cover the debt. If partial liquidation doesn't restore health, the engine automatically escalates to full liquidation.


Key Concepts

Debt Calculation

Debt represents how much the user is underwater:

Debt=max(0,Initial MarginEquity)\text{Debt} = \max(0, \text{Initial Margin} - \text{Equity})

Penalty Rate

Positions are closed at penalized prices to discourage risky behavior and compensate the system. The penalty scales with implied volatility. See Liquidation: Volatility-Adjusted Penalty for the complete formula and rate table.

Liquidation Prices

Position TypeLiquidation Price
LongMark × (1 - penalty) — sold at discount
ShortMark × (1 + penalty) — bought back at premium

Liquidation Penalty Cap

The penalty rate is capped at a maximum to ensure liquidations remain viable even in extreme IV conditions:

Penalty Rate=min(Base+IV50%100,100%)\text{Penalty Rate} = \min\left(\text{Base} + \frac{\text{IV} - 50\%}{100}, 100\%\right)

The cap at 100% ensures that liquidators never need to pay more than the asset's value to take on positions.

Liquidator Bounty

Bounty=min(Debt×5%,Total Proceeds)\text{Bounty} = \min(\text{Debt} \times 5\%, \text{Total Proceeds})

Note: Bounty is always paid first to incentivize liquidators, even in shortfall cases. The bounty is capped at total proceeds to ensure we don't pay more than available. Remaining proceeds are applied to debt, and insurance covers any shortfall.


Storage & State

The contract uses ERC-7201 namespaced storage for upgradeability:

/// @custom:storage-location erc7201:diffusal.storage.LiquidationEngine
struct LiquidationEngineStorage {
    address owner;
    address pendingOwner;                        // Two-step ownership transfer
    mapping(address => bool) operators;
    address collateralVault;
    address positionManager;
    address seriesRegistry;
    address quoter;
    address oracle;
    address insuranceFund;
    address settlementContract;                  // Extracted settlement logic
    address viewContract;                        // Extracted view functions (calculator)
    address marginCalculator;                    // Margin calculations
    uint256 partialLiquidationRatio;             // Default 50% (WAD)
    uint256 liquidatorBountyRate;                // Default 5% (WAD)
    mapping(address => bool) approvedLiquidators; // Whitelisted liquidators
}

LiquidationInfo Struct

struct LiquidationInfo {
    bool isLiquidatable;         // Can be liquidated
    uint256 portfolioId;         // The portfolio ID
    uint256 debt;                // IM - equity (USDC, 6 decimals)
    uint256 estimatedLongsCost;  // Cost for liquidator to buy longs (USDC)
    uint256 estimatedShortsCost; // Compensation liquidator receives for shorts (USDC)
    uint256 estimatedBounty;     // Expected liquidator reward
    uint256 positionCount;       // Number of positions
}

LiquidationResult Struct

struct LiquidationResult {
    address user;                  // User liquidated
    address liquidator;            // Who triggered liquidation
    uint256 portfolioId;           // The portfolio that was liquidated
    uint256 debt;                  // Amount underwater (USDC, 6 decimals)
    uint256 longsCost;             // Amount liquidator paid for longs (USDC)
    uint256 shortsCost;            // Amount user paid liquidator for shorts (USDC)
    uint256 bounty;                // Bounty paid to liquidator
    uint256 insuranceUsed;         // Amount drawn from insurance
    int256 newUserEquity;          // User's equity after liquidation
    int256 newLiquidatorEquity;    // Liquidator's equity after acquiring positions
    uint256 positionsLiquidated;   // Positions transferred
    bool isPartial;                // Partial or full liquidation
}

View Functions

calculatePortfolioDebt

Returns the user's portfolio debt (how much underwater).

function calculatePortfolioDebt(address user, uint256 portfolioId) public view returns (uint256 debt)
Debt=max(0,IMEquity)\text{Debt} = \max(0, \text{IM} - \text{Equity})

calculatePenaltyRate

Returns the penalty rate for a trading pair.

function calculatePenaltyRate(bytes32 pairId) public view returns (uint256 penaltyRate)

Returns: Penalty rate in WAD (e.g., 0.01e18 = 1%).


isApprovedLiquidator

Checks if an address is an approved liquidator.

function isApprovedLiquidator(address liquidator) external view returns (bool)

Returns: true if the address is approved to execute liquidations.


getLiquidationInfo

Returns comprehensive liquidation information.

function getLiquidationInfo(address user) external view returns (LiquidationInfo memory info)

Useful for:

  • Checking if a user is liquidatable
  • Estimating proceeds and bounty before execution
  • Monitoring user health

getInsuranceFundBalance

Returns the current insurance fund balance.

function getInsuranceFundBalance() external view returns (uint256)

Liquidation Functions

These functions require approved liquidators—only addresses approved by the owner can execute liquidations.

liquidatePortfolio

Liquidates an undercollateralized user's portfolio. Tries partial liquidation first, then escalates to full if needed.

function liquidatePortfolio(
    address user,
    uint256 portfolioId,
    uint256 liquidatorPortfolioId
) external nonReentrant returns (LiquidationResult memory result)

Parameters:

  • user — The user whose portfolio to liquidate
  • portfolioId — The portfolio ID to liquidate
  • liquidatorPortfolioId — The liquidator's portfolio to receive the acquired positions

Requirements:

  • Caller must be an approved liquidator
  • Portfolio must be liquidatable
  • User must not be an MMM
  • Caller cannot liquidate themselves (prevents self-liquidation)

Behavior:

  1. Calculates the proportion of positions to close: debt / initialMargin
  2. Closes that fraction of positions (minimum 1 position), transferring to liquidator's specified portfolio
  3. Re-checks if user is healthy after partial liquidation
  4. If still unhealthy, automatically liquidates remaining positions

Emits: PortfolioLiquidated, PositionLiquidated (per position)


Owner Functions

setApprovedLiquidator

Approves or revokes an address to execute liquidations.

function setApprovedLiquidator(address liquidator, bool approved) external onlyOwner

Emits: ApprovedLiquidatorUpdated


setOperator

Authorizes or deauthorizes an operator.

function setOperator(address operator, bool authorized) external onlyOwner

setPartialLiquidationRatio

Sets the ratio for partial liquidations.

function setPartialLiquidationRatio(uint256 ratio) external onlyOwner

Constraints: 0 < ratio <= WAD (0-100%)

Emits: PartialLiquidationRatioUpdated


setLiquidatorBountyRate

Sets the bounty rate for liquidators.

function setLiquidatorBountyRate(uint256 rate) external onlyOwner

Constraints: 0 <= rate <= 0.1e18 (0-10%)

Emits: LiquidatorBountyRateUpdated


setInsuranceFund

Sets the insurance fund address.

function setInsuranceFund(address insuranceFund_) external onlyOwner

Contract Reference Setters

function setCollateralVault(address vault_) external onlyOwner
function setPositionManager(address manager_) external onlyOwner
function setSeriesRegistry(address registry_) external onlyOwner
function setQuoter(address quoter_) external onlyOwner
function setOracle(address oracle_) external onlyOwner

transferOwnership

Transfers contract ownership.

function transferOwnership(address newOwner) external onlyOwner

Events

EventParametersDescription
PortfolioLiquidateduser, liquidator, portfolioId, debt, longsCost, shortsCost, bounty, isPartialLiquidation completed
PositionLiquidateduser, liquidator, seriesId, positionSize, markPrice, liquidationPrice, costPosition transferred to liquidator
ApprovedLiquidatorUpdatedliquidator, approvedApproved liquidator status changed
InsuranceFundUpdatedoldBalance, newBalanceInsurance fund balance changed
PartialLiquidationRatioUpdatedoldRatio, newRatioPartial ratio changed
LiquidatorBountyRateUpdatedoldRate, newRateBounty rate changed
OperatorUpdatedoperator, authorizedOperator status changed
OwnershipTransferredpreviousOwner, newOwnerOwnership changed
CollateralVaultUpdatedoldVault, newVaultCollateral vault address changed
PositionManagerUpdatedoldManager, newManagerPosition manager address changed
SeriesRegistryUpdatedoldRegistry, newRegistrySeries registry address changed
QuoterUpdatedoldQuoter, newQuoterQuoter address changed
OracleUpdatedoldOracle, newOracleOracle address changed
InsuranceFundAddressUpdatedoldFund, newFundInsurance fund address changed

Liquidation Example

Setup

  • User has 10 long ETH calls
  • Mark price: $100 per contract
  • Penalty rate: 1.5%
  • Debt: $500
  • Deposit: $800

Calculation

Step 1: Calculate liquidation price

Long position → sell at discount
Liquidation price = $100 × (1 - 1.5%) = $98.50

Step 2: Calculate proceeds

Proceeds = 10 contracts × $98.50 = $985

Step 3: Calculate bounty

Bounty = min($500 × 5%, $985) = min($25, $985) = $25

Step 4: Settlement

1. Pay bounty first: $25 to liquidator
2. Remaining proceeds: $985 - $25 = $960
3. Apply to debt: $960 >= $500 ✓
4. Credit to user: $960 - $500 = $460

Result:

  • Liquidator earns $25 bounty (paid first)
  • User loses positions, retains $460 + original $800 = $1260

Integration Points

Depends On

ContractPurpose
DiffusalCollateralVaultEquity/margin queries, collateral transfers
DiffusalOptionsPositionManagerPosition queries, zeroing
DiffusalOptionsSeriesRegistrySeries info for pricing
DiffusalOptionsQuoterMark prices
DiffusalOracleVolatility for penalty calculation
DiffusalInsuranceFundShortfall coverage

Used By

ContractPurpose
Approved liquidation botsLiquidation triggering (requires approval)
KeepersAutomated liquidation monitoring

Security Considerations

Approved Liquidators

Only approved liquidators can execute liquidations:

function liquidatePortfolio(address user, uint256 portfolioId, uint256 liquidatorPortfolioId)
    external nonReentrant returns (...)
// Requires: msg.sender is an approved liquidator

Liquidator approval is controlled by the owner via setApprovedLiquidator(). This ensures only trusted entities (e.g., keeper bots, protocol operators) can execute liquidations while maintaining protocol solvency.

MMM Protection

Main Market Makers cannot be liquidated:

if (pm.isMmm(user)) revert Errors.CannotLiquidateMmm();

Reentrancy Protection

All liquidation functions use nonReentrant modifier.

Position Transfer Cash Flow

Liquidation uses a position transfer model where the liquidator acquires positions rather than closing them. This creates bidirectional cash flows:

┌─────────────────────────────────────────────────────────────────────────────┐
│ LONG positions (liquidator buys at discount)                                │
│                                                                             │
│   ┌────────────┐    USDC (discounted)     ┌────────────┐                    │
│   │            │    mark × (1-penalty)    │            │                    │
│   │ Liquidator │ ─────────────────────►   │   Vault    │                    │
│   │            │                          │            │                    │
│   └────────────┘                          └─────┬──────┘                    │
│                                                 │                           │
│                                            position                         │
│                                                 │                           │
│                                                 ▼                           │
│                                           ┌────────────┐                    │
│                                           │    User    │                    │
│                                           └────────────┘                    │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│ SHORT positions (liquidator takes obligation)                               │
│                                                                             │
│   ┌────────────┐    receives USDC         ┌────────────┐                    │
│   │            │    mark × (1+penalty)    │            │                    │
│   │ Liquidator │ ◄─────────────────────   │   Vault    │                    │
│   │            │                          │            │                    │
│   └────────────┘                          └────────────┘                    │
│                                                 ▲                           │
│                                                 │                           │
│                                            pays USDC                        │
│                                                 │                           │
│                                           ┌────────────┐                    │
│                                           │    User    │                    │
│                                           └────────────┘                    │
└─────────────────────────────────────────────────────────────────────────────┘

The settlement order in _settleCollateral():

  1. Transfer for LONGS: Liquidator → User

    • Liquidator pays longsCost USDC to acquire long positions
    • User receives cash for their longs (at penalized price)
  2. Transfer for SHORTS: User → Liquidator

    • User pays shortsCost USDC to liquidator
    • Liquidator takes on the short obligations
  3. Pay BOUNTY: User → Liquidator (5% of debt)

    • First from user's remaining deposit
    • Insurance fund covers any bounty shortfall
  4. Cover remaining shortfall (if user still has negative equity)

    • Insurance fund covers bad debt
// Actual implementation (simplified from _settleCollateral)
// 1. Transfer for LONGS: liquidator → user
if (longsCost > 0) {
    vault.transferCollateral(liquidator, user, longsCost);
}

// 2. Transfer for SHORTS: user → liquidator
if (shortsCost > 0) {
    uint256 actualShortsCost = min(shortsCost, userDeposit);
    vault.transferCollateral(user, liquidator, actualShortsCost);
}

// 3. Pay BOUNTY from user, then insurance covers shortfall
uint256 bountyFromUser = min(bounty, userDepositAfterShorts);
vault.transferCollateral(user, liquidator, bountyFromUser);
if (bounty > bountyFromUser) {
    insuranceFund.cover(bounty - bountyFromUser);
    vault.creditCollateral(liquidator, bounty - bountyFromUser);
}

// 4. Cover remaining bad debt from insurance
if (marginCalculator.getEquity(user) < 0) {
    insuranceFund.cover(abs(userEquity));
}

Shortfall Handling

The insurance fund provides two types of coverage:

  1. Bounty guarantee — Bounty is always paid (5% of debt) to incentivize liquidators
  2. Bad debt coverage — If user still has negative equity after transfers

See DiffusalInsuranceFund for details on the coverage mechanism.

Oracle Dependency

Liquidations rely on accurate oracle prices. If oracle is stale or manipulated:

  • Penalty calculation may be incorrect
  • Mark prices may be wrong
  • System could liquidate healthy users or miss unhealthy ones

Code Reference

Source: packages/contracts/src/DiffusalLiquidationEngine.sol

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

Key Constants

// From Constants.sol
uint256 constant LIQUIDATION_PENALTY_BASE = 0.01e18;           // 1%
uint256 constant LIQUIDATION_PENALTY_IV_BASELINE = 0.5e18;     // 50%
uint256 constant MAX_LIQUIDATION_PENALTY = 1e18;               // 100% cap
uint256 constant LIQUIDATOR_BOUNTY_RATE = 0.05e18;             // 5%
uint256 constant SETTLEMENT_READINESS_WINDOW = 1 days;         // 1 day before expiry
uint256 constant WAD = 1e18;

Protocol Documentation

Contract Documentation

On this page