Liquidation
How the ID-based liquidation threshold works, the two-condition gate, the 105% payout split with insurance backstop, and worked examples.
Liquidation
A liquidator burns UPD against an underwater Position. The Position pays the liquidator 1.05× par in stETH and sends any excess to the InsuranceEscrow. When the position is too underwater to cover the full bonus, insurance tops up the liquidator (best-effort).
Liquidation is how UPD recovers solvency when a Stabilizer's position drifts below its required collateralization. Unlike Maker-style CDPs, the liquidation threshold is not a single global number — it depends on who is liquidating.
Why ID-Based Thresholds?
The protocol wants two things at once:
- Anonymous liquidators to backstop the system in extreme conditions, even if they don't operate a Stabilizer position
- Active Stabilizers to be able to police each other earlier — they have skin in the game and reputational stake, so they get a higher (more privileged) threshold
The mechanism: an address calling liquidatePosition passes a liquidatorTokenId. If they pass 0, they are treated as anonymous. If they pass a tokenId they own, the threshold scales up based on the ID.
// CollateralMathLib.liquidationThreshold
ID 0 → 110% (anonymous floor)
ID 1 → 125% (highest privilege — equal to default min ratio)
ID 2 → 124.5%
ID 3 → 124%
... (-0.5% per ID step)
ID 31+ → 110% (floored)This means Stabilizer #1 can liquidate any underwater position the moment it dips below 125%. An anonymous liquidator must wait until that same position falls below 110%. By then, Stabilizer #1 has had every opportunity to act — and presumably did, if the math made sense.
The Two-Condition Gate
liquidatePosition requires both of these to hold:
1. position.ratio < system.ratio (no above-average liquidations)
2. position.ratio < liquidator.threshold (per the ID table above)The first condition is the important one. It prevents a privileged liquidator from picking off positions that are below their threshold but still healthier than the system as a whole. If every Stabilizer is at 120% and you happen to be Stabilizer #1 (125% threshold), you cannot liquidate everyone — you can only liquidate positions that are worse than the system.
When the system has no UPD liability at all, getSystemCollateralizationRatio returns type(uint256).max and the first condition is bypassed — that edge case exists so the very first stale position can still be cleaned up.
Payout Math
actualBacking = stEthParValue × positionRatio / 10000
targetPayout = stEthParValue × liquidationLiquidatorPayoutPercent / 100 // default 105The default liquidationLiquidatorPayoutPercent is 105 (105% of par), settable by DEFAULT_ADMIN_ROLE within the range [105, 200].
Case A — position has enough collateral to pay the bonus:
actualBacking ≥ targetPayout
liquidator gets: targetPayout (e.g. 1.05× par in stETH)
insurance gets: actualBacking - targetPayout
totalFromPos: actualBackingThe position is fully drained for that slice — both the bonus and the seized excess go out. The Stabilizer keeps nothing from the liquidated portion.
Case B — position is too underwater to pay the bonus:
actualBacking < targetPayout
liquidator gets: actualBacking + insurance_topup (best-effort)
insurance gets: 0
totalFromPos: actualBackingThe liquidator gets everything the position has, and InsuranceEscrow pays the rest of the bonus if it has the funds. If insurance is depleted to zero, the liquidator simply receives less than the configured 105%. There is no circuit breaker — depletion is an accepted operating mode.
Worked Example 1 — Healthy Liquidation
Setup:
- ETH/USD = $2,300
- Stabilizer #5 has a position backing 2,300 UPD with 1.10 stETH collateral (ratio = 110%)
- System-wide ratio = 115%
- Insurance has 5 stETH available
- Liquidator owns Stabilizer #2 (threshold = 124.5%)
- Liquidator burns the full 2,300 UPD
Calculations:
stEthParValue = 2,300 / 2,300 = 1.000 stETH
actualBacking = 1.000 × 11000 / 10000 = 1.100 stETH
targetPayout = 1.000 × 105 / 100 = 1.050 stETH
actualBacking ≥ targetPayout — Case A.
liquidator gets: 1.050 stETH (105% of par)
insurance gets: 0.050 stETH
position loses: 1.100 stETH (totalFromPos)
position liability: -2,300 UPD (burned)Both gate conditions held: 110% < 115% (system) and 110% < 124.5% (threshold). The liquidator earns a 5% bonus in stETH, insurance picks up the residual buffer, and the Stabilizer is wiped out for that slice.
Worked Example 2 — Underwater Position with Insurance Topup
Setup:
- Same Stabilizer #5, but now the position has only 0.95 stETH backing 2,300 UPD (ratio = 95%)
- System-wide ratio = 102% (still above 100%, but the system is stressed)
- Insurance has 0.20 stETH available
- Anonymous liquidator (no NFT, threshold = 110%)
Calculations:
stEthParValue = 1.000 stETH
actualBacking = 1.000 × 9500 / 10000 = 0.950 stETH
targetPayout = 1.000 × 105 / 100 = 1.050 stETH
actualBacking < targetPayout — Case B.
From position: 0.950 stETH
Bonus shortfall: 1.050 - 0.950 = 0.100 stETH
Insurance has: 0.200 stETH
Insurance pays: 0.100 stETH (full shortfall covered)
liquidator gets: 0.950 + 0.100 = 1.050 stETH (full 105% bonus achieved)
insurance left: 0.200 - 0.100 = 0.100 stETHBoth gates passed: 95% < 102% (system) and 95% < 110% (threshold). The liquidator still received the full bonus — insurance absorbed the shortfall.
Worked Example 3 — Insurance Depleted
Same setup as Example 2, but insurance has only 0.03 stETH left.
Bonus shortfall: 0.100 stETH
Insurance pays: 0.030 stETH (all available — best-effort)
liquidator gets: 0.950 + 0.030 = 0.980 stETH (98% of par — under target)
insurance left: 0The transaction does not revert. The liquidator receives 98% of par, which is still positive — not the configured 105% bonus, but more than the actualBacking alone. Future liquidations against undercollateralized positions will receive only what the position itself holds, until insurance is replenished (excess from healthy liquidations refills it).
Finding Liquidatable Positions
StabilizerNFT does not maintain a sorted-by-ratio list. Liquidator infrastructure is responsible for discovering candidates off-chain. The minimal recipe:
- Iterate Stabilizer NFT IDs (ERC721Enumerable provides
tokenByIndexandtotalSupply) - For each ID, read
positionEscrows(tokenId)then callgetCollateralizationRatio(price)on the escrow - Compare against your effective threshold (
liquidationThreshold(yourTokenId)) - Cross-check against the system ratio from
OvercollateralizationReporter.getSystemCollateralizationRatio(price) - Submit
liquidatePositionwith the eligible position
A subgraph or indexed event log makes this O(1) instead of O(n) per scan. The events to watch are FundsAllocated, FundsUnallocated, Rebalanced, and the stETH balance changes on each PositionEscrow.
Burn vs Liquidate Under Stress
Burn paths and liquidation paths diverge below 100%
When the system-wide collateralization ratio drops below 100%, burnUPD reverts (SystemUnstableUnallocationNotAllowed). UPD holders cannot redeem at par because the redemption math doesn't add up — there isn't enough stETH to pay everyone $1 worth. liquidatePosition and unallocateMyself both bypass this check, because they are how the system recovers from undercollateralization.
Practically: during a stress event, holders wait or use sUPD's queued/pro-rata redemption paths. Liquidators continue to clean up underwater positions, restoring the system ratio above 100%, at which point burnUPD becomes callable again.
Self-Liquidation: The unallocateMyself Escape Hatch
A Stabilizer that wants to exit before being liquidated by a third party can call unallocateMyself(tokenId, updAmount, priceQuery). This burns UPD they hold and returns all freed stETH to their own StabilizerEscrow — including the user-side par. Combined with rebalanceToMinRatio, this gives Stabilizers a full unwind path without dependency on any other actor.
The economics of self-liquidation differ from third-party liquidation: there is no 5% bonus to forfeit, but there is also no insurance topup. A Stabilizer holding an underwater position who self-unallocates gets back exactly the position's actualBacking (in stETH terms, at the position's current ratio).