Skip to content
Protocol

Liquidations

Partial by default, confirmed before execution, throttled by measured liquidity, and paid for by a bonus that moves with conditions.

A liquidation reduces a position's debt against a proportional seizure of its collateral, plus a bonus paid to whoever supplied the fyUSD. Fyber liquidates partially by default, requires a two-step confirmation, caps how much can be liquidated per transaction, per branch per hour and across the protocol per hour, and scales the bonus with the difficulty of the sale rather than fixing it.

Preconditions

A liquidation requires all of the following (Rule R-6.1.1):

  • The branch is activated and not shut down.
  • The regime is LIVE or DRIFT, never BLIND.
  • No Closer liquidation freeze is in effect. The freeze covers liquidate and liquidateOpen under the same mask, so it cannot be routed around, and two such freezes on one branch must be at least 7 days apart.
  • If a token upgrade freeze is running, at least 24 hours have passed since it started.
  • The position has been flagged, and the flag is neither too recent nor expired.
  • The position's ICR at pLiq is below the threshold at the moment of execution, and not merely at the moment of flagging.
  • liquidate additionally requires LIVE with no session edge running; liquidateOpen requires DRIFT and the whole-position condition below.

The price condition is per position, never global. pLiq is pRef when the fall is confirmed and pOfficial when it is not, so an unconfirmed fall leaves only the positions that were already under the threshold at the last print sellable. Version 0.4 removed the global dropConfirmed gate on liquidateOpen, which contradicted that sentence for any position that entered the weekend below the threshold after a Friday evening when the live bucket had run dry (Rule R-6.5.2, decision D85).

One threshold, at every hour

TierLiquidation threshold
A — broad-market index ETFs115%
B — GLD, IWM120%
BLINDNo liquidation at all
Shut branchNo liquidation; settlement replaces it

Guarantee

The threshold does not move with the session, with the calendar, or with the protocol's confidence in the price. It is the same number on a Sunday as on a Tuesday. Everything the protocol does differently out of session is about proof and about who buys — never about the number a borrower is measured against.

Flag, then confirm

function flag(address branch, address user) external;
function liquidate(address branch, address user, uint256 maxDebtToRepay)
    external returns (uint256 debtRepaid, uint256 collSeized);
function liquidateOpen(address branch, address user, uint256 maxDebtToRepay, uint256 minCollOut)
    external returns (uint256 debtRepaid, uint256 collSeized);

flag requires the position to be below its threshold now, and records the timestamp. In LIVE, liquidate requires between 90 seconds and 1 800 seconds to have elapsed. In DRIFT, liquidateOpen requires between 30 minutes and 6 hours (Rule R-6.1.2). An expired flag must be re-set. Any owner operation that lifts the ICR back above the threshold effectively clears the flag, because the threshold is re-checked at execution in any case.

Guarantee

No position is liquidated in the same transaction, or the same block, as the price update that made it liquidatable. Out of session the borrower gets at least 90 minutes of on-chain visibility: 60 minutes for a one-hour average to move at all, then 30 minutes between the flag and the sale. Repaying and depositing work throughout, and neither reads a price (Rule R-6.1.2).

Out of session: confirmation, then an open sale

Out of session, once a fall is confirmed, pLiq becomes min(pOfficial, pDex clamped down by the band). The band is a constant: 4% on tier 1, 6% on tier 2, whatever the depth or duration of the fall. The persistence clock starts once the on-chain price is 2% below the official print.

A fall is confirmed only if one of these holds (Rule R-3.2.14):

  • two independent qualified pools of the same branch agree within 3%;
  • the fall has persisted for 3 hours on a single qualified pool;
  • a verified Data Streams report agrees, where such a stream exists and the market is open.

Confirmation by the basket of the other equity branches was deleted in version 0.4, along with the widening. The branches read the same pools, so the basket was never a second source, and it allowed the cost of manipulation to be paid once per pool while confirmation was collected once per branch (decision D78).

Guarantee

The deepest fall the protocol will ever recognise out of session is 4% below the official print on tier 1, for any duration. A position at or above 119.8% at the close therefore cannot be sold over a weekend by any on-chain price move at all.

Guarantee

In DRIFT the Stability Pool is never the buyer. liquidateOpen pays the debt from the caller's own fyUSD and hands them the collateral at 2% below pLiq. If nobody wants it at that price, nothing happens and the position waits for the next official print. A pool depositor agreed to buy at the oracle price; nobody signed up to buy on a Sunday estimate.

Whole-position solvency, before anything executes

liquidateOpen requires the entire position to satisfy coll × pLiq ≥ debt × (1 + B_MIN), which is 102% on tier 1 and 103% on tier 2, before it runs. It reverts BuyerWouldLoseCollateral otherwise (Rule R-6.5.3).

