Diffusal
Contracts

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.


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}

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
7. If proceeds after bounty >= debt:
   - Credit surplus to user
8. If proceeds after bounty < debt (shortfall):
   - Draw from insurance 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

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;
    mapping(address => bool) operators;
    address collateralVault;
    address positionManager;
    address seriesRegistry;
    address quoter;
    address oracle;
    address insuranceFund;
    uint256 partialLiquidationRatio;  // Default 50%
}

LiquidationInfo Struct

struct LiquidationInfo {
    bool isLiquidatable;      // Can be liquidated
    uint256 debt;             // IM - equity (USDC)
    uint256 estimatedProceeds; // Expected from closing positions
    uint256 estimatedBounty;  // Expected liquidator reward
    uint256 positionCount;    // Number of positions
}

LiquidationResult Struct

struct LiquidationResult {
    address user;               // User liquidated
    address liquidator;         // Who triggered liquidation
    uint256 debt;               // Amount underwater
    uint256 totalProceeds;      // Actual proceeds from closing
    uint256 bounty;             // Bounty paid to liquidator
    uint256 insuranceUsed;      // Amount drawn from insurance
    uint256 positionsLiquidated; // Positions closed
    int256 newEquity;           // User's equity after liquidation
    bool isPartial;             // Partial or full liquidation
}

View Functions

calculateDebt

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

function calculateDebt(address user) 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%).


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 are permissionless—anyone can liquidate an undercollateralized user.

liquidate

Performs full liquidation (closes all positions).

function liquidate(address user) external nonReentrant returns (LiquidationResult memory result)

Requirements:

  • User must be liquidatable
  • User must not be an MMM

Emits: UserLiquidated, PositionLiquidated (per position)


liquidatePartial

Performs partial liquidation using proportional position closure.

function liquidatePartial(address user) external nonReentrant returns (LiquidationResult memory result)

Behavior:

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

Use case: Often sufficient to restore user to healthy state without full wipeout.

Emits: Same as full liquidation.


Owner Functions

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


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
UserLiquidateduser, liquidator, debt, proceeds, bounty, isPartialLiquidation completed
PositionLiquidateduser, seriesId, positionSize, markPrice, liquidationPrice, proceedsIndividual position closed
InsuranceFundUpdatedoldBalance, newBalanceInsurance fund balance changed
PartialLiquidationRatioUpdatedoldRatio, newRatioPartial ratio 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+original460 + 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
Liquidation botsPermissionless liquidation triggering
KeepersAutomated liquidation monitoring

Security Considerations

Permissionless Liquidation

Anyone can liquidate an undercollateralized user:

function liquidate(address user) external nonReentrant returns (...)

This ensures the protocol remains solvent without relying on trusted actors.

MMM Protection

Main Market Makers cannot be liquidated:

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

Reentrancy Protection

All liquidation functions use nonReentrant modifier.

Shortfall Handling

When proceeds don't cover debt, the settlement order is:

  1. Bounty paid first (capped at total proceeds) to incentivize liquidators
  2. Remaining proceeds applied to debt
  3. Insurance fund covers shortfall (debt - proceedsAfterBounty)
  4. If insurance insufficient, bad debt remains
// Always pay bounty to incentivize liquidators, even in shortfall cases
actualBountyPaid = bounty > totalProceeds ? totalProceeds : bounty;

if (actualBountyPaid > 0) {
    vault.creditCollateral(msg.sender, actualBountyPaid);
}

uint256 proceedsAfterBounty = totalProceeds - actualBountyPaid;
if (proceedsAfterBounty >= debt) {
    // Sufficient proceeds - credit remaining to user
    uint256 remaining = proceedsAfterBounty - debt;
    if (remaining > 0) vault.creditCollateral(user, remaining);
} else {
    // Shortfall - use insurance fund
    insuranceUsed = _coverShortfall(debt - proceedsAfterBounty);
}

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

Testnet: View on MonadVision

Key Constants

// From Constants.sol
uint256 constant LIQUIDATION_PENALTY_BASE = 0.01e18;      // 1%
uint256 constant LIQUIDATION_PENALTY_IV_BASELINE = 0.5e18; // 50%
uint256 constant LIQUIDATOR_BOUNTY_RATE = 0.05e18;        // 5%
uint256 constant WAD = 1e18;

On this page