// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.10; import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol'; import {GPv2SafeERC20} from '../../../dependencies/gnosis/contracts/GPv2SafeERC20.sol'; import {PercentageMath} from '../../libraries/math/PercentageMath.sol'; import {MathUtils} from '../../libraries/math/MathUtils.sol'; import {TokenMath} from '../../libraries/helpers/TokenMath.sol'; import {DataTypes} from '../../libraries/types/DataTypes.sol'; import {ReserveLogic} from './ReserveLogic.sol'; import {ValidationLogic} from './ValidationLogic.sol'; import {GenericLogic} from './GenericLogic.sol'; import {UserConfiguration} from '../../libraries/configuration/UserConfiguration.sol'; import {ReserveConfiguration} from '../../libraries/configuration/ReserveConfiguration.sol'; import {EModeConfiguration} from '../../libraries/configuration/EModeConfiguration.sol'; import {IAToken} from '../../../interfaces/IAToken.sol'; import {IPool} from '../../../interfaces/IPool.sol'; import {IVariableDebtToken} from '../../../interfaces/IVariableDebtToken.sol'; import {IPriceOracleGetter} from '../../../interfaces/IPriceOracleGetter.sol'; import {SafeCast} from 'openzeppelin-contracts/contracts/utils/math/SafeCast.sol'; import {Errors} from '../helpers/Errors.sol'; /** * @title LiquidationLogic library * @author Aave * @notice Implements actions involving management of collateral in the protocol, the main one being the liquidations */ library LiquidationLogic { using TokenMath for uint256; using PercentageMath for uint256; using ReserveLogic for DataTypes.ReserveCache; using ReserveLogic for DataTypes.ReserveData; using UserConfiguration for DataTypes.UserConfigurationMap; using ReserveConfiguration for DataTypes.ReserveConfigurationMap; using GPv2SafeERC20 for IERC20; using SafeCast for uint256; /** * @dev Default percentage of borrower's debt to be repaid in a liquidation. * @dev Percentage applied when the users health factor is above `CLOSE_FACTOR_HF_THRESHOLD` * Expressed in bps, a value of 0.5e4 results in 50.00% */ uint256 internal constant DEFAULT_LIQUIDATION_CLOSE_FACTOR = 0.5e4; /** * @dev This constant represents the upper bound on the health factor, below(inclusive) which the full amount of debt becomes liquidatable. * A value of 0.95e18 results in 0.95 */ uint256 public constant CLOSE_FACTOR_HF_THRESHOLD = 0.95e18; /** * @dev This constant represents a base value threshold. * If the total collateral or debt on a position is below this threshold, the close factor is raised to 100%. * @notice The default value assumes that the basePrice is usd denominated by 8 decimals and needs to be adjusted in a non USD-denominated pool. */ uint256 public constant MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD = 2000e8; /** * @dev This constant represents the minimum amount of assets in base currency that need to be leftover after a liquidation, if not clearing a position completely. * This parameter is inferred from MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD as the logic is dependent. * Assuming a MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD of `n` a liquidation of `n+1` might result in `n/2` leftover which is assumed to be still economically liquidatable. * This mechanic was introduced to ensure liquidators don't optimize gas by leaving some wei on the liquidation. */ uint256 public constant MIN_LEFTOVER_BASE = MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD / 2; /** * @notice Reduces a portion or all of the deficit of a specified reserve by burning the equivalent aToken `amount` * The caller of this method MUST always be the Umbrella contract and the Umbrella contract is assumed to never have debt. * @dev Emits the `DeficitCovered() event`. * @dev If the coverage admin covers its entire balance, `ReserveUsedAsCollateralDisabled()` is emitted. * @param reservesData The state of all the reserves * @param userConfig The user configuration mapping that tracks the supplied/borrowed assets * @param params The additional parameters needed to execute the eliminateDeficit function * @return The amount of deficit covered */ function executeEliminateDeficit( mapping(address => DataTypes.ReserveData) storage reservesData, DataTypes.UserConfigurationMap storage userConfig, DataTypes.ExecuteEliminateDeficitParams memory params ) external returns (uint256) { require(params.amount != 0, Errors.InvalidAmount()); DataTypes.ReserveData storage reserve = reservesData[params.asset]; uint256 currentDeficit = reserve.deficit; require(currentDeficit != 0, Errors.ReserveNotInDeficit()); require(!userConfig.isBorrowingAny(), Errors.UserCannotHaveDebt()); DataTypes.ReserveCache memory reserveCache = reserve.cache(); reserve.updateState(reserveCache); bool isActive = reserveCache.reserveConfiguration.getActive(); require(isActive, Errors.ReserveInactive()); uint256 balanceWriteOff = params.amount; if (params.amount > currentDeficit) { balanceWriteOff = currentDeficit; } uint256 userScaledBalance = IAToken(reserveCache.aTokenAddress).scaledBalanceOf(params.user); uint256 scaledBalanceWriteOff = balanceWriteOff.getATokenBurnScaledAmount( reserveCache.nextLiquidityIndex ); require(scaledBalanceWriteOff <= userScaledBalance, Errors.NotEnoughAvailableUserBalance()); bool isCollateral = userConfig.isUsingAsCollateral(reserve.id); if (isCollateral && scaledBalanceWriteOff == userScaledBalance) { userConfig.setUsingAsCollateral(reserve.id, params.asset, params.user, false); } IAToken(reserveCache.aTokenAddress).burn({ from: params.user, receiverOfUnderlying: reserveCache.aTokenAddress, amount: balanceWriteOff, scaledAmount: scaledBalanceWriteOff, index: reserveCache.nextLiquidityIndex }); reserve.deficit -= balanceWriteOff.toUint128(); reserve.updateInterestRatesAndVirtualBalance( reserveCache, params.asset, 0, 0, params.interestRateStrategyAddress ); emit IPool.DeficitCovered(params.asset, params.user, balanceWriteOff); return balanceWriteOff; } struct LiquidationCallLocalVars { uint256 borrowerCollateralBalance; uint256 borrowerReserveDebt; uint256 actualDebtToLiquidate; uint256 actualCollateralToLiquidate; uint256 liquidationBonus; uint256 healthFactor; uint256 liquidationProtocolFeeAmount; uint256 totalCollateralInBaseCurrency; uint256 totalDebtInBaseCurrency; uint256 borrowerReserveDebtInBaseCurrency; uint256 borrowerReserveCollateralInBaseCurrency; uint256 borrowerScaledCollateralBalance; uint256 collateralAssetPrice; uint256 debtAssetPrice; uint256 collateralAssetUnit; uint256 debtAssetUnit; DataTypes.ReserveCache debtReserveCache; DataTypes.ReserveCache collateralReserveCache; } /** * @notice Function to liquidate a position if its Health Factor drops below 1. The caller (liquidator) * covers `debtToCover` amount of debt of the user getting liquidated, and receives * a proportional amount of the `collateralAsset` plus a bonus to cover market risk * @dev Emits the `LiquidationCall()` event, and the `DeficitCreated()` event if the liquidation results in bad debt * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @param usersConfig The users configuration mapping that track the supplied/borrowed assets * @param eModeCategories The configuration of all the efficiency mode categories * @param params The additional parameters needed to execute the liquidation function */ function executeLiquidationCall( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, mapping(address => DataTypes.UserConfigurationMap) storage usersConfig, mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, DataTypes.ExecuteLiquidationCallParams memory params ) external { LiquidationCallLocalVars memory vars; DataTypes.ReserveData storage collateralReserve = reservesData[params.collateralAsset]; DataTypes.ReserveData storage debtReserve = reservesData[params.debtAsset]; DataTypes.UserConfigurationMap storage borrowerConfig = usersConfig[params.borrower]; vars.debtReserveCache = debtReserve.cache(); debtReserve.updateState(vars.debtReserveCache); // caching of the collateral happens after debtReserveCache is updated // this ensures a non stale cache is used vars.collateralReserveCache = collateralReserve.cache(); collateralReserve.updateState(vars.collateralReserveCache); ( vars.totalCollateralInBaseCurrency, vars.totalDebtInBaseCurrency, , , vars.healthFactor, ) = GenericLogic.calculateUserAccountData( reservesData, reservesList, eModeCategories, DataTypes.CalculateUserAccountDataParams({ userConfig: borrowerConfig, user: params.borrower, oracle: params.priceOracle, userEModeCategory: params.borrowerEModeCategory }) ); vars.borrowerScaledCollateralBalance = IAToken(vars.collateralReserveCache.aTokenAddress) .scaledBalanceOf(params.borrower); vars.borrowerCollateralBalance = vars.borrowerScaledCollateralBalance.getATokenBalance( vars.collateralReserveCache.nextLiquidityIndex ); vars.borrowerReserveDebt = IVariableDebtToken(vars.debtReserveCache.variableDebtTokenAddress) .scaledBalanceOf(params.borrower) .getVTokenBalance(vars.debtReserveCache.nextVariableBorrowIndex); ValidationLogic.validateLiquidationCall( borrowerConfig, collateralReserve, debtReserve, DataTypes.ValidateLiquidationCallParams({ debtReserveCache: vars.debtReserveCache, totalDebt: vars.borrowerReserveDebt, healthFactor: vars.healthFactor, borrower: params.borrower, liquidator: params.liquidator }) ); if ( params.borrowerEModeCategory != 0 && EModeConfiguration.isReserveEnabledOnBitmap( eModeCategories[params.borrowerEModeCategory].collateralBitmap, collateralReserve.id ) ) { vars.liquidationBonus = eModeCategories[params.borrowerEModeCategory].liquidationBonus; } else { vars.liquidationBonus = vars .collateralReserveCache .reserveConfiguration .getLiquidationBonus(); } vars.collateralAssetPrice = IPriceOracleGetter(params.priceOracle).getAssetPrice( params.collateralAsset ); vars.debtAssetPrice = IPriceOracleGetter(params.priceOracle).getAssetPrice(params.debtAsset); vars.collateralAssetUnit = 10 ** vars.collateralReserveCache.reserveConfiguration.getDecimals(); vars.debtAssetUnit = 10 ** vars.debtReserveCache.reserveConfiguration.getDecimals(); vars.borrowerReserveDebtInBaseCurrency = MathUtils.mulDivCeil( vars.borrowerReserveDebt, vars.debtAssetPrice, vars.debtAssetUnit ); // @note floor rounding vars.borrowerReserveCollateralInBaseCurrency = (vars.borrowerCollateralBalance * vars.collateralAssetPrice) / vars.collateralAssetUnit; // by default whole debt in the reserve could be liquidated uint256 maxLiquidatableDebt = vars.borrowerReserveDebt; // but if debt and collateral is above or equal MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD // and health factor is above CLOSE_FACTOR_HF_THRESHOLD this amount may be adjusted if ( vars.borrowerReserveCollateralInBaseCurrency >= MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD && vars.borrowerReserveDebtInBaseCurrency >= MIN_BASE_MAX_CLOSE_FACTOR_THRESHOLD && vars.healthFactor > CLOSE_FACTOR_HF_THRESHOLD ) { uint256 totalDefaultLiquidatableDebtInBaseCurrency = vars.totalDebtInBaseCurrency.percentMul( DEFAULT_LIQUIDATION_CLOSE_FACTOR ); // if the debt is more then DEFAULT_LIQUIDATION_CLOSE_FACTOR % of the whole, // then we CAN liquidate only up to DEFAULT_LIQUIDATION_CLOSE_FACTOR % if (vars.borrowerReserveDebtInBaseCurrency > totalDefaultLiquidatableDebtInBaseCurrency) { maxLiquidatableDebt = (totalDefaultLiquidatableDebtInBaseCurrency * vars.debtAssetUnit) / vars.debtAssetPrice; } } vars.actualDebtToLiquidate = params.debtToCover > maxLiquidatableDebt ? maxLiquidatableDebt : params.debtToCover; ( vars.actualCollateralToLiquidate, vars.actualDebtToLiquidate, vars.liquidationProtocolFeeAmount ) = _calculateAvailableCollateralToLiquidate( vars.collateralReserveCache.reserveConfiguration, vars.collateralAssetPrice, vars.collateralAssetUnit, vars.debtAssetPrice, vars.debtAssetUnit, vars.actualDebtToLiquidate, vars.borrowerCollateralBalance, vars.liquidationBonus ); // to prevent accumulation of dust on the protocol, it is enforced that you either // 1. liquidate all debt // 2. liquidate all collateral // 3. leave more than MIN_LEFTOVER_BASE of collateral & debt if ( vars.actualDebtToLiquidate < vars.borrowerReserveDebt && vars.actualCollateralToLiquidate + vars.liquidationProtocolFeeAmount < vars.borrowerCollateralBalance ) { bool isDebtMoreThanLeftoverThreshold = MathUtils.mulDivCeil( vars.borrowerReserveDebt - vars.actualDebtToLiquidate, vars.debtAssetPrice, vars.debtAssetUnit ) >= MIN_LEFTOVER_BASE; // @note floor rounding bool isCollateralMoreThanLeftoverThreshold = ((vars.borrowerCollateralBalance - vars.actualCollateralToLiquidate - vars.liquidationProtocolFeeAmount) * vars.collateralAssetPrice) / vars.collateralAssetUnit >= MIN_LEFTOVER_BASE; require( isDebtMoreThanLeftoverThreshold && isCollateralMoreThanLeftoverThreshold, Errors.MustNotLeaveDust() ); } // Determine whether the user's collateral is fully depleted after this liquidation. // // The actual token operations use rayDivCeil independently for the liquidator transfer and // the protocol fee transfer. The sum of these ceil-rounded scaled amounts can meet or exceed // the scaled balance even when the unscaled arithmetic predicts a small leftover. When this // happens (reserveFullyConsumed), we use the full reserve value as consumed$ to correctly // detect that no collateral remains. // // For high-decimal tokens (e.g., 18-decimal WETH), a genuine leftover of a few wei may // round to $0 in base currency. In this case consumed$ also equals totalCollateral$ because // the worthless dust vanishes in the floor division — so hasNoCollateralLeft is set correctly. // // Both getATokenBurnScaledAmount and getATokenTransferScaledAmount use rayDivCeil, // so the scaled consumption is correct for both the receiveAToken and the burn path. bool hasNoCollateralLeft; { uint256 scaledCollateralConsumed = vars.actualCollateralToLiquidate.getATokenBurnScaledAmount( vars.collateralReserveCache.nextLiquidityIndex ) + vars.liquidationProtocolFeeAmount.getATokenTransferScaledAmount( vars.collateralReserveCache.nextLiquidityIndex ); // Cap to the actual scaled balance (the fee transfer is capped on-chain too, see L430) if (scaledCollateralConsumed > vars.borrowerScaledCollateralBalance) { scaledCollateralConsumed = vars.borrowerScaledCollateralBalance; } bool reserveFullyConsumed = scaledCollateralConsumed == vars.borrowerScaledCollateralBalance; // Compute consumed$ from the capped scaled consumption. // When reserveFullyConsumed, this equals the reserve's full base value. // When not, a few-wei leftover that rounds to $0 makes consumed$ == totalCollateral$ too. uint256 consumedInBaseCurrency = (scaledCollateralConsumed.getATokenBalance( vars.collateralReserveCache.nextLiquidityIndex ) * vars.collateralAssetPrice) / vars.collateralAssetUnit; hasNoCollateralLeft = consumedInBaseCurrency == vars.totalCollateralInBaseCurrency; // Clear the collateral flag when: // - the reserve's scaled balance will be zero post-transfer, or // - all user collateral is consumed (deficit will be created) if (reserveFullyConsumed || hasNoCollateralLeft) { borrowerConfig.setUsingAsCollateral( collateralReserve.id, params.collateralAsset, params.borrower, false ); } } _burnDebtTokens( vars.debtReserveCache, debtReserve, borrowerConfig, params.borrower, params.debtAsset, vars.borrowerReserveDebt, vars.actualDebtToLiquidate, hasNoCollateralLeft, params.interestRateStrategyAddress ); if (params.receiveAToken) { IAToken(vars.collateralReserveCache.aTokenAddress).transferOnLiquidation( params.borrower, params.liquidator, vars.actualCollateralToLiquidate, vars.actualCollateralToLiquidate.getATokenTransferScaledAmount( vars.collateralReserveCache.nextLiquidityIndex ), vars.collateralReserveCache.nextLiquidityIndex ); } else { // @note Manually updating the cache in case the debt and collateral are the same asset. // This ensures the rates are updated correctly, considering the burning of debt // in the `_burnDebtTokens` function. if (params.collateralAsset == params.debtAsset) { vars.collateralReserveCache.nextScaledVariableDebt = vars .debtReserveCache .nextScaledVariableDebt; } _burnCollateralATokens(collateralReserve, params, vars); } // Transfer fee to treasury if it is non-zero if (vars.liquidationProtocolFeeAmount != 0) { // getATokenTransferScaledAmount has been used because under the hood, transferOnLiquidation is calling AToken.transfer uint256 scaledDownLiquidationProtocolFee = vars .liquidationProtocolFeeAmount .getATokenTransferScaledAmount(vars.collateralReserveCache.nextLiquidityIndex); uint256 scaledDownBorrowerBalance = IAToken(vars.collateralReserveCache.aTokenAddress) .scaledBalanceOf(params.borrower); // To avoid trying to send more aTokens than available on balance, due to 1 wei imprecision if (scaledDownLiquidationProtocolFee > scaledDownBorrowerBalance) { scaledDownLiquidationProtocolFee = scaledDownBorrowerBalance; vars.liquidationProtocolFeeAmount = scaledDownBorrowerBalance.getATokenBalance( vars.collateralReserveCache.nextLiquidityIndex ); } IAToken(vars.collateralReserveCache.aTokenAddress).transferOnLiquidation({ from: params.borrower, to: IAToken(vars.collateralReserveCache.aTokenAddress).RESERVE_TREASURY_ADDRESS(), amount: vars.liquidationProtocolFeeAmount, scaledAmount: scaledDownLiquidationProtocolFee, index: vars.collateralReserveCache.nextLiquidityIndex }); } // burn bad debt if necessary // Each additional debt asset already adds around ~75k gas to the liquidation. // To keep the liquidation gas under control, 0 usd collateral positions are not touched, as there is no immediate benefit in burning or transferring to treasury. if (hasNoCollateralLeft && borrowerConfig.isBorrowingAny()) { _burnBadDebt(reservesData, reservesList, borrowerConfig, params); } // Transfers the debt asset being repaid to the aToken, where the liquidity is kept IERC20(params.debtAsset).safeTransferFrom( params.liquidator, vars.debtReserveCache.aTokenAddress, vars.actualDebtToLiquidate ); emit IPool.LiquidationCall( params.collateralAsset, params.debtAsset, params.borrower, vars.actualDebtToLiquidate, vars.actualCollateralToLiquidate, params.liquidator, params.receiveAToken ); } /** * @notice Burns the collateral aTokens and transfers the underlying to the liquidator. * @dev The function also updates the state and the interest rate of the collateral reserve. * @param collateralReserve The data of the collateral reserve * @param params The additional parameters needed to execute the liquidation function * @param vars The executeLiquidationCall() function local vars */ function _burnCollateralATokens( DataTypes.ReserveData storage collateralReserve, DataTypes.ExecuteLiquidationCallParams memory params, LiquidationCallLocalVars memory vars ) internal { collateralReserve.updateInterestRatesAndVirtualBalance( vars.collateralReserveCache, params.collateralAsset, 0, vars.actualCollateralToLiquidate, params.interestRateStrategyAddress ); // Burn the equivalent amount of aToken, sending the underlying to the liquidator IAToken(vars.collateralReserveCache.aTokenAddress).burn({ from: params.borrower, receiverOfUnderlying: params.liquidator, amount: vars.actualCollateralToLiquidate, scaledAmount: vars.actualCollateralToLiquidate.getATokenBurnScaledAmount( vars.collateralReserveCache.nextLiquidityIndex ), index: vars.collateralReserveCache.nextLiquidityIndex }); } /** * @notice Burns the debt tokens of the user up to the amount being repaid by the liquidator * or the entire debt if the user is in a bad debt scenario. * @dev The function alters the `debtReserveCache` state in `vars` to update the debt related data. * @param debtReserveCache The cached debt reserve parameters * @param debtReserve The storage pointer of the debt reserve parameters * @param borrowerConfig The pointer of the user configuration * @param borrower The user address * @param debtAsset The debt asset address * @param actualDebtToLiquidate The actual debt to liquidate * @param hasNoCollateralLeft The flag representing, will user will have no collateral left after liquidation */ function _burnDebtTokens( DataTypes.ReserveCache memory debtReserveCache, DataTypes.ReserveData storage debtReserve, DataTypes.UserConfigurationMap storage borrowerConfig, address borrower, address debtAsset, uint256 borrowerReserveDebt, uint256 actualDebtToLiquidate, bool hasNoCollateralLeft, address interestRateStrategyAddress ) internal { bool noMoreDebt = true; // Prior v3.1, there were cases where, after liquidation, the `isBorrowing` flag was left on // even after the user debt was fully repaid, so to avoid this function reverting in the `_burnScaled` // (see ScaledBalanceTokenBase contract), we check for any debt remaining. if (borrowerReserveDebt != 0) { uint256 burnAmount = hasNoCollateralLeft ? borrowerReserveDebt : actualDebtToLiquidate; // As vDebt.burn rounds down, we ensure an equivalent of <= amount debt is burned. (noMoreDebt, debtReserveCache.nextScaledVariableDebt) = IVariableDebtToken( debtReserveCache.variableDebtTokenAddress ).burn({ from: borrower, scaledAmount: burnAmount.getVTokenBurnScaledAmount( debtReserveCache.nextVariableBorrowIndex ), index: debtReserveCache.nextVariableBorrowIndex }); } uint256 outstandingDebt = borrowerReserveDebt - actualDebtToLiquidate; if (hasNoCollateralLeft && outstandingDebt != 0) { debtReserve.deficit += outstandingDebt.toUint128(); emit IPool.DeficitCreated(borrower, debtAsset, outstandingDebt); } if (noMoreDebt) { borrowerConfig.setBorrowing(debtReserve.id, false); } debtReserve.updateInterestRatesAndVirtualBalance( debtReserveCache, debtAsset, actualDebtToLiquidate, 0, interestRateStrategyAddress ); } struct AvailableCollateralToLiquidateLocalVars { uint256 maxCollateralToLiquidate; uint256 baseCollateral; uint256 bonusCollateral; uint256 collateralAmount; uint256 debtAmountNeeded; uint256 liquidationProtocolFeePercentage; uint256 liquidationProtocolFee; uint256 collateralAssetPrice; } /** * @notice Calculates how much of a specific collateral can be liquidated, given * a certain amount of debt asset. * @dev This function needs to be called after all the checks to validate the liquidation have been performed, * otherwise it might fail. * @param collateralReserveConfiguration The data of the collateral reserve * @param collateralAssetPrice The price of the underlying asset used as collateral * @param collateralAssetUnit The asset units of the collateral * @param debtAssetPrice The price of the underlying borrowed asset to be repaid with the liquidation * @param debtAssetUnit The asset units of the debt * @param debtToCover The debt amount of borrowed `asset` the liquidator wants to cover * @param borrowerCollateralBalance The collateral balance for the specific `collateralAsset` of the user being liquidated * @param liquidationBonus The collateral bonus percentage to receive as result of the liquidation * @return The maximum amount that is possible to liquidate given all the liquidation constraints (user balance, close factor) * @return The amount to repay with the liquidation * @return The fee taken from the liquidation bonus amount to be paid to the protocol */ function _calculateAvailableCollateralToLiquidate( DataTypes.ReserveConfigurationMap memory collateralReserveConfiguration, uint256 collateralAssetPrice, uint256 collateralAssetUnit, uint256 debtAssetPrice, uint256 debtAssetUnit, uint256 debtToCover, uint256 borrowerCollateralBalance, uint256 liquidationBonus ) internal pure returns (uint256, uint256, uint256) { AvailableCollateralToLiquidateLocalVars memory vars; vars.collateralAssetPrice = collateralAssetPrice; vars.liquidationProtocolFeePercentage = collateralReserveConfiguration .getLiquidationProtocolFee(); // This is the base collateral to liquidate based on the given debt to cover vars.baseCollateral = (debtAssetPrice * debtToCover * collateralAssetUnit) / (vars.collateralAssetPrice * debtAssetUnit); vars.maxCollateralToLiquidate = vars.baseCollateral.percentMulFloor(liquidationBonus); if (vars.maxCollateralToLiquidate > borrowerCollateralBalance) { vars.collateralAmount = borrowerCollateralBalance; vars.debtAmountNeeded = ((vars.collateralAssetPrice * vars.collateralAmount * debtAssetUnit) / (debtAssetPrice * collateralAssetUnit)).percentDivCeil(liquidationBonus); } else { vars.collateralAmount = vars.maxCollateralToLiquidate; vars.debtAmountNeeded = debtToCover; } if (vars.liquidationProtocolFeePercentage != 0) { vars.bonusCollateral = vars.collateralAmount - vars.collateralAmount.percentDivFloor(liquidationBonus); vars.liquidationProtocolFee = vars.bonusCollateral.percentMulCeil( vars.liquidationProtocolFeePercentage ); vars.collateralAmount -= vars.liquidationProtocolFee; } return (vars.collateralAmount, vars.debtAmountNeeded, vars.liquidationProtocolFee); } /** * @notice Remove a user's bad debt by burning debt tokens. * @dev This function iterates through all active reserves where the user has a debt position, * updates their state, and performs the necessary burn. * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @param borrowerConfig The user configuration * @param params The txn params */ function _burnBadDebt( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, DataTypes.UserConfigurationMap storage borrowerConfig, DataTypes.ExecuteLiquidationCallParams memory params ) internal { // the cache is muted inside the iteration and should not be used for other operations uint256 unsafe_cachedBorrowerConfig = borrowerConfig.data; uint256 i = 0; bool isBorrowed = false; while (unsafe_cachedBorrowerConfig != 0) { (unsafe_cachedBorrowerConfig, isBorrowed, ) = UserConfiguration.getNextFlags( unsafe_cachedBorrowerConfig ); if (isBorrowed) { address reserveAddress = reservesList[i]; // @dev legacy check from when dropReserve could leave gaps; see docs/3.7/drop-reserve-removal.md if (reserveAddress != address(0)) { DataTypes.ReserveCache memory reserveCache = reservesData[reserveAddress].cache(); if (reserveCache.reserveConfiguration.getActive()) { reservesData[reserveAddress].updateState(reserveCache); _burnDebtTokens( reserveCache, reservesData[reserveAddress], borrowerConfig, params.borrower, reserveAddress, IVariableDebtToken(reserveCache.variableDebtTokenAddress) .scaledBalanceOf(params.borrower) .getVTokenBalance(reserveCache.nextVariableBorrowIndex), 0, true, params.interestRateStrategyAddress ); } } } unchecked { ++i; } } } }