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. Liquidation is permissionless on any account with health_factor < 1e18. You repay part of the debt and receive collateral worth the repayment plus a bonus. Supply debt assets and a budget; the controller computes repayment, seizure, refund, and protocol fee.

Goal

Bring an account with health_factor < 1e18 back toward health by repaying its debt, and collect the seized collateral (repayment value plus the liquidation bonus, net of the protocol fee).
liquidate(
    liquidator: Address,
    account_id: u64,
    debt_payments: Vec<(Address, i128)>,
)

Preconditions

  • The target’s health_factor(account_id) is below 1e18 (can_be_liquidated returns true). Otherwise #101 HealthFactorTooHigh.
  • liquidator != account owner. Self-liquidation is rejected with #13 AccountNotInMarket.
  • liquidator holds at least the budgeted amount of every asset in debt_payments.
  • Every collateral and debt asset on the account is priced under the strict Liquidation oracle policy. Stale, deviating, or degraded-dual prices are not tolerated.
  • debt_payments lists the debt assets you are willing to repay with a maximum budget each; the engine may use less and refunds the excess.

Inputs

liquidator
Address
required
Funds the repayment and receives the seized collateral. Authorizes the transfers. Must not own the account.
account_id
u64
required
The unhealthy account being liquidated.
debt_payments
Vec<(Address, i128)>
required
Each entry is (debt_asset, max_amount) in asset-native units: your repayment budget per debt asset. Excess above the engine’s computed repayment is refunded.

Estimate first

Before sending, simulate the outcome with read-only views so you know the repayment, seizure, refund, and bonus up front:
ViewReturns
can_be_liquidated(account_id)boolhealth_factor < 1e18.
health_factor(account_id)i128 (WAD) — current health factor.
liquidation_estimations_detailed(account_id, debt_payments)LiquidationEstimate — seized collateral, protocol fees, refunds, max payment, and bonus rate for your budget.
liquidation_collateral_available(account_id)i128 (WAD) — liquidation-threshold-weighted collateral.
# Estimate a liquidation repaying up to 600 USDC against account 42.
stellar contract invoke \
  --id <CONTROLLER> \
  --source-account liquidator \
  --network testnet \
  -- liquidation_estimations_detailed \
  --account_id 42 \
  --debt_payments '[["CUSDC...", "6000000000"]]'
import { Contract, nativeToScVal, Address } from "@stellar/stellar-sdk";

const controller = new Contract("C...");

const debtPayments = nativeToScVal(
  [[new Address("CUSDC...").toScVal(), nativeToScVal(6_000_000_000n, { type: "i128" })]],
  { type: "Vec" },
);

// Read-only: simulate to read the LiquidationEstimate without submitting.
const op = controller.call("liquidation_estimations_detailed", nativeToScVal(42n, { type: "u64" }), debtPayments);
// const sim = await server.simulateTransaction(tx);  // decode the returned LiquidationEstimate

Liquidation engine

There is no fixed close factor. The engine picks the repayment that restores the most health it can, at a bonus that scales with how unhealthy the account is:
  1. Try the repayment that lifts the health factor to 1.02 at an interpolated bonus; accept it if the resulting health factor is at least 1.0.
  2. Otherwise try target 1.01.
  3. Otherwise fall back to d_max = total_collateral / (1 + base_bonus) (capped at total debt) at the base bonus.
Bonus interpolation: bonus = base + (max − base)·min(2·gap, 1), where gap = (target − HF) / target. base_bonus is the value-weighted average of each collateral’s liquidation_bonus_bps; max_bonus derives from the seized asset’s liquidation threshold. The deeper underwater the account, the closer the bonus moves to max. Seizure and fee: total seizure value is repaid_usd · (1 + bonus), split across collaterals by value share. Per collateral, base = floor(seized / (1 + bonus)), bonus_part = seized − base, and the protocol fee is liquidation_fees_bps · bonus_part. The fee comes out of the bonus only, never the liquidator’s principal. The liquidator receives seized − protocol_fee. Any budget above the computed repayment is refunded. Partial closes: the engine expands repayment sizing to avoid leaving uneconomical residual debt when a full close is feasible. The pool calls are: repay(action) for the debt leg, then withdraw(action, is_liquidation = true, protocol_fee) for the seize leg, which skims the protocol fee as revenue and bypasses the max-utilization check.

Worked example

