Skip to main content
Liquidation is the protocol’s mechanism for closing positions that have lost too much value relative to their locked margin. When a position’s equity drops below its maintenance margin threshold, anyone can trigger liquidation. This protects the liquidity pool from accumulating excessive bad debt by closing distressed positions before losses grow further.

Liquidation Condition

A position becomes liquidatable when its equity falls below the maintenance margin (MM) threshold:
positionEquity < mmThreshold
Where:
  • positionEquity = imLocked + unrealizedPnl
  • mmThreshold = notional * snapshotMmBps / 10000
The comparison is strict less-than (<). A position whose equity is exactly equal to its MM threshold is NOT liquidatable. This matters at the boundary — a position at equity = 10 USDC with mmThreshold = 10 USDC remains safe.

Liquidation Flow

1

Trigger Liquidation

Anyone calls SettlementEngine.liquidatePosition(positionId) or SettlementEngine.batchLiquidatePositions(pairId, maxCount). There is no role restriction — liquidation is fully permissionless.
2

Verify Preconditions

The contract checks that the position is OPEN and that it is currently liquidatable (positionEquity < mmThreshold). If either check fails, the transaction reverts.
3

Get Forward Price

The current forward price for the position’s fixing timestamp is fetched from the OracleModule. An oracle fee is collected from the trader’s collateral for this price read.
4

Calculate PnL

PnL is computed using the forward price:
  • LONG: pnl = notional * (forwardPrice - entryStrike) / PRICE_PRECISION
  • SHORT: pnl = notional * (entryStrike - forwardPrice) / PRICE_PRECISION
Both realizedPnl (capped) and marketPnl (uncapped) are recorded.
5

Calculate Fees

Liquidation incurs both the trading fee and the liquidation penalty:
tradingFee = notional * snapshotTradingFeeBps / 10000
liquidationPenalty = notional * snapshotLiquidationPenaltyBps / 10000
totalFee = tradingFee + liquidationPenalty
Both rates use the snapshotted values from position open time. The total fee is capped at available margin after PnL settlement.
6

Settle and Close

The position is settled with CloseReason.LIQUIDATED. USDC flows are distributed: PnL between trader and pool, fees to fee destinations (30% treasury, 70% pool). The PositionLiquidated event is emitted.

Liquidation Guard

The protocol enforces a critical guard: liquidatable positions cannot be closed through early termination. When a position owner calls closePosition(), the contract checks !isLiquidatable() and reverts with EarlyTerminationNotAllowed() if the position is underwater. This guard exists to ensure that the liquidation penalty is always collected on distressed positions. Without it, a trader could close their liquidatable position via early termination and avoid the penalty fee.
The liquidation guard is one-directional. It prevents early termination of liquidatable positions, but it does not prevent adding margin. A trader can always call addPositionMargin to add margin to a liquidatable position, potentially rescuing it from liquidation by raising equity above the MM threshold.

Account-Level Guard

The protocol also enforces an account-level liquidation guard: AccountHasLiquidatablePosition — A trader cannot open new positions while any of their existing positions is liquidatable. This prevents a scenario where a trader ignores a liquidatable position and continues taking on new risk. The trader must either add margin to rescue the position or wait for it to be liquidated before opening new positions.

Permissionless Liquidation

Liquidation is fully permissionless. Any Ethereum address can call the liquidation functions:
FunctionDescription
liquidatePosition(uint256 positionId)Liquidate a single position
batchLiquidatePositions(bytes32 pairId, uint256 maxCount)Liquidate up to maxCount eligible positions for a pair
There is no special liquidator role, no allowlist, and no priority ordering. The first transaction to execute successfully liquidates the position.

No Liquidator Reward

Unlike many DeFi protocols, the Open Nile Protocol does not reward the liquidator. The liquidation penalty goes entirely to fee destinations (30% treasury, 70% pool), not to the address that triggered the liquidation. The liquidator pays gas costs with no direct economic incentive.In M2 (testnet), the keeper service performs liquidations as a protocol service. M3 may introduce liquidator incentives for external participants.

Bad Debt

When a position’s loss exceeds its locked margin, the excess becomes bad debt:
if |marketPnl| > imLocked:
    realizedPnl = -imLocked              // trader loses all locked margin
    badDebt = |marketPnl| - imLocked     // excess absorbed by pool
Bad debt reduces pool equity and LP share price. A BadDebt event is emitted with the account, position ID, and bad debt amount for off-chain monitoring. The pool’s equity is clamped at zero — it cannot go negative even under extreme bad debt accumulation. This preserves the ERC-4626 vault accounting invariant.

Worked Example

Setup:
  • Notional: 1,000 USDC
  • Side: LONG
  • Entry strike: 1.0800
  • IM locked: 20 USDC (2% of notional)
  • MM threshold: 10 USDC (1% of notional, from snapshotMmBps = 100)
  • Snapshotted trading fee: 5 bps (0.05%)
  • Snapshotted liquidation penalty: 30 bps (0.3%)
Price moves against the trader:
  • Current forward price: 1.0689
  • PnL = 1,000 * (1.0689 - 1.0800) = -11 USDC
Liquidation check:
  • Position equity = 20 (IM) + (-11) (PnL) = 9 USDC
  • MM threshold = 10 USDC
  • 9 < 10 —> position is liquidatable
Fee calculation:
  • Trading fee = 1,000 * 5 / 10,000 = 0.50 USDC
  • Liquidation penalty = 1,000 * 30 / 10,000 = 3.00 USDC
  • Total fee = 3.50 USDC
Settlement:
  • Margin at risk: 20 USDC
  • Loss: 11 USDC (within margin, no bad debt)
  • Margin after loss: 20 - 11 = 9 USDC
  • Fee deducted: 3.50 USDC (capped at available 9 USDC, so full amount collected)
  • Returned to trader: 9 - 3.50 = 5.50 USDC
  • Pool receives: 11 USDC (from trader’s loss)
  • Fee destinations receive: 3.50 USDC (1.05 treasury, 2.45 pool)

Liquidation Timing

The protocol does not enforce a grace period or delay before liquidation. The moment a position’s equity drops below the MM threshold, it is immediately eligible for liquidation. In practice, the keeper service checks for liquidatable positions on every cycle (aligned with block time), so there is a small operational delay between a position becoming liquidatable and actual liquidation execution.

Before Maturity

Positions can be liquidated at any time before maturity if the forward price moves sufficiently against the trader. The forward price is the reference for the liquidation check.

At Maturity

Matured positions that are also liquidatable must go through liquidation, not maturity settlement. The settlePosition function checks !isLiquidatable() and reverts for underwater positions, ensuring the liquidation penalty is collected.