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:
- DiffusalLiquidationCalculator — Complex margin and liquidation calculations
- DiffusalLiquidationSettlement — Collateral settlement and bounty logic
- LiquidationLib — Pure calculation utilities
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:
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
| Type | Positions Closed | Use Case |
|---|---|---|
| Full | All positions | User is deeply underwater |
| Partial | Proportional (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:
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 Type | Liquidation Price |
|---|---|
| Long | Mark × (1 - penalty) — sold at discount |
| Short | Mark × (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:
The cap at 100% ensures that liquidators never need to pay more than the asset's value to take on positions.
Liquidator Bounty
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)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 liquidateportfolioId— The portfolio ID to liquidateliquidatorPortfolioId— 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:
- Calculates the proportion of positions to close:
debt / initialMargin - Closes that fraction of positions (minimum 1 position), transferring to liquidator's specified portfolio
- Re-checks if user is healthy after partial liquidation
- 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 onlyOwnerEmits: ApprovedLiquidatorUpdated
setOperator
Authorizes or deauthorizes an operator.
function setOperator(address operator, bool authorized) external onlyOwnersetPartialLiquidationRatio
Sets the ratio for partial liquidations.
function setPartialLiquidationRatio(uint256 ratio) external onlyOwnerConstraints: 0 < ratio <= WAD (0-100%)
Emits: PartialLiquidationRatioUpdated
setLiquidatorBountyRate
Sets the bounty rate for liquidators.
function setLiquidatorBountyRate(uint256 rate) external onlyOwnerConstraints: 0 <= rate <= 0.1e18 (0-10%)
Emits: LiquidatorBountyRateUpdated
setInsuranceFund
Sets the insurance fund address.
function setInsuranceFund(address insuranceFund_) external onlyOwnerContract 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 onlyOwnertransferOwnership
Transfers contract ownership.
function transferOwnership(address newOwner) external onlyOwnerEvents
| Event | Parameters | Description |
|---|---|---|
PortfolioLiquidated | user, liquidator, portfolioId, debt, longsCost, shortsCost, bounty, isPartial | Liquidation completed |
PositionLiquidated | user, liquidator, seriesId, positionSize, markPrice, liquidationPrice, cost | Position transferred to liquidator |
ApprovedLiquidatorUpdated | liquidator, approved | Approved liquidator status changed |
InsuranceFundUpdated | oldBalance, newBalance | Insurance fund balance changed |
PartialLiquidationRatioUpdated | oldRatio, newRatio | Partial ratio changed |
LiquidatorBountyRateUpdated | oldRate, newRate | Bounty rate changed |
OperatorUpdated | operator, authorized | Operator status changed |
OwnershipTransferred | previousOwner, newOwner | Ownership changed |
CollateralVaultUpdated | oldVault, newVault | Collateral vault address changed |
PositionManagerUpdated | oldManager, newManager | Position manager address changed |
SeriesRegistryUpdated | oldRegistry, newRegistry | Series registry address changed |
QuoterUpdated | oldQuoter, newQuoter | Quoter address changed |
OracleUpdated | oldOracle, newOracle | Oracle address changed |
InsuranceFundAddressUpdated | oldFund, newFund | Insurance 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.50Step 2: Calculate proceeds
Proceeds = 10 contracts × $98.50 = $985Step 3: Calculate bounty
Bounty = min($500 × 5%, $985) = min($25, $985) = $25Step 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 = $460Result:
- Liquidator earns $25 bounty (paid first)
- User loses positions, retains $460 + original $800 = $1260
Integration Points
Depends On
| Contract | Purpose |
|---|---|
| DiffusalCollateralVault | Equity/margin queries, collateral transfers |
| DiffusalOptionsPositionManager | Position queries, zeroing |
| DiffusalOptionsSeriesRegistry | Series info for pricing |
| DiffusalOptionsQuoter | Mark prices |
| DiffusalOracle | Volatility for penalty calculation |
| DiffusalInsuranceFund | Shortfall coverage |
Used By
| Contract | Purpose |
|---|---|
| Approved liquidation bots | Liquidation triggering (requires approval) |
| Keepers | Automated 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 liquidatorLiquidator 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():
-
Transfer for LONGS: Liquidator → User
- Liquidator pays
longsCostUSDC to acquire long positions - User receives cash for their longs (at penalized price)
- Liquidator pays
-
Transfer for SHORTS: User → Liquidator
- User pays
shortsCostUSDC to liquidator - Liquidator takes on the short obligations
- User pays
-
Pay BOUNTY: User → Liquidator (5% of debt)
- First from user's remaining deposit
- Insurance fund covers any bounty shortfall
-
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:
- Bounty guarantee — Bounty is always paid (5% of debt) to incentivize liquidators
- 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;Related
Protocol Documentation
- Liquidation (Protocol) — High-level liquidation mechanics, penalty formula, worked examples
- Margin System (Protocol) — IM/MM calculation, stress scenarios
Contract Documentation
- DiffusalSettlementReadinessLiquidator — Settlement readiness liquidation for users lacking cash
- DiffusalInsuranceFund — Shortfall and bounty coverage mechanism
- DiffusalCollateralVault — Equity calculation,
transferCollateral()function - DiffusalOptionsPositionManager — Position updates during liquidation
- DiffusalSettlementEngine — Settlement of expired options