An account holds 1,000ofXLMcollateral(liquidationthreshold781,000** of XLM collateral (liquidation threshold 78%, `liquidation_bonus_bps = 750`, `liquidation_fees_bps = 100`) against **900 of USDC debt. Prices in WAD; all USD figures are WAD-scaled.
  • Weighted collateral = 1000 · 0.78 = 780. Health factor = 780 / 900 = 0.8667e18 → below 1e18, liquidatable.
  • The single collateral fixes base_bonus = 750 bps = 0.075. Suppose the engine’s cascade settles on repaying $300 of USDC at the interpolated bonus, which for this gap resolves to 7.5% (the base, for a shallow shortfall).
  • Seizure value = 300 · (1 + 0.075) = 322.5 of XLM collateral.
  • base = 322.5 / 1.075 = 300; bonus_part = 322.5 − 300 = 22.5.
  • Protocol fee = 0.01 · 22.5 = 0.225 (1% of the bonus). Liquidator receives 322.5 − 0.225 = 322.275 of XLM.
  • After: collateral 1000 − 322.5 = 677.5, debt 900 − 300 = 600. New weighted collateral 677.5 · 0.78 = 528.45; health factor 528.45 / 600 = 0.8808e18.
You repaid 300ofUSDCandwalkedawaywith300 of USDC and walked away with 322.275 of XLM, a 22.275grossgain,whiletheprotocolkept22.275 gross gain, while the protocol kept 0.225 from the bonus.

Build the call

# Liquidate account 42, repaying up to 600 USDC.
stellar contract invoke \
  --id <CONTROLLER> \
  --source-account liquidator \
  --network testnet \
  -- liquidate \
  --liquidator C... \
  --account_id 42 \
  --debt_payments '[["CUSDC...", "6000000000"]]'
import { Contract, nativeToScVal, Address } from "@stellar/stellar-sdk";

const controller = new Contract("C...");

const debtPayments = nativeToScVal(
  [[new Address("CUSDC...").toScVal(), nativeToScVal(6_000_000_000n, { type: "i128" })]],
  { type: "Vec" },
);

const op = controller.call(
  "liquidate",
  new Address(liquidatorPublicKey).toScVal(),
  nativeToScVal(42n, { type: "u64" }),
  debtPayments,
);
// build, prepareTransaction (simulate), sign with liquidator, send

Authorize

liquidator.require_auth() runs inside liquidate. The transaction authorizes one token transfer per debt asset from liquidator to the pool (the repayment leg). The seizure transfer (collateral from the pool to liquidator) is pool-internal and needs no extra signature. The call is permissionless: only the liquidator’s signature is required, never the target owner’s.

Verify

On success the controller emits:
  • UpdatePositionBatchEvent: ["position", "batch_update"]; the target’s reduced debt and seized supply deltas.
  • UpdateMarketStateBatchEvent: ["market", "batch_state_update"]; refreshed indexes.
  • CleanBadDebtEvent: ["debt", "bad_debt"]; only when the liquidation triggers bad-debt socialization (see below).
Confirm with health_factor(account_id) (now higher, or i128::MAX if fully closed), collateral_amount_for_token / borrow_amount_for_token for the seized and repaid assets, and your own token balances for the received collateral and any refund.

Bad debt

When an account is so far underwater that total_debt > total_collateral and total_collateral ≤ $5 WAD (BAD_DEBT_USD_THRESHOLD = 5·WAD), it cannot be cleared profitably. The protocol socializes the loss: it seizes all remaining supply and debt, removes the account, and reduces each affected market’s supply_index proportionally (new = index · (1 − capped_bad_debt / total_supplied_value), floored at WAD). This runs inline when a liquidation crosses the threshold, or any caller can invoke clean_bad_debt(caller, account_id) directly (#114 CannotCleanBadDebt if the account is not eligible). Either path emits CleanBadDebtEvent with account_id, total_borrow_usd_wad, and total_collateral_usd_wad.

Failure modes

ErrorCauseFix
#101 HealthFactorTooHighTarget’s health factor is at or above 1e18Not liquidatable; wait for it to drop or check can_be_liquidated.
#13 AccountNotInMarketliquidator owns the account (self-liquidation)Use a different address; you cannot liquidate yourself.
#205 UnsafePriceNotAllowedAn asset’s oracle price is stale or deviates too farRetry once the feed recovers; the Liquidation policy is strict.
#114 CannotCleanBadDebtclean_bad_debt on an account not meeting the bad-debt ruleOnly call when collateral ≤ $5 WAD and debt exceeds collateral.

Next

Health factor

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

Risk parameters

Liquidation threshold, bonus, and fee per market, with configured values.