Liquidations
Liquidations are the mechanism that keeps the UnitFlow Lending protocol solvent. When a borrower's health factor falls below 1.0, their position becomes undercollateralised relative to the protocol's risk parameters. Any wallet can then repay part of that debt and receive the borrower's collateral at a discount — the liquidation bonus.
Health Factor
The health factor (HF) is a single number that summarises the safety of a position:
healthFactor = (totalCollateralUSD × liquidationThreshold) / totalDebtUSD
= (totalCollateralUSD × 0.6667) / totalDebtUSD
// Expressed as a WAD (1e18) integer on-chain
// HF = 1.5e18 → displayed as "1.50"
// HF = MaxUint256 → no debt, displayed as "∞"| Health Factor | Status | UI Colour |
|---|---|---|
| ≥ 2.0 | Safe | Green |
| 1.5 – 2.0 | Moderate risk | Yellow |
| 1.1 – 1.5 | High risk | Orange |
| 1.0 – 1.1 | Critical | Red (pulsing dot) |
| < 1.0 | Liquidatable | Red |
What Causes HF to Drop?
- Borrow interest accruing over time (debt grows, collateral stays the same)
- Withdrawing collateral while a borrow is active
- Borrowing more against the same collateral
- A fall in the oracle price of the collateral asset (relevant when non-stablecoin assets are added)
Liquidation Mechanics
Close Factor — 50%
A liquidator can repay at most 50% of a borrower's outstanding debt in a single call. This prevents a single liquidation from over-correcting the position and ensures borrowers retain some collateral after a liquidation event.
maxDebtToCover = totalDebtUSD × 0.50 // in USD debtToCover = min(requestedAmount, maxDebtToCover)
Collateral Seizure
The liquidator specifies which collateral asset they want to receive. The engine calculates the equivalent collateral amount at the oracle price, applies the liquidation bonus, and transfers it from the borrower's position to the liquidator:
collateralToSeize = debtToCoverUSD / collateralPriceUSD × (1 + liquidationBonus) // liquidationBonus is set per reserve by the Configurator
Transaction Flow
// Liquidator calls: LiquidationEngine.liquidate( borrower: 0xBorrowerAddress, collateralAsset: 0x3600...0000, // USDC — asset to receive debtAsset: 0x89b5...d72a, // EURC — asset to repay debtToCover: 500_000 // 0.5 EURC (6 decimals) ) // Engine internally: // 1. Checks HF < 1e18 (position is liquidatable) // 2. Checks debtToCover ≤ 50% of outstanding debt // 3. Transfers debtToCover from liquidator to pool (repays debt) // 4. Burns borrower's dTokens proportionally // 5. Calculates collateralToSeize with bonus // 6. Transfers collateral from borrower's position to liquidator // 7. Updates borrower's userCollateral mapping // 8. Emits Liquidation event
Liquidation Dashboard
The Liquidation Dashboard at the bottom of the Lending page (visible when your wallet is connected) shows all positions with a health factor below 1.2. It is populated by:
- Scanning
Supplyevents from the last 50,000 blocks to discover all unique borrower addresses. - Calling
LiquidationEngine.getHealthFactor()for each address in a batched multicall. - Fetching full account data for addresses with HF < 1.2.
Each row shows the borrower address, their collateral amount and asset, their debt amount and asset, and their health factor badge. Positions with HF < 1.0 show a red Liquidate button. Clicking it:
- Approves the LiquidationEngine to spend your debt asset (if needed)
- Calls
liquidate()with 50% of the borrower's debt asdebtToCover - Shows a Done link to the transaction on ArcScan on success
Running a Liquidation Bot
For automated liquidation, you can monitor the pool off-chain and call the LiquidationEngine directly. A minimal bot pattern:
import { createPublicClient, createWalletClient, http } from 'viem'
import { arcTestnet } from './chains'
const publicClient = createPublicClient({ chain: arcTestnet, transport: http() })
// 1. Discover borrowers from Supply events
const logs = await publicClient.getLogs({
address: LENDING_POOL,
event: supplyEventAbi,
fromBlock: latestBlock - 50_000n,
})
const borrowers = [...new Set(logs.map(l => l.args.user))]
// 2. Check health factors in a single multicall
const hfResults = await publicClient.multicall({
contracts: borrowers.map(b => ({
address: LIQUIDATION_ENGINE,
abi: liquidationEngineAbi,
functionName: 'getHealthFactor',
args: [b],
})),
})
// 3. Liquidate any position with HF < 1e18
for (const [i, result] of hfResults.entries()) {
if (result.status === 'success' && result.result < 10n ** 18n) {
const borrower = borrowers[i]
// fetch account data, compute debtToCover, call liquidate()
}
}Protecting Your Position
To avoid being liquidated:
- Keep HF above 1.5 — this gives you a comfortable buffer against interest accrual and any oracle price movements.
- Repay regularly — even small repayments reset the interest clock and improve your HF.
- Supply more collateral — adding collateral directly increases your HF without requiring a repayment.
- Monitor the Borrow APY — high utilisation means high borrow rates, which means your debt grows faster. Consider repaying during high-rate periods.