> ## Documentation Index
> Fetch the complete documentation index at: https://xoxno.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Stellar lending liquidations

> Liquidate unhealthy accounts: estimate, repay debt, seize collateral, dynamic liquidation bonus, protocol fee, and bad-debt rules.

Repay an unhealthy account's debt and seize its collateral at a bonus, in one permissionless controller call. **Read first:** [Health factor](/stellar-lending/dev/health-factor), [Risk parameters](/stellar-lending/dev/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).

```rust theme={"system"}
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

<ParamField path="liquidator" type="Address" required>
  Funds the repayment and receives the seized collateral. Authorizes the transfers. Must not own the account.
</ParamField>

<ParamField path="account_id" type="u64" required>
  The unhealthy account being liquidated.
</ParamField>

<ParamField path="debt_payments" type="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.
</ParamField>

## Estimate first

Before sending, simulate the outcome with read-only views so you know the repayment, seizure, refund, and bonus up front:

| View                                                          | Returns                                                                                                         |
| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `can_be_liquidated(account_id)`                               | `bool` — `health_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.                                                       |

<CodeGroup>
  ```bash Stellar CLI theme={"system"}
  # 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"]]'
  ```

  ```typescript TypeScript theme={"system"}
  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
  ```
</CodeGroup>

## 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,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 $300 of USDC and walked away with $322.275 of XLM, a $22.275 gross gain, while the protocol kept $0.225 from the bonus.

## Build the call

<CodeGroup>
  ```bash Stellar CLI theme={"system"}
  # 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"]]'
  ```

  ```typescript TypeScript theme={"system"}
  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
  ```
</CodeGroup>

## 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

| Error                        | Cause                                                        | Fix                                                                 |
| ---------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------- |
| `#101 HealthFactorTooHigh`   | Target's health factor is at or above `1e18`                 | Not liquidatable; wait for it to drop or check `can_be_liquidated`. |
| `#13 AccountNotInMarket`     | `liquidator` owns the account (self-liquidation)             | Use a different address; you cannot liquidate yourself.             |
| `#205 UnsafePriceNotAllowed` | An asset's oracle price is stale or deviates too far         | Retry once the feed recovers; the `Liquidation` policy is strict.   |
| `#114 CannotCleanBadDebt`    | `clean_bad_debt` on an account not meeting the bad-debt rule | Only call when collateral ≤ \$5 WAD and debt exceeds collateral.    |

## Next

<CardGroup cols={2}>
  <Card title="Health factor" icon="heart-pulse" href="/stellar-lending/dev/health-factor">
    The exact definition that triggers liquidation, with a worked example.
  </Card>

  <Card title="Risk parameters" icon="sliders" href="/stellar-lending/dev/risk-parameters">
    Liquidation threshold, bonus, and fee per market, with configured values.
  </Card>
</CardGroup>
