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