UPD — Universal Private DollarConcepts

Liquidation

How the ID-based liquidation threshold works, the two-condition gate, the 105% payout split with insurance backstop, and worked examples.

Liquidation

Liquidatorburns 2,300 UPDPosition1.10 stETHbacking 2,300 UPDratio: 110% (underwater)InsuranceEscrow+ 0.050 stETHUPDexcess1.05× par

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:

  1. Anonymous liquidators to backstop the system in extreme conditions, even if they don't operate a Stabilizer position
  2. 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 0110%   (anonymous floor)
ID 1125%   (highest privilege — equal to default min ratio)
ID 2124.5%
ID 3124%
...                (-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 105

The 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:    actualBacking

The 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:    actualBacking

The 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 stETH

Both 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:   0

The 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:

  1. Iterate Stabilizer NFT IDs (ERC721Enumerable provides tokenByIndex and totalSupply)
  2. For each ID, read positionEscrows(tokenId) then call getCollateralizationRatio(price) on the escrow
  3. Compare against your effective threshold (liquidationThreshold(yourTokenId))
  4. Cross-check against the system ratio from OvercollateralizationReporter.getSystemCollateralizationRatio(price)
  5. Submit liquidatePosition with 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).

On this page