End-to-end execution flow for liquidating unhealthy positions in Aave V3.
| Aspect | Details |
|---|---|
| Entry Point | Pool.liquidationCall(collateralAsset, debtAsset, user, debtToCover, receiveAToken) |
| Key Transformations | Debt Value → Collateral Amount |
| State Changes | Burn debt, burn/transfer collateral |
| Events Emitted | LiquidationCall, ReserveUsedAsCollateralEnabled (conditional) |
flowchart TD
%% Styling definitions
classDef validation fill:#ffcccc,stroke:#ff0000,stroke-width:2px
classDef transformation fill:#ccffcc,stroke:#00aa00,stroke-width:2px
classDef storage fill:#ccccff,stroke:#0000ff,stroke-width:2px
classDef event fill:#ffffcc,stroke:#aaaa00,stroke-width:2px
classDef error fill:#ff0000,stroke:#000000,color:#fff
classDef critical stroke:#ff0000,stroke-width:3px
%% Entry point
Entry["Pool.liquidationCall<br/>collateralAsset, debtAsset,<br/>user, debtToCover,<br/>receiveAToken"] --> Execute["LiquidationLogic<br/>executeLiquidationCall"]
subgraph StateUpdate ["1. State Updates"]
direction TB
UpdateState["ReserveLogic<br/>updateState (debt)<br/>Updates indexes"]
end
subgraph HealthCheck ["2. Health Factor / Safety Checks"]
direction TB
CalcAccount["GenericLogic<br/>calculateUserAccountData<br/>Calculates HF, totalDebt"] --> Validate["ValidationLogic<br/>validateLiquidationCall<br/>CRITICAL: HF < 1.0"]
class Validate validation
class Validate critical
end
subgraph Calculations ["3. Liquidation Calculations"]
direction TB
Validate --> CalcDebt["_calculateDebt<br/>Gets variable + stable debt"]
CalcDebt --> CalcConfig["_getConfigurationData<br/>closeFactor: HF<0.95=100%<br/>HF>=0.95=50%"]
CalcConfig --> CalcCollateral["_calculateAvailableCollateralToLiquidate<br/>KEY: collateralAmount =<br/>debtToCover.percentMul(bonus)<br/>.wadToRay().rayDiv(price)"]
class CalcCollateral transformation
end
Execute --> StateUpdate
StateUpdate --> HealthCheck
HealthCheck --> Calculations
CalcCollateral --> BurnDebt["_burnDebtTokens<br/>CRITICAL: Burns debt"]
subgraph TokenBurn ["4. Debt Token Burn"]
direction TB
BurnDebt --> VarDebtBurn{"Variable Debt<br/>>= debtToCover?"}
VarDebtBurn -->|Yes| VDBurn["VariableDebtToken<br/>burn"]
VarDebtBurn -->|No| VarStableBurn["Burn all variable +<br/>partial stable"]
VarStableBurn --> VDMint["VariableDebtToken<br/>burn(userVariableDebt)"]
VDMint --> SDMint["StableDebtToken<br/>burn(remaining)"]
VDBurn --> IsoLogic
SDMint --> IsoLogic
end
subgraph CollateralMgmt ["5. Collateral Management"]
direction TB
IsoLogic["IsolationModeLogic<br/>updateIsolatedDebtIfIsolated"]
class IsoLogic storage
IsoLogic --> UpdateRates["ReserveLogic<br/>updateInterestRates"]
UpdateRates --> ATokensCheck{"receiveAToken?"}
ATokensCheck -->|Yes| LiquidateATokens["_liquidateATokens<br/>Transfer aTokens"]
ATokensCheck -->|No| BurnCollateral["_burnCollateralATokens<br/>Burn aTokens + transfer"]
LiquidateATokens --> CollatValidate["ValidationLogic<br/>validateUseAsCollateral"]
class CollatValidate validation
CollatValidate --> EnableCollat["UserConfig<br/>setUsingAsCollateral"]
class EnableCollat storage
EnableCollat --> CollatEvent["EMIT<br/>ReserveUsedAsCollateralEnabled"]
class CollatEvent event
BurnCollateral --> BurnAToken["AToken<br/>burn"]
class BurnAToken transformation
BurnAToken --> TransferUnderlying["IERC20<br/>safeTransfer"]
class TransferUnderlying storage
end
TokenBurn --> CollateralMgmt
CollatEvent --> RepayTransfer["IERC20<br/>safeTransferFrom<br/>Liquidator pays debt"]
class RepayTransfer storage
TransferUnderlying --> RepayTransfer
RepayTransfer --> FinalEvent["EMIT<br/>LiquidationCall"]
class FinalEvent event
%% Error annotations
%% CRITICAL: HF must be < 1.0 - validation reverts otherwise
%% CRITICAL: Collateral calculation applies liquidation bonus
%% CRITICAL: Debt token burn is irreversible
%% CRITICAL: Close factor limits liquidation amount
%% Link styles for critical paths
linkStyle 10 stroke:#ff0000,stroke-width:3px
linkStyle 16 stroke:#ff0000,stroke-width:3px
linkStyle 22 stroke:#ff0000,stroke-width:3px
File: contracts/protocol/pool/Pool.sol
function liquidationCall(
address collateralAsset,
address debtAsset,
address user,
uint256 debtToCover,
bool receiveAToken
) external virtual override {
LiquidationLogic.executeLiquidationCall(
_reserves,
_reservesList,
_usersConfig[user],
_eModeCategories,
DataTypes.ExecuteLiquidationCallParams({
collateralAsset: collateralAsset,
debtAsset: debtAsset,
user: user,
debtToCover: debtToCover,
receiveAToken: receiveAToken,
priceOracle: ADDRESSES_PROVIDER.getPriceOracle(),
userEModeCategory: _usersEModeCategory[user],
priceOracleSentinel: ADDRESSES_PROVIDER.getPriceOracleSentinel()
})
);
}File: contracts/protocol/libraries/logic/LiquidationLogic.sol
function executeLiquidationCall(
mapping(address => DataTypes.ReserveData) storage reserves,
mapping(uint256 => address) storage reservesList,
DataTypes.UserConfigurationMap storage userConfig,
mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories,
DataTypes.ExecuteLiquidationCallParams memory params
) external {
LiquidationCallLocalVars memory vars;
// Get debt reserve data
DataTypes.ReserveData storage debtReserve = reserves[params.debtAsset];
DataTypes.ReserveCache memory debtReserveCache = debtReserve.cache();
// Update debt reserve state
debtReserve.updateState(debtReserveCache);
// Calculate user account data
(
vars.totalCollateralInBaseCurrency,
vars.totalDebtInBaseCurrency,
vars.avgLtv,
vars.avgLiquidationThreshold,
vars.healthFactor,
vars.hasZeroLtvCollateral
) = GenericLogic.calculateUserAccountData(
reserves,
reservesList,
eModeCategories,
DataTypes.CalculateUserAccountDataParams({
userConfig: userConfig,
reservesCount: params.reservesCount,
user: params.user,
oracle: params.oracle,
userEModeCategory: params.userEModeCategory
})
);
// Validate liquidation
ValidationLogic.validateLiquidationCall(
debtReserveCache,
params.debtAsset,
params.user,
vars.healthFactor,
vars.totalDebtInBaseCurrency,
params.priceOracleSentinel
);
// Get user's debt
vars.userVariableDebt = IERC20(debtReserveCache.variableDebtTokenAddress)
.balanceOf(params.user);
vars.userStableDebt = IERC20(debtReserveCache.stableDebtTokenAddress)
.balanceOf(params.user);
vars.userTotalDebt = vars.userVariableDebt + vars.userStableDebt;
// Get collateral reserve data
DataTypes.ReserveData storage collateralReserve = reserves[params.collateralAsset];
DataTypes.ReserveCache memory collateralReserveCache = collateralReserve.cache();
// Get user's collateral balance
vars.userCollateralBalance = IERC20(collateralReserveCache.aTokenAddress)
.balanceOf(params.user);
// Determine close factor
vars.closeFactor = vars.healthFactor > CLOSE_FACTOR_HF_THRESHOLD
? DEFAULT_LIQUIDATION_CLOSE_FACTOR
: MAX_LIQUIDATION_CLOSE_FACTOR;
// Calculate actual debt to liquidate
vars.actualDebtToLiquidate = params.debtToCover >
vars.userTotalDebt.percentMul(vars.closeFactor)
? vars.userTotalDebt.percentMul(vars.closeFactor)
: params.debtToCover;
// Calculate available collateral to liquidate
(
vars.actualDebtToLiquidate,
vars.actualCollateralToLiquidate,
vars.liquidationProtocolFeeAmount
) = _calculateAvailableCollateralToLiquidate(
collateralReserve,
debtReserve,
collateralReserveCache,
debtReserveCache,
collateralAssetPrice,
debtAssetPrice,
vars.actualDebtToLiquidate,
vars.userCollateralBalance,
vars.liquidationBonus
);
// Burn debt tokens
_burnDebtTokens(
debtReserve,
params.debtAsset,
params.user,
vars.actualDebtToLiquidate,
vars.userVariableDebt,
vars.userStableDebt
);
// Update isolation mode debt if applicable
IsolationModeLogic.updateIsolatedDebtIfIsolated(
reserves,
reservesList,
userConfig,
debtReserveCache,
vars.actualDebtToLiquidate
);
// Update debt reserve interest rates
debtReserve.updateInterestRates(
debtReserveCache,
params.debtAsset,
0,
vars.actualDebtToLiquidate
);
// Handle collateral
if (params.receiveAToken) {
_liquidateATokens(
reserves,
reservesList,
userConfig,
collateralReserve,
collateralReserveCache,
params,
vars
);
} else {
_burnCollateralATokens(
collateralReserve,
collateralReserveCache,
params,
vars
);
}
// Transfer debt repayment from liquidator
IERC20(params.debtAsset).safeTransferFrom(
msg.sender,
debtReserveCache.aTokenAddress,
vars.actualDebtToLiquidate
);
emit LiquidationCall(
params.collateralAsset,
params.debtAsset,
params.user,
vars.actualDebtToLiquidate,
vars.actualCollateralToLiquidate,
msg.sender,
params.receiveAToken
);
}File: contracts/protocol/libraries/logic/LiquidationLogic.sol
function _calculateAvailableCollateralToLiquidate(
DataTypes.ReserveData storage collateralReserve,
DataTypes.ReserveData storage debtReserve,
DataTypes.ReserveCache memory collateralReserveCache,
DataTypes.ReserveCache memory debtReserveCache,
uint256 collateralAssetPrice,
uint256 debtAssetPrice,
uint256 debtToCover,
uint256 userCollateralBalance,
uint256 liquidationBonus
) internal view returns (uint256, uint256, uint256) {
// Calculate collateral amount equivalent to debt
uint256 collateralAmount = debtToCover
.percentMul(PercentageMath.PERCENTAGE_FACTOR + liquidationBonus) // Add bonus
.wadToRay()
.rayDiv(collateralAssetPrice);
// Cap at user's collateral balance
uint256 maxCollateralToLiquidate = userCollateralBalance.rayMul(
collateralReserveCache.liquidityIndex
);
if (collateralAmount > maxCollateralToLiquidate) {
// Recalculate debt to cover with capped collateral
collateralAmount = maxCollateralToLiquidate;
debtToCover = collateralAmount
.rayMul(collateralAssetPrice)
.rayToWad()
.percentDiv(PercentageMath.PERCENTAGE_FACTOR + liquidationBonus);
}
// Calculate protocol fee on liquidation bonus
uint256 liquidationProtocolFee = collateralReserveCache
.reserveConfiguration
.getLiquidationProtocolFee();
uint256 liquidationProtocolFeeAmount;
if (liquidationProtocolFee != 0) {
uint256 bonusCollateral = collateralAmount -
debtToCover.wadToRay().rayMul(debtAssetPrice).rayToWad();
liquidationProtocolFeeAmount = bonusCollateral.percentMul(
liquidationProtocolFee
);
}
return (
debtToCover,
collateralAmount - liquidationProtocolFeeAmount,
liquidationProtocolFeeAmount
);
}[TRANSFORMATION]: See Liquidation Calculations for detailed formula breakdown
File: contracts/protocol/libraries/logic/LiquidationLogic.sol
function _burnDebtTokens(
DataTypes.ReserveData storage debtReserve,
address debtAsset,
address user,
uint256 debtToCover,
uint256 userVariableDebt,
uint256 userStableDebt
) internal {
if (userVariableDebt >= debtToCover) {
// Burn only variable debt
IVariableDebtToken(debtReserveCache.variableDebtTokenAddress).burn(
user,
debtToCover,
debtReserveCache.nextVariableBorrowIndex
);
} else {
// Burn all variable debt + some stable debt
if (userVariableDebt != 0) {
IVariableDebtToken(debtReserveCache.variableDebtTokenAddress).burn(
user,
userVariableDebt,
debtReserveCache.nextVariableBorrowIndex
);
}
uint256 stableDebtToBurn = debtToCover - userVariableDebt;
if (stableDebtToBurn != 0) {
IStableDebtToken(debtReserveCache.stableDebtTokenAddress).burn(
user,
stableDebtToBurn
);
}
}
}File: contracts/protocol/libraries/logic/LiquidationLogic.sol
function _liquidateATokens(
mapping(address => DataTypes.ReserveData) storage reserves,
mapping(uint256 => address) storage reservesList,
DataTypes.UserConfigurationMap storage userConfig,
DataTypes.ReserveData storage collateralReserve,
DataTypes.ReserveCache memory collateralReserveCache,
DataTypes.ExecuteLiquidationCallParams memory params,
LiquidationCallLocalVars memory vars
) internal {
// Transfer aTokens from user to liquidator
IAToken(collateralReserveCache.aTokenAddress).transferOnLiquidation(
params.user,
msg.sender,
vars.actualCollateralToLiquidate
);
// Check if liquidator can use collateral
if (!userConfig.isUsingAsCollateral(collateralReserve.id)) {
bool canUseAsCollateral = ValidationLogic.validateUseAsCollateral(
reserves,
reservesList,
collateralReserveCache
);
if (canUseAsCollateral) {
userConfig.setUsingAsCollateral(collateralReserve.id, true);
emit ReserveUsedAsCollateralEnabled(
params.collateralAsset,
msg.sender
);
}
}
}File: contracts/protocol/libraries/logic/LiquidationLogic.sol
function _burnCollateralATokens(
DataTypes.ReserveData storage collateralReserve,
DataTypes.ReserveCache memory collateralReserveCache,
DataTypes.ExecuteLiquidationCallParams memory params,
LiquidationCallLocalVars memory vars
) internal {
// Update collateral reserve state
collateralReserve.updateState(collateralReserveCache);
// Burn user's collateral aTokens
IAToken(collateralReserveCache.aTokenAddress).burn(
params.user,
msg.sender,
vars.actualCollateralToLiquidate,
collateralReserveCache.nextLiquidityIndex
);
// Transfer underlying to liquidator
IERC20(params.collateralAsset).safeTransfer(
msg.sender,
vars.actualCollateralToLiquidate
);
}File: contracts/protocol/libraries/logic/ValidationLogic.sol
function validateLiquidationCall(
DataTypes.ReserveCache memory debtReserveCache,
address debtAsset,
address user,
uint256 healthFactor,
uint256 totalDebtInBaseCurrency,
address priceOracleSentinel
) internal view {
require(
healthFactor < HEALTH_FACTOR_LIQUIDATION_THRESHOLD,
Errors.HEALTH_FACTOR_NOT_BELOW_THRESHOLD
);
require(totalDebtInBaseCurrency != 0, Errors.NO_DEBT);
// Check oracle sentinel if configured
if (priceOracleSentinel != address(0)) {
require(
IPriceOracleSentinel(priceOracleSentinel).isLiquidationAllowed(),
Errors.PRICE_ORACLE_SENTINEL_CHECK_FAILED
);
}
}debtToCover (in debt Asset decimals)
|
v
// Get prices
collateralPrice = oracle.getAssetPrice(collateralAsset)
debtPrice = oracle.getAssetPrice(debtAsset)
|
v
// Calculate collateral with bonus
collateralAmount = debtToCover
.percentMul(100% + liquidationBonus) // Add liquidation incentive
.wadToRay() // Convert to RAY precision
.rayDiv(collateralPrice) // Divide by collateral price
|
v
// Check against available collateral
maxCollateral = userCollateralBalance.rayMul(liquidityIndex)
if (collateralAmount > maxCollateral):
collateralAmount = maxCollateral
debtToCover = recalculate(collateralAmount) // Work backwards
|
v
// Calculate protocol fee on bonus only
bonusCollateral = collateralAmount - debtValueInCollateral
protocolFee = bonusCollateral.percentMul(liquidationProtocolFee)
|
v
liquidatorReceives = collateralAmount - protocolFee
debtToRepay = recalculatedDebtToCover
User position:
- Debt: 1000 USDC
- Collateral: 1 ETH
- ETH price: $1500
- Liquidation threshold: 80%
- HF = (1 ETH * $1500 * 80%) / $1000 = 1.2 (healthy)
... price drops ...
New ETH price: $1100
New HF = (1 ETH * $1100 * 80%) / $1000 = 0.88 (unhealthy!)
Liquidation:
- debtToCover = 1000 USDC * 50% = 500 USDC (close factor)
- liquidationBonus = 8%
- collateralAmount = 500 USDC * 1.08 / $1100 per ETH
= 0.4909 ETH
- protocolFee = 10% of bonus = 10% * (0.4909 - 0.4545) ETH
= 0.00364 ETH
- liquidator receives = 0.4909 - 0.00364 = 0.4873 ETH
Key Points:
- Liquidation bonus incentivizes liquidators
- Protocol fee taken from bonus portion only
- Close factor limits how much can be liquidated at once
- HF < 0.95: 50% close factor, HF >= 0.95: 100% close factor
event LiquidationCall(
address indexed collateralAsset, // Collateral being seized
address indexed debtAsset, // Debt being repaid
address indexed user, // User being liquidated
uint256 debtToCover, // Amount of debt repaid
uint256 liquidatedCollateralAmount, // Amount of collateral seized
address liquidator, // msg.sender
bool receiveAToken // True if liquidator got aTokens
);Emitted when liquidator receives aTokens and enables collateral.
event ReserveUsedAsCollateralEnabled(
address indexed reserve,
address indexed user
);| Error | Condition | File |
|---|---|---|
HEALTH_FACTOR_NOT_BELOW_THRESHOLD |
healthFactor >= 1.0 |
ValidationLogic.sol |
NO_DEBT |
User has no debt to liquidate | ValidationLogic.sol |
PRICE_ORACLE_SENTINEL_CHECK_FAILED |
Sentinel disallows liquidation | ValidationLogic.sol |
- Borrow Flow - How debt is created
- Supply Flow - How collateral is deposited
- Health Factor Calculation
contracts/protocol/pool/Pool.sol
contracts/protocol/libraries/logic/LiquidationLogic.sol
contracts/protocol/libraries/logic/ValidationLogic.sol
contracts/protocol/libraries/logic/GenericLogic.sol
contracts/protocol/libraries/logic/IsolationModeLogic.sol
contracts/protocol/tokenization/AToken.sol
contracts/protocol/tokenization/VariableDebtToken.sol
contracts/protocol/tokenization/StableDebtToken.sol
contracts/protocol/libraries/logic/ReserveLogic.sol