The earlier condition applied only to the slice being bought. A buyer could pass a small maxDebtToRepay against a position at 101%, take 1.02 of collateral per unit of debt from a position that held 1.01, lower its ratio, and come back every hour. That contradicted the rule that a liquidation never worsens the position it touches. A position below 1 + B_MIN now waits for the next print and for the pool, which is the path that ends in bad debt being recorded properly rather than handed to a buyer piece by piece (decision D83, invariant I-12).

This is also what makes the attack unprofitable. Moving a qualified pool 4% and holding it for 90 minutes costs tens of thousands of dollars on the measured depths; the debt sitting in the window it unlocks, between 115% and 119.8%, is 3 to 5% of a branch, capped again by the bucket below. See Risks.

Order of payers

Three steps, in order, for an amount X of debt to be liquidated at price p with bonus b, in LIVE only and never during a session edge (Rule R-6.2.1). In LIVE, p is always pOfficial: the official print, never an interpolation of it and never an on-chain estimate.

1. Stability Pool. X_sp = min(X, SP.totalDeposits). The pool burns X_sp of its deposits and receives min(X_sp × (1 + b) / p, coll) in stock token, less the keeper's share.

2. Direct liquidation. If debt remains and the caller passed a non-zero maxDebtToRepay, the caller burns their own fyUSD through a standard allowance and receives the same seizure ratio, plus the keeper share, since they are both keeper and payer.

3. Bad debt. If debt still remains and the position's ICR is below 100%, the remaining collateral is transferred to the Backstop, the remaining debt is removed from the branch and recorded as badDebt. Otherwise the position simply keeps its residual debt and collateral and remains liquidatable.

Guarantee

The seizure is always derived from the amount actually burned, returned by the internal repay, never from a caller-supplied parameter. If the collateral is worth less than debt plus bonus, the seizure is capped at the collateral and the pool or the direct liquidator takes the loss — the borrower is never charged more collateral than they have (Rule R-6.2.2).

After a full liquidation, any collateral not seized goes to collSurplus[user], claimable by the owner at any time, in any state, forever (Rule R-6.2.3).

Partial by default: the dynamic close factor

The liquidated amount is exactly what is needed to bring the position back to a target ratio, not a fixed fraction of the debt (Rule R-6.3.1):

How it is calculated

X = (CR_target × D − C × p) / (CR_target − (1 + b)), where D is debt, C collateral, p the execution price and b the bonus.

CR_target is MCR × 1.10 for Tier 1 and × 1.15 for Tier 2, giving 126.5% and 138%. The same target applies in both regimes; an open sale is an ordinary partial liquidation with a different buyer and a smaller discount.

A liquidation becomes total when the ICR is below 105%, when the residual debt would fall under minDebt, or when X would exceed the debt (Rule R-6.3.2).

Invariants after a partial liquidation (Rule R-6.3.4). The post-liquidation ICR is at least the pre-liquidation ICR, and the residual debt is at least minDebt. A liquidation always improves the position it touches.

Throughput buckets

Throughput is limited in four nested ways, all expressed in units of measured exit liquidity d2_eff and all counted in debt burned, the fyUSD the liquidation destroys (Rule R-6.3.3):

LimitValue
Per transaction, LIVE0.25× × d2_eff of debt
Per transaction, DRIFT0.10× × d2_eff of debt
Per branch per hour, LIVE1.0× × d2_eff, or 1.5 × in the regular session, refilled linearly over 60 minutes
Per branch per hour, DRIFT0.25× × d2_eff
Per shut period, DRIFTat most min(1.0 × d2_eff, 15% of branch debt)
Global, all branches, per hour0.6× × Σ d2_eff

When a bucket is empty, the call reverts BucketEmpty or PeriodCapReached. The position stays flagged, and the flag may expire and need re-setting.

Two changes arrived with version 0.4 (decision D89). The buckets used to be counted in collateral seized, valued at the execution price, while invariant I-8 counted debt: at pLiq = 0.96 × close the mismatch let through 6% more tokens than intended, so both now use the same unit. And the out-of-session per-transaction cap was lowered to 0.10×, so two or three open sales fit in an hour instead of a single one taken by whoever knew the confirmation time first on a first-come sequencer.

The per-shut-period ceiling is the one that bounds a manipulation: whatever an attacker manages to prove, they cannot extract more than 15% of a branch's debt across one weekend.

Guarantee

The protocol never allows more collateral to be seized in an hour than the measured on-chain depth can absorb. Liquidations are spread over hours rather than concentrated into a single block, which is what turns a correlated crash into a series of ordinary sales rather than a cascade (Rule R-6.3.3).

Dynamic bonus

In LIVE, the bonus slides with confidence and with the size of the sale (Rule R-6.4.2):

How it is calculated

b = clamp(B_MIN + (B_BASE − B_MIN) × conf + adj_size, 0, 15%)

ComponentTier 1Tier 2
B_BASE, at full confidence5%5%
B_MIN, the floor2%3%
Seizure above 0.5 × d2_eff+2.0 points+2.0 points
Seizure above 1.0 × d2_eff+4.0 points, replacing the above+4.0 points

