Liquidations
Overview
The goal of this document is to specify the high-level design of liquidation execution.
For risk specifics, see Margin.
Liquidation consists of several phases, which are enabled based on the account's health.
All actions are initiated by a permissionless liquidator operating through a regular user account. The liquidator does not need to post collateral.
Phases
The first phase consists of per-market or per-balance activity on behalf of the user. Order cancellation, position reduction, and trading an existing token for a token owed to the system are typical actions. As long as the user is unhealthy and there are actions that can improve health per market, they are executed.
The next phase is account-wide. The backstop takes the user's positions and some amount of balance, up to the full balance, at prices near the index price.
Liquidation search strategy
The liquidator and engine state may be described as a distributed state machine running in a loop, potentially until bankruptcy.
Here is the general flow of liquidation states:
stateDiagram-v2
state can_cancel <<choice>>
state can_reduce_perp_position <<choice>>
state can_sell <<choice>>
[*] --> can_cancel
CancelOrders --> can_cancel
can_cancel --> CancelOrders: Can cancel
can_cancel --> can_reduce_perp_position : Cannot cancel
ReducePerpPosition --> can_cancel
can_reduce_perp_position --> ReducePerpPosition : Can reduce negative PnL perpetual
can_reduce_perp_position --> can_sell : Cannot reduce negative PnL
can_sell --> SellOrCollectPositivePnL: Can sell or collect positive PnL
SellOrCollectPositivePnL --> can_cancel
can_sell --> TakeAllOrBankruptcy: Cannot sell or collect positive PnL
TakeAllOrBankruptcy --> [*]
The priority of a borrow or perp position is based on its notional value: the value of the borrow for spot positions or the value of the position for perps.
The actions and choices available during liquidation are outlined in Margin.
So, in general, the next steps are:
1. Reduce or cancel outstanding orders
The liquidator closes all orders that would increase the user's position and make the account less healthy if filled.
This ensures the user will not increase the risk of their positions through newly settled orders.
Orders outside the price band are cancelled.
2. Reduce perp positions and borrows
Per-market perp reduction is executed by a liquidation action that does not specify a size. The engine derives the reducing side from the liquidated account's current position, computes the ideal reduction size from the current risk state, position, market margin parameters, and conservative index price, then executes an internal, immediate, reduce-only order.
Orderbook depth does not determine the ideal size. It only constrains how much of the internal order can actually fill.
The liquidator selects the target account and market, while the engine computes the ideal reduction size.
Here, RO_quote_size is the quote notional to reduce. It is converted to base
size using the same reference price used for the selected market's risk notional
and rounded up.
3. Not enough balances to reduce
If , token trading is allowed unless the token being sold is not owed to the market.
Otherwise:
Not enough token to pay debt
Selling is allowed if:
where .
The token is sold for USDC, which is then used to buy the token owed.
Not enough token to close perp position
If there is not enough USDC to close the position, tokens with no outstanding debt may be sold for USDC.
The same sell-decision formula is used.
Details
The liquidator sorts tokens to check the smallest first, but still validates that the sell-decision formula holds.
This also allows a low-collateral liquidator to improve health, or bankrupt the account, by market-trading the position.
4. Take all positions below 2/3 MMF when not bankrupt
- Validate eligibility and select transfer scope = all positions.
- Cancel all open orders for the liquidated account.
- Settle funding for the liquidated account and the backstop account.
- Compute transfer prices for each position using index bounds and exchange-favoring rounding.
- With
favor_exchange = true, a long uses the low floor (lower mantissa), and a short uses the high ceiling (higher mantissa). - With
favor_exchange = false, a long uses the high ceiling (higher mantissa), and a short uses the low floor (lower mantissa).
- With
- Apply closing fees before transfer:
notional = abs(size) * transfer_pricefee = ceil((trading_fee_bps + 1 bp) * notional)- Split fee between system and backstop.
- Transfer positions to the backstop at the transfer price:
- Liquidated account realizes price PnL at the transfer price.
- The backstop's unrealized PnL starts at zero for the transferred size.
- If the account cannot pay debt after fees and price PnL, fall through to deficit absorption:
- The backstop takes the minimum positive token balances needed to cover the remaining negative quote balance.
- Token conversion uses a conservative token value: the weighted low index price.
- Conversion stops once the user's quote balance becomes non-negative or no positive token balances remain.
- Leave remaining balances in the user account.
- Emit a transfer notification with per-market details and fee split.
Bankruptcy
All assets are moved from the liquidated account to the backstop account at index prices to cover the liquidated account's negative AV. The liquidated account retains zero balances, and the negative quote balance is applied to the system account.
Liquidation score
The liquidator uses liquidation_score to prioritize accounts. Higher scores are checked first.
| Account state | liquidation_score |
|---|---|
NoPosition | not tracked |
Bankrupt | |
Active, , | |
Active, , , | |
Active, , | |
Active, , , |
The score bands keep bankruptcy above position liquidation, and position liquidation above open-order cancellation.
Backstop liquidation specification
1. Overview and principles
This section specifies the deterministic logic for backstop liquidation in the Nord engine. This process transfers distressed positions from a user to the system backstop.
Core invariants
- Zero-sum value transfer (excluding system fees): The sum of the user and backstop account values (
balance + base_size * (index - open_price)) changes only by the fees taken by the system. Including the system account, the net change is zero. - Conservative valuation: Backstop position merging uses rounding that favors the system (round up for longs, round down for shorts).
- Non-negative prices: Prices are
u64. Merge logic followsapply_fillsemantics and never stores a negative price.
2. Data types and definitions
| Variable | Type | Notes |
|---|---|---|
balance | i64 | Signed quote token balance (USDC). |
base_size | i64 | Signed position size (positive for long, negative for short). |
open_price | u64 | Entry price (always positive). |
3. Algorithm details
Step A. Funding settlement
Before transfer, funding is settled to mark positions to the current index.
If the backstop merge is implemented via apply_fill, backstop funding is
already handled there, so only user settlement is strictly required.
pnl = position.base_size * (position.funding_index - market.funding_index)account.balance += pnlposition.funding_index = market.funding_index
Step B. Transfer price
The transfer occurs at the index mantissa that favors backstop liquidity and is least favorable to the user.
- If
user_pos.base_size > 0(long):transfer_price = index.mantissa_low - If
user_pos.base_size < 0(short):transfer_price = index.mantissa_high
Step C. Exchange fees
Fees are calculated as if the user closed the position at transfer_price. One
basis point of the fees is paid to the backstop.
notional = abs(base_size) * transfer_pricetotal_fee = ceil(abs(base_size) * transfer_price * fee_rate)backstop_fee = min(1 bps of notional, total_fee)system_fee = total_fee - backstop_fee- Updates:
user.balance -= total_feesystem.balance += system_feebackstop.balance += backstop_fee
Step D. Position transfer
The user closes the position at transfer_price, and the position is
transferred to the backstop in the same direction. PnL is realized at the
transfer price; there is no direct balance transfer between the user and
backstop.
user_pnl = base_size * (transfer_price - open_price)- Updates:
user.balance += user_pnluser.positions.remove(id)
Step E. Backstop merge logic
The backstop now has an incoming position piece: (incoming_size, transfer_price),
where incoming_size = user_pos.base_size because the position is transferred,
not traded. It is merged into the existing backstop position:
(old_size, old_price).
This merge is equivalent to applying a fill of incoming_size at
transfer_price to the backstop position, using PerpPosition::apply_fill
semantics.
1. Zeroed out (exact close)
- Condition:
new_size == 0 - Logic: The entire position is closed at
transfer_price. - PnL:
closed_size = abs(old_size)backstop.balance += closed_size * sign(old_size) * (transfer_price - old_price)
- State: Remove the position.
2. Increase (same sign)
- Condition:
sign(old_size) == sign(incoming_size) - Logic: Use the weighted-average price over the total size.
- Price:
new_size = old_size + incoming_sizenew_price = (abs(old_size) * old_price + abs(incoming_size) * transfer_price) / abs(new_size)
- Balance: No change.
3. Reduce (opposite sign, no flip)
- Condition:
sign(old_size) != sign(incoming_size)andsign(new_size) == sign(old_size) - Logic: Realize PnL on the closed portion at
transfer_price. - PnL:
closed_size = min(abs(old_size), abs(incoming_size))backstop.balance += closed_size * sign(old_size) * (transfer_price - old_price)
- Price:
new_price = old_price.
4. Flip (opposite sign)
- Condition:
sign(old_size) != sign(incoming_size)andsign(new_size) != sign(old_size) - Logic: Realize PnL on the closed portion at
transfer_price. - PnL:
closed_size = abs(old_size)backstop.balance += closed_size * sign(old_size) * (transfer_price - old_price)
- Price:
new_price = transfer_price.
Step F. Absorb negative user balance
If the user ends with a negative balance, the system absorbs it.
- If
user.balance < 0:system.balance += user.balanceuser.balance = 0
This is a pure transfer between the user and system and does not change total value.
4. Invariant validation (pre-absorption)
This validation is a proof sketch only and is not a runtime step. It is evaluated
after Step E and before loss absorption. For this proof sketch only, use index
everywhere (so transfer_price = index) and ignore rounding and
mantissa_low/mantissa_high selection.
Assumptions:
Target equalities:
Start state:
After fees and transfer (Step C + Step D):
Backstop merge proof by cases (Step E):
Case 1: same-sign increase
Case 2: opposite-sign reduce (no flip)
Case 3: opposite-sign exact close (new_size = 0)
Case 4: opposite-sign flip
5. Mermaid flowchart
flowchart TD
Start([Start Backstop Liquidation]) --> Funding[Step A: Settle Funding<br>User; Backstop optional if apply_fill]
Funding --> TransferPrice[Step B: Transfer Price<br>mantissa_low or mantissa_high]
TransferPrice --> Fees[Step C: Fees<br>User -> System/Backstop]
Fees --> Transfer[Step D: Transfer Position<br>User realizes PnL<br>Backstop receives same side]
Transfer --> Merge[Step E: Merge Backstop Position]
Merge --> SameSign{"sign old_size == sign incoming_size?"}
SameSign -- Yes (Increase) --> Increase[Weighted avg price<br>Conservative rounding]
SameSign -- No --> NewSizeZero{"new_size == 0?"}
NewSizeZero -- Yes (Exact Close) --> Close[Realize PnL<br>Remove Position]
NewSizeZero -- No --> Flip{"sign new_size != sign old_size?"}
Flip -- Yes (Flip) --> FlipPrice[Realize PnL<br>Set price = transfer_price]
Flip -- No (Reduce) --> Reduce[Realize PnL<br>Keep price = old_price]
Increase --> Deficit{"User balance < 0?"}
Close --> Deficit
FlipPrice --> Deficit
Reduce --> Deficit
Deficit -- Yes --> Absorb[Step F: System absorbs deficit<br>User balance = 0]
Deficit -- No --> End([Finish])
Absorb --> End