# Understanding Aave V2 Code (5/n) - Liquidation Mechanism
## TLDR
- **When**: Health factor < 1.0 (the only trigger condition)
- **What**: Liquidators repay up to 50% of a borrower's debt and receive collateral + liquidation bonus (5-10%)
- **How**: Two settlement modes
- **aToken mode**: Receive interest-bearing tokens (always succeeds, no liquidity check needed)
- **Underlying mode**: Receive actual assets (requires pool has enough liquidity)
- **Why**: Protects protocol solvency while giving borrowers recovery opportunity through 50% close factor cap
## What's Liquidation in Aave V2
### Simple explanation
- Your collateral loses value or your debt grows too much
- Health factor drops below 1.0 (positoin underwater)
- Anyone can step in as a liquidator to repay part of your debt
- Liquidator gets your collateral + liquidation bonus (5-10%) as profit
- You lose collateral but debt is reduced, hopefully recovering HF to > 1.0
### Why liquidation exists
- **Protocol safety**: Prevent bad debt accumulation
- **Lender protection**: Ensure depositors can always withdraw their funds
- **System solvency**: Keep total collateral > total debt across protocol
- **Market efficiency**: Quick price discovery during volatile conditions
### What trigger liquidation?
- Health factor < 1.0 (the ONLY condition)
- Caused by
- Collateral price drops
- Debt price increases
- Interest accrual on debt (debt grows over time)
- User withdrawing collateral without repaying debt
### Who can liquidate?
- Anyone, it's permissionless
- Usually done by bots competing for profit
- Common liquidators: specialized MEV bots, keeper networks, whale traders
### What liquidators get?
- Collateral and liquidation bonus (typically 5-10% of debt repaid)
### What borrowers lose?
- Collateral seized, but debt is reduced by the amount liquidator paid
- Net loss is the liquidation bonus amount
## High-Level Flow
```
┌──────────────┐
│ Liquidator │
│ Initiates │
└──────┬───────┘
│ Want to repay borrower's USDC position
│ and get aETH or underlying ETH
┌───────────▼──────────────┐
│ Calculate Health Factor │
└───────────┬──────────────┘
│
┌──────▼──────┐
│ HF < 1.0? │
└──┬───────┬──┘
YES │ │ NO
┌───────▼ ▼────────┐
│ REJECT │
│ │
┌───────▼────────┐ │
│ Validate Call │ │
│ - Reserves OK? │ │
│ - Collateral? │ │
│ - Has debt? │ │
└───────┬────────┘ │
│ ALL CHECKS PASS │
┌───────▼────────────┐ │
│ Calculate Amounts │ │
│ - Max debt (50%) │ │
│ - Collateral needed│ │
│ - Check liquidity │ │
└───────┬────────────┘ │
│ │
┌───────▼────────────┐ │
│ Update State │ │
│ - Debt reserve │ │
│ - Accrue interest │ │
└───────┬────────────┘ │
│ │
┌───────▼────────────┐ │
│ Burn Debt Tokens │ │
│ - Variable/Stable │ │
│ - Update rates │ │
└───────┬────────────┘ │
│ │
│ │
┌───────────▼────────────┐ │
│ receiveAToken? │ │
└────┬──────────────┬────┘ │
TRUE │ │ FALSE │
┌────────▼────┐ ┌────▼─────────┐ │
│Transfer │ │Update State │ │
│aTokens │ │Burn aTokens │ │
│ │ │Withdraw ETH │ │
│ │ │Update rates │ │
└────────┬────┘ └────┬─────────┘ │
│ │ │
└──────┬───────┘ │
│ │
┌───────▼────────────┐ │
│ Liquidator Pays │ │
│ Transfer USDC │ │
│ to pool │ │
└───────┬────────────┘ │
│ │
┌───────▼────────────┐ │
│ Emit Event │ │
│ Return Success │ │
└────────────────────┘ │
│
┌─────────────────────────┘
│
┌───────▼────────┐
│ REJECTED │
└────────────────┘
```
## Data Structure
```solidity
struct LiquidationCallLocalVars {
uint256 userCollateralBalance; // user collateral's balance
uint256 userStableDebt; // user stable debt address
uint256 userVariableDebt; // user variable debt address
uint256 maxLiquidatableDebt; // max liquidatable debt amount (50% close factor limit)
uint256 actualDebtToLiquidate; // final debt amount to liquidate
uint256 liquidationRatio; // unused variable
uint256 maxAmountCollateralToLiquidate; // unused variable (moved to AvailableCollateralToLiquidateLocalVars)
uint256 userStableRate; // unused variable
uint256 maxCollateralToLiquidate; // max amount of collateral will be transferred to liquidators
uint256 debtAmountNeeded; // amount of debt can be covered by available collateral
uint256 healthFactor; // user whole positions' health factor (need to be < 1.0 to be liquidatable)
uint256 liquidatorPreviousATokenBalance; // liquidator's aToken balance before receiving liquidation collateral
IAToken collateralAtoken; // collateral aToken contract
bool isCollateralEnabled; // unused variable
DataTypes.InterestRateMode borrowRateMode; // unused variable
uint256 errorCode; // error code of validate liquidation call
string errorMsg; // error message of validate liquidation call
}
```
| Variable | Usage |
|----------|----------|
| healthFactor | Check if the position is liquidatable, used in `validateLiquidationCall` |
| userStableDebt | Stable rate debt, used in `getUserCurrentDebt` to calculate stable debt balance |
| userVariableDebt | Variable rate debt, used in `getUserCurrentDebt` to calculate variable debt balance |
| maxLiquidatableDebt | Cap on debt liquidation, calculated as `max liquidatable debt = (stable debt + variable debt) * close factor` |
| errorCode, errorMsg | Validation results, return value of `validateLiquidationCall` for early return if error |
| collateralAtoken | Collateral contract, used for transfer/ burn operations |
| userCollateralBalance | User's collateral balance, used as input in `_calculateAvailableCollateralToLiquidate` |
| maxCollateralToLiquidate | **Final collateral amount** to transfer to liquidators, return value of `_calculateAvailableCollateralToLiquidate`, used everywhere (transfer, burn, events) |
| debtAmountNeeded | Collateral constrainted debt amount to be repaid (may be less than requested), return value of `_calculateAvailableCollateralToLiquidate`, adjust `actualDebtToLiquidate` if needed |
| actualDebtToLiquidate | **Final debt amount** to liquidate, used everywhere (burn, transfer, events) |
| liquidatorPreviousATokenBalance | Auto-enable collateral, enable collateral for liquidators as 1st-time recipients |
## Math & Key Concepts
### Health Factor - The Liquidation Trigger
- The health factor determines if a position can be liquidated. It represents the safety margin of a borrowing position
- Formula = `Total collateral * Liquidation threshold / Total debt`
- Liquidation condition
- When HF < 1.0, then the position becomes liquidatable
### Liquidation Mechanics
#### Close Factor - Max Liquidation
- Rule: Max 50% of user's debt position can be liquidated in a single transaction
- Formula: `maxLiquidatableDebt = (userStableDebt + userVariableDebt) × LIQUIDATION_CLOSE_FACTOR_PERCENT`
- Why 50% cap?
- Give borrower chance to add collateral to recover the position
- Encourage multiple liquidators
- Prevent liquidation spirals: liquidator seizes the asset then sell, price decrease further, more positions become liquidatable
- Summary
| Aspect | Without Close Factor | With 50% Close Factor |
|------------------------|----------------------|-----------------------|
| Max liquidation | 100% of debt | 50% of debt |
| User protection | None | Strong |
| Recovery chance | No | Yes |
| Liquidator competition | Winner-takes-all | Multiple participants |
| Cascade risk | High | Lower |
| Protocol safety | Vulnerable | Balanced |
| User experience | Poor | Better |
#### Liquidation Bonus - Liquidator Incentive
- A percentage premium given to liquidators on top of the debt they repay.
- Example
- You repay $1,000 of someone's debt
- With a 5% liquidation bonus, you receive collateral worth $1,050
- Your profit: $50 (minus gas costs)
- Formula: `collateralToSeize = (debtRepaid × debtPrice / collateralPrice) × liquidationBonus`
- `liquidationBonus` is stored as basis points + 10,000
- eg. 10,500 = 105% = 5% bonus
- Why does it exists
- Incentivize liquidators: Liquidator can earn a profit
- Compensate for liquidators' risks (price volatility, gas costs, competition, slippage)
- Ensure protocol health: Fast liquidation prevent bad debt accumulation, keep the protocol solvent, and protect lenders from losses
- Aave v2 liquidation bonuses
- Stablecoins = 5% bonus
| Asset | Liquidation Bonus | Percentage |
|-------|-------------------|------------|
| DAI | 10500 | 5% |
| USDC | 10500 | 5% |
| TUSD | 10500 | 5% |
- Volatile assets = 10% bonus
| Asset | Liquidation Bonus | Percentage |
|-------|-------------------|------------|
| WETH | 10500 | 5% |
| WBTC | 11000 | 10% |
| AAVE | 11000 | 10% |
| LINK | 11000 | 10% |
| UNI | 11000 | 10% |
| MKR | 11000 | 10% |
- Summary
| Component | Value | Notes |
|-----------------|-------------------------------|------------------------------------|
| Stablecoins | 5% | Low risk assets |
| Volatile assets | 10% | High risk assets |
| Who pays? | User being liquidated | Penalty for undercollateralization |
| Who receives? | Liquidator | Profit + risk compensation |
| Purpose | Incentivize fast liquidations | Protect protocol from bad debt |
### Collateral Calculation - The Two-Way Check
#### Forward Calculation (Debt -> Collateral)
> "How much collateral do we need for this debt amount?"
- Formula:
- `maxAmountCollateralToLiquidate = (debtAssetPrice × debtToCover × liquidationBonus) / collateralPrice`
#### Reverse Calculation (Collateral -> Debt)
> "How much debt can this collateral cover?"
- Triggered when: `maxAmountCollateralToLiquidate` > `userCollateralBalance`
- Formula:
- `debtAmountNeeded = (collateralPrice × collateralAmount) / (debtAssetPrice × liquidationBonus)`
#### The Two-Way Check Logic
- Step 1: Forward calculation
- Want to liquidate: $50,000 debt
- Need 26.25 ETH
- Step 2: Check avaiability
- User has 10 ETH
- 26.25 ETH > 10 ETH -> Insufficient collateral
- Step 3: Reverse calculation
- Can only cover: $19,047 debt
- Will seize for liquidator: 10 ETH (all available)
- Step 4: Adjust actual liquidation amount
- actualDebtToLiquidate = min(50,000, 19,047) = $19,047
### Two Liquidation Model
#### Receive the aToken directly
- Mechanism
- Liquidator gets the interest-bearing aTokens
- Just transfer aTokens from user to liquidator
- No withdrawal from pool needed
- **No liquidity check required**
- Key properties
- **Always available**: aTokens exist as accounting entries, not physical assets
- **Instant execution**: No dependency on pool liquidity
- **Interest bearing**: Liquidator continues earning interest on received aTokens
- **Redemption flexibility**: Can withdraw underlying later when liquidity exists
#### Receive the underyling asset
- Liquidator gets actual underlying asset (eg. ETH, not aETH)
- Must BURN aTokens and WITHDRAW underlying asset from the pool
- **Reqire liquidity check**: Pool must have enough underlying asset
- The liquidity mismatch problem
- aToken contract balance sheet
```
┌─────────────────────────────────────────────────┐
│ ASSETS (What pool controls) │
│ ───────────────────────────────────────────── │
│ Physical Tokens: 20 ETH ← Available liquidity │
│ Loan Portfolio: 80 ETH ← Borrowed by users │
│ ───────────────────────────────────────────── │
│ Total Assets: 100 ETH │
│ │
│ LIABILITIES (What pool owes) │
│ ───────────────────────────────────────────── │
│ aToken Supply: 100 aETH ← Depositor claims │
│ Each aETH = claim on ~1 ETH worth │
└─────────────────────────────────────────────────┘
```
- Why liquidity check is required
```
Scenario: User has 30 aETH, needs to liquidate 25 ETH
Mode 1 Liquidator accepts aToken:
✅ Transfer 25 aETH to liquidator
- Just accounting: aToken[user] -= 25, aToken[liquidator] += 25
- Pool still has 20 ETH physical, 80 ETH borrowed
- No problem!
Mode 2 Liquidator accepts underlying asset:
❌ Send 25 ETH to liquidator
- Need to withdraw 25 ETH from pool
- Pool only has 20 ETH available
- INSUFFICIENT LIQUIDITY!
- Transaction must fail
```
#### Liquidation Mode Comparison: Pros & Cons
| **Criteria** | **Mode 1: Receive aToken** | **Mode 2: Receive Underlying** |
|--------------|---------------------------|-------------------------------|
| **Execution Reliability** | ✅ Guaranteed - never fails due to liquidity | ❌ Can fail if insufficient pool liquidity |
| **Gas Costs** | ✅ Lower - simple transfer operation | ❌ Higher - burn + withdraw + transfers |
| **Interest Earnings** | ✅ Keep earning - continue accruing interest | ❌ Stop earning - forgo future interest |
| **Scalability** | ✅ Unlimited - liquidate any amount regardless of utilization | ❌ Limited - can't exceed available liquidity |
| **Access Speed** | ❌ Delayed - must withdraw later (subject to liquidity) | ✅ Immediate - get underlying asset instantly |
| **Protocol Exposure** | ❌ Ongoing - holding aTokens = smart contract risk | ✅ None - done with Aave after liquidation |
| **Transaction Steps** | ❌ Two-step - need separate redemption transaction | ✅ One-step - direct usability, can sell/use immediately |
| **Amount Certainty** | ✅ Exact aToken amount guaranteed | ✅ Exact underlying amount (if succeeds) |
### How Aave v2 Handles Liquidity & Solvency Risk
- Solvency risk: Risk that total liabilities > total assets (bankruptcy risk)
- Liquidity risk: Risk of inability to meet short-term obligations despite being solvent
- How Aave V2 handles the risks
| Risk Type | TradFi | Aave V2 Individual User | Aave V2 Protocol |
|----------------|--------------------------|------------------------------------------|---------------------------------------------|
| **Solvency Risk** | Assets < Liabilities | Collateral × Threshold < Debt (HF < 1.0) | N/A |
| Trigger | Balance sheet underwater | Price crash, debt growth | N/A |
| Measure | Net Worth | Health Factor | N/A |
| Resolution | Bankruptcy, bail-in | Liquidation (restore HF) | N/A |
| **Liquidity Risk** | Can't meet withdrawals | N/A (user-level concept) | Can't honor aToken redemptions |
| Trigger | Bank run | N/A | High utilization + mass withdrawals |
| Measure | Cash ratio | N/A | Utilization rate |
| Resolution | Central bank loan | N/A | Interest rate spikes & Allow liquidator to accept aTokens|
## Step by Step Code Walkthrough
### Step 1: Entry point - `liquidationCall()`
```solidity
// LendingPool.sol
function liquidationCall(
address collateralAsset, // asset to seize from borrower
address debtAsset, // the borrowed asset to repay
address user, // the borrower being liquidated
uint256 debtToCover, // how much debt the liquidator wants to repay
bool receiveAToken // if true, receive aTokens; else receive underlying
) external override whenNotPaused {
// fetch the address of the LendingPoolCollateralManager contract
address collateralManager = _addressesProvider.getLendingPoolCollateralManager();
// use delegatecall to execute the liquidationCall in LendingPoolCollateralManager's context
//solium-disable-next-line
(bool success, bytes memory result) =
collateralManager.delegatecall(
abi.encodeWithSignature(
'liquidationCall(address,address,address,uint256,bool)',
collateralAsset,
debtAsset,
user,
debtToCover,
receiveAToken
)
);
// ensure the delegatecall succeed (didn't revert)
require(success, Errors.LP_LIQUIDATION_CALL_FAILED);
// decode the return values
(uint256 returnCode, string memory returnMessage) = abi.decode(result, (uint256, string));
// check the operation succeed, otherwise revert with error msg
require(returnCode == 0, string(abi.encodePacked(returnMessage)));
}
```
- **Key purpose**
- Liquidate an unhealthy position collateral-wise (HF < 1.0)
- Use `delegatecall` to bypass the 24kb contract size limit
### Step 2: Detailed Implementation
```solidity
// LendingPoolCollateralManager.sol
function liquidationCall(
address collateralAsset,
address debtAsset,
address user,
uint256 debtToCover,
bool receiveAToken
) external override returns (uint256, string memory) { // return success/ error code and msg
// load storage reference
// collateral reserve (the asset being seized)
DataTypes.ReserveData storage collateralReserve = _reserves[collateralAsset];
// debt reserve (the borrowed asset being repaid)
DataTypes.ReserveData storage debtReserve = _reserves[debtAsset];
// user configuration
DataTypes.UserConfigurationMap storage userConfig = _usersConfig[user];
// create struct to store local variables (avoid stack too deep)
LiquidationCallLocalVars memory vars;
// calculate user's health factor
(, , , , vars.healthFactor) = GenericLogic.calculateUserAccountData(
user,
_reserves,
userConfig,
_reservesList,
_reservesCount,
_addressesProvider.getPriceOracle()
);
// get user's current stable and variable debt balance
(vars.userStableDebt, vars.userVariableDebt) = Helpers.getUserCurrentDebt(user, debtReserve);
// ...
}
```
**Key Purpose**: Prepare for liquidation validation and debt & collateral amount calculation
### Step 3: Validation Liquidation Condition
```solidity
// return error code & message
(vars.errorCode, vars.errorMsg) = ValidationLogic.validateLiquidationCall(
collateralReserve, // collateral reserve (the asset being seized)
debtReserve, // debt reserve (the borrowed asset being repaid)
userConfig, // user config
vars.healthFactor, // user's health factor (calculated above)
vars.userStableDebt, // user's stable debt balance
vars.userVariableDebt // user's varaible debt balance
);
// check error code
if (Errors.CollateralManagerErrors(vars.errorCode) != Errors.CollateralManagerErrors.NO_ERROR) {
return (vars.errorCode, vars.errorMsg);
}
// ValidationLogic.sol
function validateLiquidationCall(
DataTypes.ReserveData storage collateralReserve,
DataTypes.ReserveData storage principalReserve,
DataTypes.UserConfigurationMap storage userConfig,
uint256 userHealthFactor,
uint256 userStableDebt,
uint256 userVariableDebt
) internal view returns (uint256, string memory) { // return error code & msg
if ( // both collateral & debt reserve need to be active, otherwise return error
!collateralReserve.configuration.getActive() || !principalReserve.configuration.getActive()
) {
return (
uint256(Errors.CollateralManagerErrors.NO_ACTIVE_RESERVE),
Errors.VL_NO_ACTIVE_RESERVE
);
}
// health factor needs to be < 1. otherwise return error
if (userHealthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD) {
return (
uint256(Errors.CollateralManagerErrors.HEALTH_FACTOR_ABOVE_THRESHOLD),
Errors.LPCM_HEALTH_FACTOR_NOT_BELOW_THRESHOLD
);
}
// Two collateral condition must be both true
bool isCollateralEnabled =
// reserve level: liq. threshold > 0
collateralReserve.configuration.getLiquidationThreshold() > 0 &&
// user level: user enabled this deposit as collateral
userConfig.isUsingAsCollateral(collateralReserve.id);
// if collateral isn't enabled as collateral by user, it cannot be liquidated
if (!isCollateralEnabled) {
return (
uint256(Errors.CollateralManagerErrors.COLLATERAL_CANNOT_BE_LIQUIDATED),
Errors.LPCM_COLLATERAL_CANNOT_BE_LIQUIDATED
);
}
// user must have debt in principal asset
if (userStableDebt == 0 && userVariableDebt == 0) {
return (
uint256(Errors.CollateralManagerErrors.CURRRENCY_NOT_BORROWED),
Errors.LPCM_SPECIFIED_CURRENCY_NOT_BORROWED_BY_USER
);
}
// all check passes, liquidation is valid
return (uint256(Errors.CollateralManagerErrors.NO_ERROR), Errors.LPCM_NO_ERRORS);
}
```
**Key purpose**: Validation the liquidation is allowed
- HF is below 1.0
- User has debt in the debtAsset
- Both collateral & debt reserves needs to be active
- Collateral reserve inactive: Can't safely transfer collateral to liquidator (paused tokens, bad oracle etc.)
- Debt reserve inactive: Can't safely accept debt repayment (depegged, broken oracle etc.)
- Collateral must be enabled
- Only assets actively used as collateral can be seized, on both reserve and user config level
- If validation fails, return early with error
### Step 4: Calculate Max Liquidatable Debt & Actual Debt To Liquidate
```solidity
// max liquidatable debt = (stable debt + variable debt) * close factor
// LIQUIDATION_CLOSE_FACTOR_PERCENT = 5000 = 50%
vars.maxLiquidatableDebt = vars.userStableDebt.add(vars.userVariableDebt).percentMul(
LIQUIDATION_CLOSE_FACTOR_PERCENT
);
// determine the actual debt to liquidate
// if debtToCover > maxLiquidatableDebt, then actualDebtToLiquidate = maxLiquidatableDebt
// otherwise, actualDebtToLiquidate = debtToCover
vars.actualDebtToLiquidate = debtToCover > vars.maxLiquidatableDebt
? vars.maxLiquidatableDebt
: debtToCover;
```
**Key purpose**
- From the debt side, calculate the max amount that is allowed to liquidate
- Formula: `min( maxLiquidatableDebt, debtToCover )`
### Step 5: Determine How Much Collateral Can be Seized & Corresponding Debt To Be Repaid
```solidity
// return max collateral to liquidate, and actual debt amount supported by the collateral level
(
vars.maxCollateralToLiquidate,
vars.debtAmountNeeded
) = _calculateAvailableCollateralToLiquidate(
collateralReserve,
debtReserve,
collateralAsset,
debtAsset,
vars.actualDebtToLiquidate,
vars.userCollateralBalance
);
// if there isn't enough collateral to cover the actual debt to liquidate,
// downsize the debt amount to be repaid to smaller amount
if (vars.debtAmountNeeded < vars.actualDebtToLiquidate) {
vars.actualDebtToLiquidate = vars.debtAmountNeeded;
}
// LendingPoolCollateralManager.sol
function _calculateAvailableCollateralToLiquidate(
DataTypes.ReserveData storage collateralReserve,
DataTypes.ReserveData storage debtReserve,
address collateralAsset,
address debtAsset,
uint256 debtToCover,
uint256 userCollateralBalance
) internal view returns (uint256, uint256) { // return collateral amount and debt amount needed
uint256 collateralAmount = 0;
uint256 debtAmountNeeded = 0;
// get the price oracle
IPriceOracleGetter oracle = IPriceOracleGetter(_addressesProvider.getPriceOracle());
AvailableCollateralToLiquidateLocalVars memory vars;
// get the price of the collateral asset
vars.collateralPrice = oracle.getAssetPrice(collateralAsset);
// get the price of the debt asset
vars.debtAssetPrice = oracle.getAssetPrice(debtAsset);
// get the liquidation bonus and collateral decimals
(, , vars.liquidationBonus, vars.collateralDecimals, ) = collateralReserve
.configuration
.getParams();
vars.debtAssetDecimals = debtReserve.configuration.getDecimals();
// This is the maximum possible amount of the selected collateral that can be liquidated, given the
// max amount of liquidatable debt
// max collateral to liquidate = (debt to cover * debt price * liq. bonus / collateral price
vars.maxAmountCollateralToLiquidate = vars
.debtAssetPrice
.mul(debtToCover)
.mul(10**vars.collateralDecimals)
.percentMul(vars.liquidationBonus)
.div(vars.collateralPrice.mul(10**vars.debtAssetDecimals));
// user doesn't have enough collateral
if (vars.maxAmountCollateralToLiquidate > userCollateralBalance) {
// take the max of user collateral balance
collateralAmount = userCollateralBalance;
// reverse calculate the debt amount (less than debtToCover)
// debt amount needed = (collateral amount * collateral price) / (debt price * liq. bonus)
debtAmountNeeded = vars
.collateralPrice
.mul(collateralAmount)
.mul(10**vars.debtAssetDecimals)
.div(vars.debtAssetPrice.mul(10**vars.collateralDecimals))
.percentDiv(vars.liquidationBonus);
} else {
// user has enough collateral
collateralAmount = vars.maxAmountCollateralToLiquidate; // calculated collateral needed
debtAmountNeeded = debtToCover; // same as input
}
return (collateralAmount, debtAmountNeeded);
}
```
- **Key purpose**
- From the collateral side, calculate the amount of debt can be repaid, and the amount of collateral can be transferred to the liquidator
- **Why need both**
- `maxCollateralToLiquidate`: How much collateral to transfer to the liquidator
- `debtAmountNeeded`: How much debt actually can be repaid
- **Math relationship**
- Forward: collateralAmount = (debtAmountNeeded × debtPrice × bonus) / collateralPrice
- Reverse: debtAmountNeeded = (collateralAmount × collateralPrice) / (debtPrice × bonus)
- **Example**
```
User Position:
- Collateral: 1 ETH @ $2,000 = $2,000
- Debt: $10,000 USDC
- Liquidation bonus: 5%
Liquidator wants to liquidate: $5,000 USDC
Step 1 Forward Calculation (Debt -> Collateral)
maxAmountCollateralToLiquidate = (debtPrice × debtToCover × bonus) / collateralPrice
= ($1 × $5,000 × 1.05) / $2,000
= 2.625 ETH needed
Step 2 Check Collateral Availability
if (2.625 ETH > userCollateralBalance(1 ETH)) {
// User doesn't have enough!
// Branch A: Insufficient collateral
}
Step 3 Reverse Calculate (Collateral -> Debt)
debtAmountNeeded = (collateralPrice × collateralAmount) / (debtPrice × bonus)
= ($2,000 × 1 ETH) / ($1 × 1.05)
= $1,904.76 // LESS than $5,000
Step 4 Return value
return (
maxCollateralToLiquidate: 1 ETH, // How much collateral to give liquidator
debtAmountNeeded: $1,904.76 // How much debt can actually be repaid
);
```
### Step 6: If Liquidators Want Underlying Asset -> Liquidity Check
```solidity
// if liquidators want to receive the underlying asset directly (not aToken)
if (!receiveAToken) {
// get the current balance of underlying asset in the aToken contract
uint256 currentAvailableCollateral =
IERC20(collateralAsset).balanceOf(address(vars.collateralAtoken));
// if there is not enough underlying asset, return error code & msg
if (currentAvailableCollateral < vars.maxCollateralToLiquidate) {
return (
uint256(Errors.CollateralManagerErrors.NOT_ENOUGH_LIQUIDITY),
Errors.LPCM_NOT_ENOUGH_LIQUIDITY_TO_LIQUIDATE
);
}
}
```
**Key purpose**: Check if the aToken contract have enough underlying asset to send to the liquidator
### Step 7: Update Debt Reserve's State
```solidity
// update debt reserve's variable borrow index, liquidity index, mint treasury fees, and update timestamp
debtReserve.updateState();
```
**Key purpose**: Update debt reserve state with accrued interest before any state-changing operation (burn debt tokens, update collateral, transfer assets)
### Step 8: Burn Debt Tokens
```solidity
// if user has enough variable debt, burn all the debt from variable debt token
if (vars.userVariableDebt >= vars.actualDebtToLiquidate) {
IVariableDebtToken(debtReserve.variableDebtTokenAddress).burn(
user,
vars.actualDebtToLiquidate, // final debt to repay
debtReserve.variableBorrowIndex // variable borrow index
);
} else {
// If the user doesn't have variable debt, burn all variable debt token, then the remaining from stable debt token
if (vars.userVariableDebt > 0) {
IVariableDebtToken(debtReserve.variableDebtTokenAddress).burn(
user,
vars.userVariableDebt,
debtReserve.variableBorrowIndex
);
}
IStableDebtToken(debtReserve.stableDebtTokenAddress).burn(
user,
vars.actualDebtToLiquidate.sub(vars.userVariableDebt)
);
}
```
**Key purpose**: Burn the amount of repaid debt token from variable and/or stable debt contract
### Step 9: Update Interest Rates For Debt Reserve
```solidity
debtReserve.updateInterestRates(
debtAsset, // debt reserve address
debtReserve.aTokenAddress, // debt reserve aToken address
vars.actualDebtToLiquidate, // liquidity added
0 // liquidity taken
);
```
**Key purpose**: As debt being repaid, liquidity increases, borrow rates & liquidity rate decrease
### Step 9: Transfer aTokens Or Underlying Asset To Liquidators
```solidity
// if liquidator wants aTokens
if (receiveAToken) {
// get liquidator's previous aToken balance
vars.liquidatorPreviousATokenBalance = IERC20(vars.collateralAtoken).balanceOf(msg.sender);
// transfer aTokens from user to the liquidator
vars.collateralAtoken.transferOnLiquidation(
user, // from user
msg.sender, // to liquidator
vars.maxCollateralToLiquidate); // final amount of aTokens
// if it's liquidator's 1st aTokens of this asset
if (vars.liquidatorPreviousATokenBalance == 0) {
// enable the asset as collateral for the liquidator automatically
DataTypes.UserConfigurationMap storage liquidatorConfig = _usersConfig[msg.sender];
liquidatorConfig.setUsingAsCollateral(collateralReserve.id, true);
emit ReserveUsedAsCollateralEnabled(collateralAsset, msg.sender);
}
}
// if liquidator wants underlying asset directly
else {
// update collateral reserve's indexes
collateralReserve.updateState();
// update collateral reserve's borrow rates & liquidity rate
collateralReserve.updateInterestRates(
collateralAsset, // reserve address
address(vars.collateralAtoken), // aToken address
0, // liquidity added
vars.maxCollateralToLiquidate // liquidity taken
);
// Burn the equivalent amount of aToken, sending the underlying to the liquidator
vars.collateralAtoken.burn(
user, // user
msg.sender, // receiver of the underlying
vars.maxCollateralToLiquidate, // amount
collateralReserve.liquidityIndex // index
);
}
// AToken.sol
function burn(
address user,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external override onlyLendingPool {
// calculated scaled amount using index
uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.CT_INVALID_BURN_AMOUNT);
// burn scaled amount
_burn(user, amountScaled);
// transfer the actual amount to the receiver
IERC20(_underlyingAsset).safeTransfer(receiverOfUnderlying, amount);
// emit events
emit Transfer(user, address(0), amount);
emit Burn(user, receiverOfUnderlying, amount, index);
}
```
- **Key purpose**
- Transfer seized collateral to liquidators in aTokens or underlying asset
- **Key operation**
- Model 1: Receive aTokens
- Transfer aTokens from user to liquidator
- Auto enable the asset as collateral for liquidator if it's 1st time receiving the aToken
- No pool state changes -> No updates needed
- Model 2: Receive underlying
- Update collateral reserve state (accrue interest to current time)
- Update interest rates (recalculate as liquidity decreases due to collateral withdraw)
- Burn aTokens and send underlying asset to liquidator
### Step 10: Cleanup
```solidity
// if all collateral was seized, disable this asset as collateral for the user
if (vars.maxCollateralToLiquidate == vars.userCollateralBalance) {
userConfig.setUsingAsCollateral(collateralReserve.id, false);
emit ReserveUsedAsCollateralDisabled(collateralAsset, user);
}
```
**Key purpose**: Disable the asset as collateral if all of it is seized by the liquidator
### Step 11: Liquidator Repay the Debt
```solidity
// liquidator transfer the debt amount to the reserve pool, repaying borrower's debt
IERC20(debtAsset).safeTransferFrom(
msg.sender, // from liquidator
debtReserve.aTokenAddress, // to debt reserve aToken address
vars.actualDebtToLiquidate // final debt amount to liquidate
);
```
**Key purpose**: Liquidator transfer the final debt amount to the protocol's reserve pool, repaying borrower's position
### Step 12: Emit Event
```solidity
emit LiquidationCall(
collateralAsset,
debtAsset,
user,
vars.actualDebtToLiquidate,
vars.maxCollateralToLiquidate,
msg.sender,
receiveAToken
);
```
## Key Insights
### 1. Portfolio-Level Health, Not Asset-Level
Aave liquidates entire portfolios, not individual positions:
- **Health factor is global**: Calculated across ALL collateral and ALL debt, not per asset pair
- **Cross-collateral protection**: Your USDC deposit helps protect your ETH collateral because they're aggregated in HF calculation
- **Liquidation flexibility**: Once HF < 1.0, liquidators can repay ANY debt and seize ANY collateral (subject to 50% cap)
- **No cherry-picking healthy positions**: If your portfolio HF ≥ 1.0, no liquidation is possible even if individual pairs look underwater
### 2. Atomic Execution Eliminates Trust Assumptions
Every liquidation is all-or-nothing:
- **No partial states**: Either the full transaction succeeds (debt burned + collateral transferred + payment received) or everything reverts
- **Trust-minimized**: Liquidators can't take collateral without paying; users can't have debt reduced without liquidators receiving collateral
- **Protocol safety**: Impossible to create bad debt through failed liquidations
### 3. Dual Settlement Modes Decouple Solvency from Liquidity
The aToken settlement option is Aave's killer feature for crisis resilience:
**Why it matters:**
- **Solvency enforcement persists during bank runs**: Liquidations execute even when 99% of pool assets are borrowed
- **Liquidity risk becomes optional**: Liquidators voluntarily absorb redemption timing by accepting aTokens instead of forcing pool withdrawals
- **Liquidations become balance-sheet operations**: No immediate market selling required—collateral changes hands as accounting entries first
- **MEV competition without fragility**: Liquidator bots compete on speed without depending on AMM liquidity or forcing cascading sell pressure
**The trade-off:**
- aToken mode: Guaranteed execution, ongoing yield, deferred liquidity access
- Underlying mode: Immediate exit, no smart contract exposure, but can fail if pool is illiquid
This design ensures that **undercollateralized positions get liquidated even when the protocol faces liquidity constraints**, preventing the death spiral seen in other lending protocols during market stress.
## Reference
- Aave v2 whitepaper: https://github.com/aave/protocol-v2/blob/master/aave-v2-whitepaper.pdf
- Aave v2 repo: https://github.com/aave/protocol-v2
## Discussion
- Twitter: [@chloe_zhuX](https://x.com/Chloe_zhuX)
- Telegram: [@Chloe_zhu](https://t.me/chloe_zhu)
- GitHub: [@Chloezhu010](https://github.com/Chloezhu010)
---
*Last updated: Dec 24th, 2025*
*Part of my #LearnInPublic Defi series*