Skip to main content
Repay an unhealthy account’s debt and seize its collateral at a bonus, in one permissionless controller call. Read first: Health factor, Risk parameters.
Returns the receiving account id in Credit mode, or 0 in Transfer mode.

Preconditions

  • The target’s get_health_factor(account_id) is below 1e18 (is_liquidatable returns true). Otherwise #101 HealthFactorTooHigh.
  • liquidator holds at least the budgeted amount of every asset in debt_payments.
  • Every collateral and debt asset on the account resolves a valid price. A stale, deviating, or unavailable feed reverts the whole call — the protocol will not liquidate on a price it does not trust.
  • debt_payments lists the debt assets you are willing to repay with a maximum budget each; the engine may use less and simply leaves the excess with you.
Self-liquidation of an account is allowed. liquidate is fully permissionless, including by the account’s own owner. The only self-reference that is rejected is SeizeMode::Credit(account_id) naming the liquidated account itself, which would hand its collateral straight back — #133 SelfLiquidationNotAllowed.

Seize modes

One SeizeMode governs the whole call, because seizure is pro-rata across the account’s entire collateral set — a per-asset choice would have no meaning. Credit(0) creates the receiving account, owned by the liquidator and bound to the liquidated account’s spoke. A non-zero id must already exist, not be the liquidated account, be owned by or delegated to the liquidator, sit in the liquidated account’s spoke, and be in PositionMode::Normal. Share credit exists for liveness: a seizure that must be paid in underlying can fail purely because the market has no spare cash, exactly when liquidations matter most. Moving shares needs none. Because scaled amounts are index-independent, a share credit is also immune to index drift between planning and application, which the transfer path is not.

Estimate first

Two traps in LiquidationEstimate:
  1. seized_collaterals is gross of protocol_fees. You end up with the difference.
  2. The units follow the mode — asset units for Transfer, RAY-scaled supply shares for Credit.

The liquidation curve

There is no fixed close factor, and the bonus is not a constant. Both are driven by the account’s live health factor against its spoke’s configured curve (SpokeConfig): Read the live values with get_spoke(spoke_id).

How the bonus is picked

The bonus sits between a floor and a ceiling, and slides between them as the account gets sicker. First, one ratio matters throughout:
That is the account’s blended liquidation threshold. The floor (base). Take each collateral position’s stamped liquidation_bonus and average them, weighted by each position’s USD value. The ceiling (max). The largest bonus that still satisfies (1 + bonus) · proportion_seized = 1. Above that, a seizure would take more value than the threshold allows. The slide. The bonus ramps linearly as health factor falls:
At or above target_hf you get exactly base. At or below hf_for_max_bonus the scale hits 1 and you get the full ramp. One last clamp. The result is capped at hf · BPS / proportion_seized − BPS, which is the most the account’s current health factor can support. If that cap lands below the account’s own base, no partial close can be priced at all — so the engine closes the entire debt at the base bonus instead.

How much you can repay

The ideal repayment is whatever brings health factor to target_hf at that bonus. It is capped twice: by d_max = total_collateral / (1 + bonus), and by total debt. One adjustment: if repaying the ideal would leave less than $5 of debt behind, the ideal is raised to a full close. That lets you finish the dust stub. It does not require you to. What you actually pay is min(offered, ideal). The protocol never pulls more than the ideal — it trims each entry, and drops whole legs, before any transfer. So you simply keep the excess.
Those trimmed amounts are what get_liquidation_estimate calls refunds. The name is misleading: no refund transfer happens. The money never leaves your wallet in the first place.
#135 FullCloseRequired means a partial repayment fell below the ideal while that last bonus clamp was binding. Repay the full debt instead.

Seizure and fee

Total seizure value is repaid_usd · (1 + bonus). It is split across the account’s collaterals in proportion to their value. The protocol fee is liquidation_fees bps of the bonus portion only. It never touches the principal you repaid. If the debt token delivers less than it is sent, every seizure shrinks in proportion, floor-rounded. LiquidationEvent.repaid_usd_wad always reports the measured repayment, never the planned one.
The bonus is not fixed at submission time. If another liquidator lands first and improves the account, the bonus on arrival is lower than the estimate your transaction was built from. Only the account’s base bonus is guaranteed as a floor. Enforce your own profitability bound and revert rather than execute below expectation.Two more effects to price in:
  • Rounding runs against you on the collateral leg. There are two rounding sites per seized position, each costing under one collateral unit. On a very small position that can exceed what the bonus pays.
  • A repayment too small to seize a whole unit seizes nothing. The debt still settles. Size the repayment against the seizure you expect it to produce, not just against the debt.

Build the call

Authorize

liquidator.require_auth() runs inside liquidate. The transaction authorizes one token transfer per debt asset from liquidator to the pool (the repayment leg). In Transfer mode the seizure payout is pool-internal and needs no extra signature; in Credit mode no collateral tokens move at all. Only the liquidator’s signature is required, never the target owner’s. Liquidation is not pause-gated and is not blocked by a collateral’s paused flag — but a collateral carrying no_seize cannot be taken (#318 SpokeAssetSeizureHalted).

Verify

On success:
  • LiquidationEvent["position", "liquidation"]; liquidator, account_id, repaid_usd_wad (measured), bonus_bps. It carries no seizure or fee figure.
  • UpdatePositionBatchEvent["position", "batch_update"]; LiqRepay and LiqSeize legs on the liquidated account. A Credit liquidation publishes two batches: the liquidated account’s first, the receiving account’s second (supply-side only, tagged LiqCredit).
  • PoolMarketStateBatchEvent["market", "batch_state_update"]; refreshed indexes.
  • CleanBadDebtEvent["debt", "bad_debt"]; only when the liquidation triggers bad-debt socialization.
LiqSeize is gross of the protocol fee; LiqCredit is net. In share-credit mode the fee is exactly LiqSeize.amount − LiqCredit.amount. Summing both tags as the same quantity double-counts. Confirm with get_health_factor(account_id), get_collateral_amount / get_borrow_amount for the seized and repaid assets, and your own token balances.

Bad debt

Anyone can socialize an account’s bad debt when both of these hold:
  • Total debt exceeds total collateral, and
  • the remaining collateral is at or below BAD_DEBT_USD_THRESHOLD = 5 · WAD ($5).
The protocol then seizes the remaining supply and debt, removes the account, burns its NFT, and writes the loss into the market’s supply index:
The two floors compound. That makes the written-down index at most the single-step value, never more. The extra truncation falls on suppliers, never on the protocol. The result is clamped at SUPPLY_INDEX_FLOOR_RAW = RAY / 1_000 (0.001), not at WAD. Only suppliers of that one market absorb the loss. This runs inline when a liquidation leaves an eligible residue, or any caller may invoke clean_bad_debt(caller, account_id) directly (#114 CannotCleanBadDebt if the account is not eligible). Above the $5 threshold, socialization requires the governance-only force_socialize_bad_debt(account_id), which drops the dust cap and needs only that debt exceeds collateral. Either path emits CleanBadDebtEvent with account_id, total_borrow_usd_wad, and total_collateral_usd_wad. It records no position deltas and is published after any position batch.

Failure modes

Next

Health factor

The exact definition that triggers liquidation, with a worked example.

Risk parameters

Liquidation threshold, bonus, fee, and the halt flags.