In DRIFT the discount is fixed at B_MIN and the size adjustment does not apply, because the buyer chooses their own size and is their own keeper. B_MIN is calibrated to be enough to attract someone who has an opinion about the real price, and too little to pay for a manipulation.

The logic is otherwise unchanged: the bonus pays for the difficulty of the sale. A large seizure in a thin market is worth more; a seizure done on a live, deep market costs the borrower less.

Keeper share

keeperShare = 0.5% of the seized collateral, capped at 200 USD at price p

Taken from the seizure before it reaches the pool or the direct liquidator, and paid in stock token to msg.sender (Rule R-6.4.1). There is no keeper fund, no gas reimbursement, no urgency premium. The constants are frozen at construction. liquidateOpen pays no keeper share at all: the buyer is their own keeper.

What was removed

The closed-market design carried four mechanisms that no longer exist, and it is worth being explicit about their absence:

RemovedWhy
The Friday sweep, in the last hour of the week's sessionIt liquidated positions at 160% for a crash that, on the measured distribution, does not arrive 99 times out of 100.
The higher closed-market threshold MCR_closedReplaced by one threshold and a solvency cushion of 6.09% on tier 1.
The HardFloor relief valveReplaced by the ordinary threshold plus an open sale, which needs no special case.
The Monday grace windowA grace is a window in which the protocol knows and does nothing. Replaced by the 10 minutes session edge.
The return ramp on a print arriving after a long silenceIt had the Stability Pool executing at an interpolated price above the print, which cost the pool up to 13.8% per liquidation and paid the liquidated borrower exactly that. Replaced by the same session edge.

The purpose the sweep served — reduce a fragile position while the market is deep — is now served continuously by the haircut: as confidence falls, the ratio needed to add risk rises, so a position drifting toward the threshold cannot be topped up with more debt on a Friday evening.

Mint freeze

A branch enters MINT_FREEZE whenever its TCR is below its critical ratio — 140% on tier 1, 145% on tier 2 — or bad debt exists, or a token upgrade freeze is running (Rule R-6.8.1). While frozen, borrow, open and withdraw are refused; repay, addCollateral, liquidate and redeem behave normally. The freeze reverses automatically as soon as its cause does.

That TCR is measured on pRef without the haircut, since version 0.4 (decision D80). The haircut belongs to the position that is acting, and putting it into a branch-level condition made the effective Sunday threshold 203% rather than 140%: a healthy branch refused a withdrawal from a position at 300% every weekend, and a branch that had just been through a crash stayed frozen after the market had recovered. Invariant I-17 states the corrected form directly: a branch above CCR on pRef with no bad debt and no upgrade freeze is never in MINT_FREEZE, whatever the haircut.

This is the whole of Fyber's recovery mode. There is no global recovery mode, no protocol-wide ratio that changes everyone's terms at once, and no mechanism that forces a borrower to act (Rule R-0.3).

Bad debt

Bad debt is fyUSD supply with no debt behind it. Any entry also increments badDebtCumul, which is monotone and permanently locks the branch's debt-ceiling tier (Rule R-6.8.2).

Three things then happen, all permissionless:

  1. Backstop.coverBadDebt(branch, amount) burns fyUSD from the backstop and reduces the recorded bad debt.
  2. Backstop.sellCollateral(branch, amount, minFy) sells the residual collateral on-chain at the oracle price less 3%, during a live session only, at most 0.25 × d2_eff per transaction. The proceeds stay in the backstop as fyUSD. There is no off-chain carrying by a treasury.
  3. If bad debt has stood for 72 hours and the backstop balance is below minDebt, Backstop.redistribute(branch) becomes callable by anyone: the bad debt is added pro rata by debt to the remaining positions of that branch, and the backstop's collateral for that branch is distributed pro rata as well (Rule R-6.8.4).

Risk

Redistribution is a real loss to remaining borrowers on the same branch. It is confined to that branch — never socialised across the protocol — and the 72-hour delay exists to give anyone the chance to fund the backstop first. There is no timelock and no vote; the mechanism simply becomes callable.

Shutdown and settlement

The four shutdown triggers are listed in Price regimes. Once a branch is shut, liquidation stops entirely and is replaced by settlement (Rule R-6.9.8):

function settleAfterShutdown(address user) external;  // permissionless, 30 days after urgentFrom

The position's debt is absorbed by the branch's Stability Pool at the frozen lastGood price, with zero bonus and zero keeper share, up to the pool's total deposits. Any remainder may be paid by the caller in fyUSD, again with no bonus. Unconsumed collateral goes to collSurplus[user]; a residual debt with no collateral becomes bad debt. The position is then closed.

Settlement is the reason a shut branch converges rather than lingering. The surplus stays claimable without any time limit.

Last reviewed: 2026-09-07 · Spec v0.4