Liquidation Condition
A position becomes liquidatable when its equity falls below the maintenance margin (MM) threshold:positionEquity = imLocked + unrealizedPnlmmThreshold = notional * snapshotMmBps / 10000
Liquidation Flow
Trigger Liquidation
Anyone calls
SettlementEngine.liquidatePosition(positionId) or SettlementEngine.batchLiquidatePositions(pairId, maxCount). There is no role restriction — liquidation is fully permissionless.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.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.
Calculate PnL
PnL is computed using the forward price:
- LONG:
pnl = notional * (forwardPrice - entryStrike) / PRICE_PRECISION - SHORT:
pnl = notional * (entryStrike - forwardPrice) / PRICE_PRECISION
realizedPnl (capped) and marketPnl (uncapped) are recorded.Calculate Fees
Liquidation incurs both the trading fee and the liquidation penalty:Both rates use the snapshotted values from position open time. The total fee is capped at available margin after PnL settlement.
Liquidation Guard
The protocol enforces a critical guard: liquidatable positions cannot be closed through early termination. When a position owner callsclosePosition(), 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:| Function | Description |
|---|---|
liquidatePosition(uint256 positionId) | Liquidate a single position |
batchLiquidatePositions(bytes32 pairId, uint256 maxCount) | Liquidate up to maxCount eligible positions for a pair |
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: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
Liquidation walkthrough with numbers
Liquidation walkthrough with numbers
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%)
- Current forward price: 1.0689
- PnL = 1,000 * (1.0689 - 1.0800) = -11 USDC
- Position equity = 20 (IM) + (-11) (PnL) = 9 USDC
- MM threshold = 10 USDC
- 9 < 10 —> position is liquidatable
- 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
- 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.