Pool
Supply(Deposit)
supply function allows users to deposit assets into the Aave protocol, receiving aTokens in return. It handles reserve state updates, interest rate calculations, collateral management, and emits relevant events./// --- src/contracts/protocol/pool/Pool.sol --- /// --- src/contracts/protocol/libraries/types/DataTypes.sol --- struct ExecuteSupplyParams { address user; address asset; address interestRateStrategyAddress; uint256 amount; address onBehalfOf; uint16 referralCode; } struct UserConfigurationMap { /** * @dev Bitmap of the users collaterals and borrows. It is divided in pairs of bits, one pair per asset. * The first bit indicates if an asset is used as collateral by the user, the second whether an * asset is borrowed by the user. */ uint256 data; } /// --- src/contracts/protocol/pool/PoolStorage.sol --- // Map of reserves and their data (underlyingAssetOfReserve => reserveData) mapping(address => DataTypes.ReserveData) internal _reserves; // List of reserves as a map (reserveId => reserve). // It is structured as a mapping for gas savings reasons, using the reserve id as index mapping(uint256 => address) internal _reservesList; // Map of users address and their configuration data (userAddress => userConfiguration) mapping(address => DataTypes.UserConfigurationMap) internal _usersConfig; /// @inheritdoc IPool function supply( address asset, uint256 amount, address onBehalfOf, uint16 referralCode ) public virtual override { SupplyLogic.executeSupply( _reserves, _reservesList, _usersConfig[onBehalfOf], DataTypes.ExecuteSupplyParams({ user: _msgSender(), asset: asset, interestRateStrategyAddress: RESERVE_INTEREST_RATE_STRATEGY, amount: amount, onBehalfOf: onBehalfOf, referralCode: referralCode }) ); } /// @inheritdoc IPool /// @dev Deprecated: maintained for compatibility purposes function deposit( address asset, uint256 amount, address onBehalfOf, uint16 referralCode ) external virtual override { SupplyLogic.executeSupply( _reserves, _reservesList, _usersConfig[onBehalfOf], DataTypes.ExecuteSupplyParams({ user: _msgSender(), asset: asset, interestRateStrategyAddress: RESERVE_INTEREST_RATE_STRATEGY, amount: amount, onBehalfOf: onBehalfOf, referralCode: referralCode }) ); }
executeSupply
Steps:
- Reserve Data Retrieval and Caching
- Functionality: Retrieves the reserve data for the supplied asset and creates a cache for efficient computation
- Purpose: Caching avoids repeated storage reads during complex calculations
- Reserve State Update
Updates the reserve's liquidity index and variable borrow index, accrues interest to the current block timestamp before any operations
- Amount Scaling
converts the supplied underlying amount to scaled aToken amount using ray division
- Supply Validation
- Non-zero amount
- Reserve is active and not paused/frozen
- Supply cap not exceeded
- Cannot supply to aToken address directly (
onBehalfOfcan’t be aToken address, this is to prevent misoperation of user)
- Interest Rate Update
Recalculates liquidity and borrow rates based on new supply
- Asset Transfer
Transfers underlying tokens from user to aToken contract
- aToken Minting
Mints aTokens to the specified address
- Collateral Activation
- It's user's first supply of this asset
- Asset has non-zero LTV
- User not in isolation mode
- For isolated assets, requires user to be whitelisted isolated asset supplier.
Automatically enables asset as collateral if:
In AAVE V3, it allows isolation asset. Isolation asset is volatile asset with higher risk, user can use this as collateral to borrow only stablecoin. V3 allows user to supply isolation asset as collateral, and requires user can only selects one isolation asset as collateral, and in such case, user can’t use any other asset as collateral.
In AAVE V2, when user deposits, protocol mints protocol fee in aToken directly. But in V3, it just records it in
reserve.accruedToTreasury rather than mint directly which can save gas for user./// --- src/contracts/protocol/libraries/logic/SupplyLogic.sol --- /** * @notice Implements the supply feature. Through `supply()`, users supply assets to the Aave protocol. * @dev Emits the `Supply()` event. * @dev In the first supply action, `ReserveUsedAsCollateralEnabled()` is emitted, if the asset can be enabled as * collateral. * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @param userConfig The user configuration mapping that tracks the supplied/borrowed assets * @param params The additional parameters needed to execute the supply function */ function executeSupply( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, DataTypes.UserConfigurationMap storage userConfig, DataTypes.ExecuteSupplyParams memory params ) external { DataTypes.ReserveData storage reserve = reservesData[params.asset]; DataTypes.ReserveCache memory reserveCache = reserve.cache(); reserve.updateState(reserveCache); uint256 scaledAmount = params.amount.getATokenMintScaledAmount(reserveCache.nextLiquidityIndex); ValidationLogic.validateSupply(reserveCache, reserve, scaledAmount, params.onBehalfOf); reserve.updateInterestRatesAndVirtualBalance( reserveCache, params.asset, params.amount, 0, params.interestRateStrategyAddress ); IERC20(params.asset).safeTransferFrom(params.user, reserveCache.aTokenAddress, params.amount); // As aToken.mint rounds down the minted shares, we ensure an equivalent of <= params.amount shares is minted. bool isFirstSupply = IAToken(reserveCache.aTokenAddress).mint( params.user, params.onBehalfOf, scaledAmount, reserveCache.nextLiquidityIndex ); if (isFirstSupply) { if ( ValidationLogic.validateAutomaticUseAsCollateral( params.user, reservesData, reservesList, userConfig, reserveCache.reserveConfiguration, reserveCache.aTokenAddress ) ) { userConfig.setUsingAsCollateral(reserve.id, params.asset, params.onBehalfOf, true); } } emit IPool.Supply( params.asset, params.user, params.onBehalfOf, params.amount, params.referralCode ); } /// --- src/contracts/protocol/libraries/helpers/TokenMath.sol --- /** * @notice Calculates the scaled amount of aTokens to mint when supplying underlying assets. * The amount is rounded down to ensure the minted aTokens are less than or equal to the supplied amount. * @param amount The amount of underlying asset supplied. * @param liquidityIndex The current aToken liquidityIndex. * @return The scaled amount of aTokens to mint. */ function getATokenMintScaledAmount( uint256 amount, uint256 liquidityIndex ) internal pure returns (uint256) { return amount.rayDivFloor(liquidityIndex); }
cache
cache loads state from storage into memory to avoid repeated storage reads during complex calculations/// --- src/contracts/protocol/libraries/logic/ReserveLogic.sol --- /** * @notice Creates a cache object to avoid repeated storage reads and external contract calls when updating state and * interest rates. * @param reserve The reserve object for which the cache will be filled * @return The cache object */ function cache( DataTypes.ReserveData storage reserve ) internal view returns (DataTypes.ReserveCache memory) { DataTypes.ReserveCache memory reserveCache; reserveCache.reserveConfiguration = reserve.configuration; reserveCache.reserveFactor = reserveCache.reserveConfiguration.getReserveFactor(); reserveCache.currLiquidityIndex = reserveCache.nextLiquidityIndex = reserve.liquidityIndex; reserveCache.currVariableBorrowIndex = reserveCache.nextVariableBorrowIndex = reserve .variableBorrowIndex; reserveCache.currLiquidityRate = reserve.currentLiquidityRate; reserveCache.currVariableBorrowRate = reserve.currentVariableBorrowRate; reserveCache.aTokenAddress = reserve.aTokenAddress; reserveCache.variableDebtTokenAddress = reserve.variableDebtTokenAddress; reserveCache.reserveLastUpdateTimestamp = reserve.lastUpdateTimestamp; reserveCache.currScaledVariableDebt = reserveCache.nextScaledVariableDebt = IVariableDebtToken( reserveCache.variableDebtTokenAddress ).scaledTotalSupply(); return reserveCache; }
updateState
updateState updates the reserve's liquidity index and variable borrow index, accrues interest to the current block timestamp before any operations. It also calculates the protocol fee using the reserve’s aToken as the unit./** * @notice Updates the liquidity cumulative index, the variable borrow index and the timestamp of the update. * @param reserve The reserve object * @param reserveCache The caching layer for the reserve data */ function updateState( DataTypes.ReserveData storage reserve, DataTypes.ReserveCache memory reserveCache ) internal { // If time didn't pass since last stored timestamp, skip state update //solium-disable-next-line if (reserveCache.reserveLastUpdateTimestamp == uint40(block.timestamp)) { return; } _updateIndexes(reserve, reserveCache); _accrueToTreasury(reserve, reserveCache); //solium-disable-next-line reserve.lastUpdateTimestamp = uint40(block.timestamp); reserveCache.reserveLastUpdateTimestamp = uint40(block.timestamp); }
_updateIndexes
_updateIndexes updates liquidity interest rate index and variable borrow interest rate index./// --- src/contracts/protocol/libraries/logic/ReserveLogic.sol --- /** * @notice Updates the reserve indexes. * @param reserve The reserve reserve to be updated * @param reserveCache The cache layer holding the cached protocol data */ function _updateIndexes( DataTypes.ReserveData storage reserve, DataTypes.ReserveCache memory reserveCache ) internal { // Only cumulating on the supply side if there is any income being produced // The case of Reserve Factor 100% is not a problem (currentLiquidityRate == 0), // as liquidity index should not be updated if (reserveCache.currLiquidityRate != 0) { uint256 cumulatedLiquidityInterest = MathUtils.calculateLinearInterest( reserveCache.currLiquidityRate, reserveCache.reserveLastUpdateTimestamp ); reserveCache.nextLiquidityIndex = cumulatedLiquidityInterest.rayMul( reserveCache.currLiquidityIndex ); reserve.liquidityIndex = reserveCache.nextLiquidityIndex.toUint128(); } // Variable borrow index only gets updated if there is any variable debt. // reserveCache.currVariableBorrowRate != 0 is not a correct validation, // because a positive base variable rate can be stored on // reserveCache.currVariableBorrowRate, but the index should not increase if (reserveCache.currScaledVariableDebt != 0) { uint256 cumulatedVariableBorrowInterest = MathUtils.calculateCompoundedInterest( reserveCache.currVariableBorrowRate, reserveCache.reserveLastUpdateTimestamp ); reserveCache.nextVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul( reserveCache.currVariableBorrowIndex ); reserve.variableBorrowIndex = reserveCache.nextVariableBorrowIndex.toUint128(); } }
_accrueToTreasury
_accrueToTreasury calculates protocol fee based on accumulated interest of variable borrow, and converts it into aToken amoumt./// --- src/contracts/protocol/libraries/logic/ReserveLogic.sol --- /** * @notice Mints part of the repaid interest to the reserve treasury as a function of the reserve factor for the * specific asset. * @param reserve The reserve to be updated * @param reserveCache The caching layer for the reserve data */ function _accrueToTreasury( DataTypes.ReserveData storage reserve, DataTypes.ReserveCache memory reserveCache ) internal { if (reserveCache.reserveFactor == 0) { return; } // debt accrued is the sum of the current debt minus the sum of the debt at the last update // Rounding down to undermint to the treasury and keep the invariant healthy. uint256 totalDebtAccrued = reserveCache.currScaledVariableDebt.rayMulFloor( reserveCache.nextVariableBorrowIndex - reserveCache.currVariableBorrowIndex ); uint256 amountToMint = totalDebtAccrued.percentMul(reserveCache.reserveFactor); if (amountToMint != 0) { reserve.accruedToTreasury += amountToMint .getATokenMintScaledAmount(reserveCache.nextLiquidityIndex) .toUint128(); } } /// --- src/contracts/protocol/libraries/helpers/TokenMath.sol --- /** * @notice Calculates the scaled amount of aTokens to mint when supplying underlying assets. * The amount is rounded down to ensure the minted aTokens are less than or equal to the supplied amount. * @param amount The amount of underlying asset supplied. * @param liquidityIndex The current aToken liquidityIndex. * @return The scaled amount of aTokens to mint. */ function getATokenMintScaledAmount( uint256 amount, uint256 liquidityIndex ) internal pure returns (uint256) { return amount.rayDivFloor(liquidityIndex); }
validateSupply
validateSupply checks reserve is enbaled and reserve’s supply cap is not crossed.Note that supply cap is based on underlying asset amount rather than scaled amount.
/// --- src/contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @notice Validates a supply action. * @param reserveCache The cached data of the reserve * @param scaledAmount The scaledAmount to be supplied */ function validateSupply( DataTypes.ReserveCache memory reserveCache, DataTypes.ReserveData storage reserve, uint256 scaledAmount, address onBehalfOf ) internal view { require(scaledAmount != 0, Errors.InvalidAmount()); (bool isActive, bool isFrozen, , bool isPaused) = reserveCache.reserveConfiguration.getFlags(); require(isActive, Errors.ReserveInactive()); require(!isPaused, Errors.ReservePaused()); require(!isFrozen, Errors.ReserveFrozen()); require(onBehalfOf != reserveCache.aTokenAddress, Errors.SupplyToAToken()); uint256 supplyCap = reserveCache.reserveConfiguration.getSupplyCap(); require( supplyCap == 0 || ( (IAToken(reserveCache.aTokenAddress).scaledTotalSupply() + scaledAmount + uint256(reserve.accruedToTreasury)).getATokenBalance(reserveCache.nextLiquidityIndex) ) <= supplyCap * (10 ** reserveCache.reserveConfiguration.getDecimals()), Errors.SupplyCapExceeded() ); }
updateInterestRatesAndVirtualBalance
updateInterestRatesAndVirtualBalance updates the reserve current variable borrow rate and the current liquidity rate. Also it updates virtual liquidity./** * @notice Updates the reserve current variable borrow rate and the current liquidity rate. * @param reserve The reserve reserve to be updated * @param reserveCache The caching layer for the reserve data * @param reserveAddress The address of the reserve to be updated * @param liquidityAdded The amount of liquidity added to the protocol (supply or repay) in the previous action * @param liquidityTaken The amount of liquidity taken from the protocol (redeem or borrow) */ function updateInterestRatesAndVirtualBalance( DataTypes.ReserveData storage reserve, DataTypes.ReserveCache memory reserveCache, address reserveAddress, uint256 liquidityAdded, uint256 liquidityTaken, address interestRateStrategyAddress ) internal { uint256 totalVariableDebt = reserveCache.nextScaledVariableDebt.getVTokenBalance( reserveCache.nextVariableBorrowIndex ); (uint256 nextLiquidityRate, uint256 nextVariableRate) = IReserveInterestRateStrategy( interestRateStrategyAddress ).calculateInterestRates( DataTypes.CalculateInterestRatesParams({ unbacked: reserve.deficit, liquidityAdded: liquidityAdded, liquidityTaken: liquidityTaken, totalDebt: totalVariableDebt, reserveFactor: reserveCache.reserveFactor, reserve: reserveAddress, usingVirtualBalance: true, virtualUnderlyingBalance: reserve.virtualUnderlyingBalance }) ); reserve.currentLiquidityRate = nextLiquidityRate.toUint128(); reserve.currentVariableBorrowRate = nextVariableRate.toUint128(); if (liquidityAdded > 0) { reserve.virtualUnderlyingBalance += liquidityAdded.toUint128(); } if (liquidityTaken > 0) { reserve.virtualUnderlyingBalance -= liquidityTaken.toUint128(); } emit IPool.ReserveDataUpdated( reserveAddress, nextLiquidityRate, 0, nextVariableRate, reserveCache.nextLiquidityIndex, reserveCache.nextVariableBorrowIndex ); }
calculateInterestRates
The
calculateInterestRates function differs from the V2 implementation in the following ways:- No stable debt in V3
V3 removes stable debt, so there is no stable borrow rate or average stable borrow rate.
- aToken bridging and unbacked liquidity
borrowUsageRatio: based on the utilization of available liquidity, and used for calculating the borrow rate.supplyUsageRatio: based on both available liquidity and unbacked aToken liquidity, and used for calculating the liquidity rate.
V3 introduces support for aToken bridging. Since there may be a time gap between the minting of bridged aTokens and the arrival of the corresponding underlying assets, situations can arise where aTokens exist without being fully backed. These are referred to as unbacked aTokens.
Another scenario occurs when the protocol experiences a deficit—meaning a user does not have enough collateral to be liquidated and cover their debt. In this case, the unpaid debt is recorded as a deficit and reflected in
reserve.unbacked. This ensures that, even when a deficit occurs, borrowers continue to borrow at rates calculated based on actual liquidity usage, and liquidity providers still earn yield proportionally, regardless of whether part of the pool is in deficit. Such deficits can later be recovered through mechanisms like the treasury fund.Interest rate usage is split into two components:
This design ensures borrower pays based on actual available liquidity usage, while bridged aToken can start interest accrual immediately.
As a result, liquidity providers who supply actual underlying assets may temporarily suffer an interest rate loss. This loss is compensated once the underlying assets corresponding to unbacked aTokens are delivered on-chain.
Also aToken under deficit loss can still accrue interest fee.
Overall, the interest rate calculation remains the same as in V2, with the consideration of the protocol fee.
/// --- contracts/misc/DefaultReserveInterestRateStrategyV2.sol --- /// @dev Map of reserves address and their interest rate data (reserveAddress => interestRateData) mapping(address => InterestRateData) internal _interestRateData; /** * @notice Holds the interest rate data for a given reserve * * @dev Since values are in bps, they are multiplied by 1e23 in order to become rays with 27 decimals. This * in turn means that the maximum supported interest rate is 4294967295 (2**32-1) bps or 42949672.95%. * * @param optimalUsageRatio The optimal usage ratio, in bps * @param baseVariableBorrowRate The base variable borrow rate, in bps * @param variableRateSlope1 The slope of the variable interest curve, before hitting the optimal ratio, in bps * @param variableRateSlope2 The slope of the variable interest curve, after hitting the optimal ratio, in bps */ struct InterestRateData { uint16 optimalUsageRatio; uint32 baseVariableBorrowRate; uint32 variableRateSlope1; uint32 variableRateSlope2; } struct CalcInterestRatesLocalVars { uint256 availableLiquidity; uint256 currentVariableBorrowRate; uint256 currentLiquidityRate; uint256 borrowUsageRatio; uint256 supplyUsageRatio; uint256 availableLiquidityPlusDebt; } /// @inheritdoc IReserveInterestRateStrategy function calculateInterestRates( DataTypes.CalculateInterestRatesParams calldata params ) external view virtual override returns (uint256, uint256) { InterestRateDataRay memory rateData = _rayifyRateData(_interestRateData[params.reserve]); CalcInterestRatesLocalVars memory vars; vars.currentLiquidityRate = 0; vars.currentVariableBorrowRate = rateData.baseVariableBorrowRate; if (params.totalDebt != 0) { vars.availableLiquidity = params.virtualUnderlyingBalance + params.liquidityAdded - params.liquidityTaken; vars.availableLiquidityPlusDebt = vars.availableLiquidity + params.totalDebt; vars.borrowUsageRatio = params.totalDebt.rayDiv(vars.availableLiquidityPlusDebt); vars.supplyUsageRatio = params.totalDebt.rayDiv( vars.availableLiquidityPlusDebt + params.unbacked ); } else { return (0, vars.currentVariableBorrowRate); } if (vars.borrowUsageRatio > rateData.optimalUsageRatio) { uint256 excessBorrowUsageRatio = (vars.borrowUsageRatio - rateData.optimalUsageRatio).rayDiv( WadRayMath.RAY - rateData.optimalUsageRatio ); vars.currentVariableBorrowRate += rateData.variableRateSlope1 + rateData.variableRateSlope2.rayMul(excessBorrowUsageRatio); } else { vars.currentVariableBorrowRate += rateData .variableRateSlope1 .rayMul(vars.borrowUsageRatio) .rayDiv(rateData.optimalUsageRatio); } vars.currentLiquidityRate = vars .currentVariableBorrowRate .rayMul(vars.supplyUsageRatio) .percentMul(PercentageMath.PERCENTAGE_FACTOR - params.reserveFactor); return (vars.currentLiquidityRate, vars.currentVariableBorrowRate); } /** * @dev Transforms an InterestRateData struct to an InterestRateDataRay struct by multiplying all values * by 1e23, turning them into ray values * * @param data The InterestRateData struct to transform * * @return The resulting InterestRateDataRay struct */ function _rayifyRateData( InterestRateData memory data ) internal pure returns (InterestRateDataRay memory) { return InterestRateDataRay({ optimalUsageRatio: _bpsToRay(uint256(data.optimalUsageRatio)), baseVariableBorrowRate: _bpsToRay(uint256(data.baseVariableBorrowRate)), variableRateSlope1: _bpsToRay(uint256(data.variableRateSlope1)), variableRateSlope2: _bpsToRay(uint256(data.variableRateSlope2)) }); }
validateAutomaticUseAsCollateral
validateAutomaticUseAsCollateral functions checks whether the asset can be used by user as collateral.- First check whether this is isolation asset:
- If this asset is isolation asset(it has debt ceiling), then only account with role
ISOLATED_COLLATERAL_SUPPLIER_ROLEcan activate it as collateral (also needs to pass other constraints).
- Check other constraints whether user can activate it as collateral
- Only asset with Ltv greator than 0 can be used as collateral
- Ensure if user uses isolation asset, they can only use one asset (the isolation asset) as collateral
- if user hasn’t supplied any reserve as collateral, it can activate this asset as collateral.
- Only if user is not in isolation mode, and the asset to be added as collateral is not isolation asset, can this asset be used as collateral.
validateAutomaticUseAsCollateral ensures that if user is in isolation mode who has already used one isolation asset as collateral, they can’t supply other reserve(whether it’s isolation asset or not) as collateral. But user still can supply liquidity of other assets to earn interest./// --- contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @notice Validates if an asset should be automatically activated as collateral in the following actions: supply, * transfer, and liquidate * @dev This is used to ensure that isolated assets are not enabled as collateral automatically * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @param userConfig the user configuration * @param reserveConfig The reserve configuration * @return True if the asset can be activated as collateral, false otherwise */ function validateAutomaticUseAsCollateral( address sender, mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, DataTypes.UserConfigurationMap storage userConfig, DataTypes.ReserveConfigurationMap memory reserveConfig, address aTokenAddress ) internal view returns (bool) { /// check whether the reserve is isolation asset if (reserveConfig.getDebtCeiling() != 0) { // ensures only the ISOLATED_COLLATERAL_SUPPLIER_ROLE can enable collateral as side-effect of an action IPoolAddressesProvider addressesProvider = IncentivizedERC20(aTokenAddress) .POOL() .ADDRESSES_PROVIDER(); if ( !IAccessControl(addressesProvider.getACLManager()).hasRole( ISOLATED_COLLATERAL_SUPPLIER_ROLE, sender ) ) return false; } // check whether user can add this reserve as collateral return validateUseAsCollateral(reservesData, reservesList, userConfig, reserveConfig); } /** * @notice Validates the action of activating the asset as collateral. * @dev Only possible if the asset has non-zero LTV and the user is not in isolation mode * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @param userConfig the user configuration * @param reserveConfig The reserve configuration * @return True if the asset can be activated as collateral, false otherwise */ function validateUseAsCollateral( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, DataTypes.UserConfigurationMap storage userConfig, DataTypes.ReserveConfigurationMap memory reserveConfig ) internal view returns (bool) { // only reserve with non-zero LTV can be used as collateral if (reserveConfig.getLtv() == 0) { return false; } // if user hasn't use any reserve as collateral, they can always add one reserve // as collateral whether it's or not isolation asset. if (!userConfig.isUsingAsCollateralAny()) { return true; } // check whether user is in isolation mode (chose one isolation asset as collateral) // in such case: // if user tries to add same isolation asset, they don't need to mark it as collateral again // if user tries to add another isolation asset, the asset can't be used as collateral (bool isolationModeActive, , ) = userConfig.getIsolationModeState(reservesData, reservesList); return (!isolationModeActive && reserveConfig.getDebtCeiling() == 0); } /// --- contracts/protocol/libraries/configuration/UserConfiguration.sol --- /** * @notice Returns the Isolation Mode state of the user * @param self The configuration object * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @return True if the user is in isolation mode, false otherwise * @return The address of the only asset used as collateral * @return The debt ceiling of the reserve */ function getIsolationModeState( DataTypes.UserConfigurationMap memory self, mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList ) internal view returns (bool, address, uint256) { // Checks if a user has been supplying only one reserve as collateral if (isUsingAsCollateralOne(self)) { // Returns the asset id of the first asset flagged in the bitmap given the corresponding bitmask uint256 assetId = _getFirstAssetIdByMask(self, COLLATERAL_MASK); address assetAddress = reservesList[assetId]; // check whether the asset is isolation asset uint256 ceiling = reservesData[assetAddress].configuration.getDebtCeiling(); if (ceiling != 0) { return (true, assetAddress, ceiling); } } return (false, address(0), 0); } /** * @notice Checks if a user has been supplying only one reserve as collateral * @dev this uses a simple trick - if a number is a power of two (only one bit set) then n & (n - 1) == 0 * @param self The configuration object * @return True if the user has been supplying as collateral one reserve, false otherwise */ function isUsingAsCollateralOne( DataTypes.UserConfigurationMap memory self ) internal pure returns (bool) { uint256 collateralData = self.data & COLLATERAL_MASK; return collateralData != 0 && (collateralData & (collateralData - 1) == 0); } /** * @notice Returns the address of the first asset flagged in the bitmap given the corresponding bitmask * @param self The configuration object * @return The index of the first asset flagged in the bitmap once the corresponding mask is applied */ function _getFirstAssetIdByMask( DataTypes.UserConfigurationMap memory self, uint256 mask ) internal pure returns (uint256) { unchecked { uint256 bitmapData = self.data & mask; uint256 firstAssetPosition = bitmapData & ~(bitmapData - 1); uint256 id; while ((firstAssetPosition >>= 2) != 0) { id += 1; } return id; } }
Withdraw
withdraw function allows users to redeem their aTokens for the underlying asset, handling interest accrual, collateral management, and health factor validation.Note it passes user’s eMode setting to function
SupplyLogic.executeWithdraw. eMode is a new feature in V3 which allows user to select lending mode. Each mode defines collaterals and borrowable assets, so that the lending parameters can be set to be more capital efficient. For example, eMode with stablecoin as collateral and borrowable assets can have a higher liquidation threshold.
The eMode id of user is passed to function
SupplyLogic.executeWithdraw to calculate user’s health factor more efficiently./// --- contracts/protocol/pool/Pool.sol --- // Map of users address and their eMode category (userAddress => eModeCategoryId) mapping(address => uint8) internal _usersEModeCategory; /// @inheritdoc IPool function withdraw( address asset, uint256 amount, address to ) public virtual override returns (uint256) { return SupplyLogic.executeWithdraw( _reserves, _reservesList, _eModeCategories, _usersConfig[_msgSender()], DataTypes.ExecuteWithdrawParams({ user: _msgSender(), asset: asset, interestRateStrategyAddress: RESERVE_INTEREST_RATE_STRATEGY, amount: amount, to: to, oracle: ADDRESSES_PROVIDER.getPriceOracle(), userEModeCategory: _usersEModeCategory[_msgSender()] }) ); }
executeWithdraw
Steps:
- Initial Setup and Validation
- Reserve Caching: Creates cache for efficient state management
- Security Check: Prevents withdrawing to aToken address (would lock funds)
- Reserve State Update
- Functionality: Accrues interest to current block timestamp
- Updates: Liquidity index, variable borrow index, treasury accruals
- Withdrawl Amount Calculation
- Max Withdrawal (
type(uint256).max) - Specific Amount
Two Withdrawal Modes:
- Withdrawal Validation
- Non-zero amount
- Sufficient user balance
- Reserve is active and not paused
- Interest Rate Update
- Decreases
virtualUnderlyingBalanceby withdrawal amount - Recalculates interest rates based on reduced liquidity
Impact on Reserve:
- aToken Burning
from: User whose aTokens are burnedreceiverOfUnderlying: Who receives the underlying tokensamount: Actual underlying amount to withdrawscaledAmount: Scaled aToken amount to burn- Returns:
trueif user's balance becomes zero after burn
Burn Function Details:
- Collateral Management
- Collateral Auto-Disable
- If user withdraws entire balance of a collateral asset, automatically disables it as collateral
- Prevents zero-balance assets from being counted as collateral
- Health Factor Validation
- Only checks health factor if user has active borrows
- Ensures withdrawal doesn't make user undercollateralized
- Ensure collateral with LTV 0 is first withdrawe
/// --- contracts/protocol/libraries/logic/SupplyLogic.sol --- /** * @notice Implements the withdraw feature. Through `withdraw()`, users redeem their aTokens for the underlying asset * previously supplied in the Aave protocol. * @dev Emits the `Withdraw()` event. * @dev If the user withdraws everything, `ReserveUsedAsCollateralDisabled()` is emitted. * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @param eModeCategories The configuration of all the efficiency mode categories * @param userConfig The user configuration mapping that tracks the supplied/borrowed assets * @param params The additional parameters needed to execute the withdraw function * @return The actual amount withdrawn */ function executeWithdraw( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, DataTypes.UserConfigurationMap storage userConfig, DataTypes.ExecuteWithdrawParams memory params ) external returns (uint256) { /// Initial Setup and Validation DataTypes.ReserveData storage reserve = reservesData[params.asset]; DataTypes.ReserveCache memory reserveCache = reserve.cache(); require(params.to != reserveCache.aTokenAddress, Errors.WithdrawToAToken()); /// Reserve State Update reserve.updateState(reserveCache); uint256 scaledUserBalance = IAToken(reserveCache.aTokenAddress).scaledBalanceOf(params.user); /// Withdrawl Amount Calculation uint256 amountToWithdraw; uint256 scaledAmountToWithdraw; if (params.amount == type(uint256).max) { scaledAmountToWithdraw = scaledUserBalance; amountToWithdraw = scaledUserBalance.getATokenBalance(reserveCache.nextLiquidityIndex); } else { scaledAmountToWithdraw = params.amount.getATokenBurnScaledAmount( reserveCache.nextLiquidityIndex ); amountToWithdraw = params.amount; } /// Withdrawal Validation ValidationLogic.validateWithdraw(reserveCache, scaledAmountToWithdraw, scaledUserBalance); /// Interest Rate Update reserve.updateInterestRatesAndVirtualBalance( reserveCache, params.asset, 0, amountToWithdraw, params.interestRateStrategyAddress ); /// aToken Burning // As aToken.burn rounds up the burned shares, we ensure at least an equivalent of >= amountToWithdraw is burned. bool zeroBalanceAfterBurn = IAToken(reserveCache.aTokenAddress).burn({ from: params.user, receiverOfUnderlying: params.to, amount: amountToWithdraw, scaledAmount: scaledAmountToWithdraw, index: reserveCache.nextLiquidityIndex }); /// Collateral Management if (userConfig.isUsingAsCollateral(reserve.id)) { /// Collateral Auto-Disable if (zeroBalanceAfterBurn) { userConfig.setUsingAsCollateral(reserve.id, params.asset, params.user, false); } /// Health Factor Validation if (userConfig.isBorrowingAny()) { ValidationLogic.validateHFAndLtvzero( reservesData, reservesList, eModeCategories, userConfig, params.asset, params.user, params.oracle, params.userEModeCategory ); } } emit IPool.Withdraw(params.asset, params.user, params.to, amountToWithdraw); return amountToWithdraw; }
validateWithdraw
/// --- contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @notice Validates a withdraw action. * @param reserveCache The cached data of the reserve * @param scaledAmount The scaled amount to be withdrawn * @param scaledUserBalance The scaled balance of the user */ function validateWithdraw( DataTypes.ReserveCache memory reserveCache, uint256 scaledAmount, uint256 scaledUserBalance ) internal pure { require(scaledAmount != 0, Errors.InvalidAmount()); require(scaledAmount <= scaledUserBalance, Errors.NotEnoughAvailableUserBalance()); (bool isActive, , , bool isPaused) = reserveCache.reserveConfiguration.getFlags(); require(isActive, Errors.ReserveInactive()); require(!isPaused, Errors.ReservePaused()); }
validateHFAndLtvzero
validateHFAndLtvzero checks user’s portfolia is healthy. And it also ensures user always withdraw asset with LTV 0 first. This is to prevent user borrow asset based on collateral with LTV 0 as described in paper./// --- contracts/protocol/libraries/logic/ValidationLogic.sol --- struct EModeCategory { // each eMode category has a custom ltv and liquidation threshold uint16 ltv; uint16 liquidationThreshold; uint16 liquidationBonus; uint128 collateralBitmap; string label; uint128 borrowableBitmap; } /** * @notice Validates the health factor of a user and the ltvzero configuration for the asset being withdrawn/transferred or disabled as collateral. * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @param eModeCategories The configuration of all the efficiency mode categories * @param userConfig The state of the user for the specific reserve * @param asset The asset for which the ltv will be validated * @param from The user from which the aTokens are being transferred * @param oracle The price oracle * @param userEModeCategory The users active efficiency mode category */ function validateHFAndLtvzero( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, DataTypes.UserConfigurationMap memory userConfig, address asset, address from, address oracle, uint8 userEModeCategory ) internal view { (, bool hasZeroLtvCollateral) = validateHealthFactor( reservesData, reservesList, eModeCategories, userConfig, from, userEModeCategory, oracle ); require( !hasZeroLtvCollateral || reservesData[asset].configuration.getLtv() == 0, Errors.LtvValidationFailed() ); }
validateHealthFactor
validateHealthFactor calls calculateUserAccountData to calculate user’s health factor and ensure it’s not smaller than 1./// --- contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @notice Validates the health factor of a user. * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @param eModeCategories The configuration of all the efficiency mode categories * @param userConfig The state of the user for the specific reserve * @param user The user to validate health factor of * @param userEModeCategory The users active efficiency mode category * @param oracle The price oracle */ function validateHealthFactor( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, DataTypes.UserConfigurationMap memory userConfig, address user, uint8 userEModeCategory, address oracle ) internal view returns (uint256, bool) { (, , , , uint256 healthFactor, bool hasZeroLtvCollateral) = GenericLogic .calculateUserAccountData( reservesData, reservesList, eModeCategories, DataTypes.CalculateUserAccountDataParams({ userConfig: userConfig, user: user, oracle: oracle, userEModeCategory: userEModeCategory }) ); require( healthFactor >= HEALTH_FACTOR_LIQUIDATION_THRESHOLD, Errors.HealthFactorLowerThanLiquidationThreshold() ); return (healthFactor, hasZeroLtvCollateral); }
calculateUserAccountData
calculateUserAccountData function calculates comprehensive user account data including collateral value, debt value, average LTV, liquidation threshold, health factor, and identifies zero-LTV collateral positions.The overall logic is same as V2 except one difference: V3 introduces eMode. eMode defines collateral and borrowable assets and specific ltv, liquidition threshold and other parameters. User can and only can enter single eMode. If user has entered an eMode,
calculateUserAccountData calculates user asset profile based on eMode parameters.Steps:
- Early Return for Empty Accounts
- Optimization: Immediately returns for users with no positions
- Health Factor:
type(uint256).maxindicates no risk - Gas Savings: Avoids unnecessary computation
- EMode Configuration Setup
- EMode Benefits: Higher LTV and liquidation thresholds for correlated assets
- Bitmap Filter:
eModeCollateralBitmaprecords which asset is enable in this eMode. Only specific assets can be used in each EMode category.
- Reserve Iteration Loop
- Uses bit shifting to iterate through user's positions
- Each reserve occupies 2 bits (borrowing + collateral flags)
- Continues until all bits are processed
Efficient Bitmap Processing:
- Collateral Processing
- Calculate collateral value (via aToken balance, accumulate interest)
- Scaled Balance: Gets user's aToken balance in scaled form
- Normalized Income: Converts to actual underlying amount using liquidity index
- Price Conversion: Multiplies by asset price to get base currency value
- Weighted Averages
- LTV Weighting:
balance * (EModeLTV or reserveLTV) - Liquidation Threshold Weighting:
balance * (EModeLiqThreshold or reserveLiqThreshold) - Zero-LTV Detection: asset with LTV 0 can’t be used for borrowing
- Debt Processing
- Scaled Debt: Gets user's variable debt in scaled form
- Normalized Debt: Converts to actual debt using variable borrow index
- Ceiling Rounding: Rounds up to be conservative in risk assessment
Debt Calculation:
- Final Calculations
- health factor
- average LTV
- average liquidation threshold
/// --- contracts/protocol/libraries/logic/GenericLogic.sol --- /** * @notice Calculates the user data across the reserves. * @dev It includes the total liquidity/collateral/borrow balances in the base currency used by the price feed, * the average Loan To Value, the average Liquidation Ratio, and the Health factor. * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @param eModeCategories The configuration of all the efficiency mode categories * @param params Additional parameters needed for the calculation * @return The total collateral of the user in the base currency used by the price feed * @return The total debt of the user in the base currency used by the price feed * @return The average ltv of the user * @return The average liquidation threshold of the user * @return The health factor of the user * @return True if the ltv is zero, false otherwise */ function calculateUserAccountData( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, DataTypes.CalculateUserAccountDataParams memory params ) internal view returns (uint256, uint256, uint256, uint256, uint256, bool) { /// Early Return for Empty Accounts if (params.userConfig.isEmpty()) { return (0, 0, 0, 0, type(uint256).max, false); } CalculateUserAccountDataVars memory vars; /// EMode Configuration Setup if (params.userEModeCategory != 0) { vars.eModeLtv = eModeCategories[params.userEModeCategory].ltv; vars.eModeLiqThreshold = eModeCategories[params.userEModeCategory].liquidationThreshold; vars.eModeCollateralBitmap = eModeCategories[params.userEModeCategory].collateralBitmap; } uint256 userConfigCache = params.userConfig.data; bool isBorrowed = false; bool isEnabledAsCollateral = false; /// Reserve Iteration Loop while (userConfigCache != 0) { (userConfigCache, isBorrowed, isEnabledAsCollateral) = UserConfiguration.getNextFlags( userConfigCache ); if (isEnabledAsCollateral || isBorrowed) { vars.currentReserveAddress = reservesList[vars.i]; if (vars.currentReserveAddress != address(0)) { DataTypes.ReserveData storage currentReserve = reservesData[vars.currentReserveAddress]; (vars.ltv, vars.liquidationThreshold, , vars.decimals, ) = currentReserve .configuration .getParams(); unchecked { vars.assetUnit = 10 ** vars.decimals; } vars.assetPrice = IPriceOracleGetter(params.oracle).getAssetPrice( vars.currentReserveAddress ); /// Collateral Processing if (vars.liquidationThreshold != 0 && isEnabledAsCollateral) { vars.userBalanceInBaseCurrency = _getUserBalanceInBaseCurrency( params.user, currentReserve, vars.assetPrice, vars.assetUnit ); vars.totalCollateralInBaseCurrency += vars.userBalanceInBaseCurrency; vars.isInEModeCategory = params.userEModeCategory != 0 && EModeConfiguration.isReserveEnabledOnBitmap(vars.eModeCollateralBitmap, vars.i); if (vars.ltv != 0) { vars.avgLtv += vars.userBalanceInBaseCurrency * (vars.isInEModeCategory ? vars.eModeLtv : vars.ltv); } else { vars.hasZeroLtvCollateral = true; } vars.avgLiquidationThreshold += vars.userBalanceInBaseCurrency * (vars.isInEModeCategory ? vars.eModeLiqThreshold : vars.liquidationThreshold); } /// Debt Processing if (isBorrowed) { vars.totalDebtInBaseCurrency += _getUserDebtInBaseCurrency( params.user, currentReserve, vars.assetPrice, vars.assetUnit ); } } } unchecked { ++vars.i; } } /// Final Calculations // @note At this point, `avgLiquidationThreshold` represents // `SUM(collateral_base_value_i * liquidation_threshold_i)` for all collateral assets. // It has 8 decimals (base currency) + 2 decimals (percentage) = 10 decimals. // healthFactor has 18 decimals // healthFactor = (avgLiquidationThreshold * WAD / totalDebtInBaseCurrency) / 100_00 // 18 decimals = (10 decimals * 18 decimals / 8 decimals) / 2 decimals = 18 decimals vars.healthFactor = (vars.totalDebtInBaseCurrency == 0) ? type(uint256).max : vars.avgLiquidationThreshold.wadDiv(vars.totalDebtInBaseCurrency) / 100_00; unchecked { vars.avgLtv = vars.totalCollateralInBaseCurrency != 0 ? vars.avgLtv / vars.totalCollateralInBaseCurrency : 0; vars.avgLiquidationThreshold = vars.totalCollateralInBaseCurrency != 0 ? vars.avgLiquidationThreshold / vars.totalCollateralInBaseCurrency : 0; } return ( vars.totalCollateralInBaseCurrency, vars.totalDebtInBaseCurrency, vars.avgLtv, vars.avgLiquidationThreshold, vars.healthFactor, vars.hasZeroLtvCollateral ); }
Borrow
allows users to borrow assets from the Aave protocol using their supplied collateral as backing. Same as V2, it supports credit borrow (one can borrow on behalf of
onBehalf account with onBehalf's approval )DIfference from V1:
- Introduce Isolated Collateral
When users supply collateral(via
supply function), protocol ensures if user supplies isolated asset as collateral, they can and can only supply that collateral. This means user can select to enter into isolation mode, in this mode, user can only supply one kind of isolated asset as collateral, and borrow allowe asset. Usually isolated collateral has higher risk, so it has debt ceiling, and can only be used to borrow stablecoins to control risk.- Introduce Siloed Asset
Reserve can be marked as siloed asset by protocol. In that case, user can borrow and can one kind of siloed asset and no other assets.
/// --- contracts/protocol/pool/Pool.sol --- /// @inheritdoc IPool function borrow( address asset, uint256 amount, uint256 interestRateMode, uint16 referralCode, address onBehalfOf ) public virtual override { BorrowLogic.executeBorrow( _reserves, _reservesList, _eModeCategories, _usersConfig[onBehalfOf], DataTypes.ExecuteBorrowParams({ asset: asset, interestRateStrategyAddress: RESERVE_INTEREST_RATE_STRATEGY, user: _msgSender(), onBehalfOf: onBehalfOf, amount: amount, interestRateMode: DataTypes.InterestRateMode(interestRateMode), referralCode: referralCode, releaseUnderlying: true, oracle: ADDRESSES_PROVIDER.getPriceOracle(), userEModeCategory: _usersEModeCategory[onBehalfOf], priceOracleSentinel: ADDRESSES_PROVIDER.getPriceOracleSentinel() }) ); }
executeBorrow
Steps:
- Reserve Setup and State Update
- Cache Creation: Creates efficient cache to minimize storage reads
- State Update: Accrues interest and updates reserve indices
- Amount Scaling
- Scaling: Converts underlying amount to scaled debt amount using
getVTokenMintScaledAmount - Rounding: Uses ceiling rounding to ensure protocol doesn't under-account debt
- Borrow Validation
calls
validateBorrow for comprehensive validation- Debt Token Minting
mint debt token to user for debt management
- User Configuration Update
- First Borrow Flag: Sets borrowing flag for this asset in user's configuration
- Isolation Mode Debt Tracking
- If borrowing an isolated asset, increases the isolated debt counter
- Ensures isolated debt doesn't exceed debt ceiling
- Interest Rate Update
- Calculate new borrow and liquidity interest rate due to reduced available liquidity
- Updates
virtualUnderlyingBalanceif tokens are released
- Underlying Asset Release
Optional Release:
releaseUnderlying flag controls whether tokens are actually transferred.- Post-Borrow Health Check
Ensures user's health factor remains above liquidation threshold
/// --- contracts/protocol/libraries/logic/BorrowLogic.sol --- /** * @notice Implements the borrow feature. Borrowing allows users that provided collateral to draw liquidity from the * Aave protocol proportionally to their collateralization power. For isolated positions, it also increases the * isolated debt. * @dev Emits the `Borrow()` event * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @param eModeCategories The configuration of all the efficiency mode categories * @param userConfig The user configuration mapping that tracks the supplied/borrowed assets * @param params The additional parameters needed to execute the borrow function */ function executeBorrow( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, DataTypes.UserConfigurationMap storage userConfig, DataTypes.ExecuteBorrowParams memory params ) external { /// Reserve Setup and State Update DataTypes.ReserveData storage reserve = reservesData[params.asset]; DataTypes.ReserveCache memory reserveCache = reserve.cache(); /// Amount Scaling reserve.updateState(reserveCache); uint256 amountScaled = params.amount.getVTokenMintScaledAmount( reserveCache.nextVariableBorrowIndex ); /// Borrow Validation ValidationLogic.validateBorrow( reservesData, reservesList, eModeCategories, DataTypes.ValidateBorrowParams({ reserveCache: reserveCache, userConfig: userConfig, asset: params.asset, userAddress: params.onBehalfOf, amountScaled: amountScaled, interestRateMode: params.interestRateMode, oracle: params.oracle, userEModeCategory: params.userEModeCategory, priceOracleSentinel: params.priceOracleSentinel }) ); /// Debt Token Minting reserveCache.nextScaledVariableDebt = IVariableDebtToken(reserveCache.variableDebtTokenAddress) .mint( params.user, params.onBehalfOf, params.amount, amountScaled, reserveCache.nextVariableBorrowIndex ); /// User Configuration Update uint16 cachedReserveId = reserve.id; if (!userConfig.isBorrowing(cachedReserveId)) { userConfig.setBorrowing(cachedReserveId, true); } /// Isolation Mode Debt Tracking IsolationModeLogic.increaseIsolatedDebtIfIsolated( reservesData, reservesList, userConfig, reserveCache, params.amount ); /// Interest Rate Update reserve.updateInterestRatesAndVirtualBalance( reserveCache, params.asset, 0, params.releaseUnderlying ? params.amount : 0, params.interestRateStrategyAddress ); /// Underlying Asset Release if (params.releaseUnderlying) { IAToken(reserveCache.aTokenAddress).transferUnderlyingTo(params.user, params.amount); } /// Post-Borrow Health Check ValidationLogic.validateHFAndLtv( reservesData, reservesList, eModeCategories, userConfig, params.onBehalfOf, params.userEModeCategory, params.oracle ); emit IPool.Borrow( params.asset, params.user, params.onBehalfOf, params.amount, DataTypes.InterestRateMode.VARIABLE, reserve.currentVariableBorrowRate, params.referralCode ); }
validateBorrow
validateBorrow function executes Comprehensive validation to ensure borrow operation is safe and compliant with protocol rules.Steps
- Basic non-zero Amount Check
- Amount Conversion
- Converts scaled amount back to underlying for validation
- Uses ceiling rounding for conservative debt accounting
- Reserve Status Checks
- is active
- is not paused
- is not frozen
- is enabled for borrow
- Liquidity Availability Check
- Ensures protocol has sufficient liquidity to fulfill borrow
- Uses aToken total supply as proxy for available liquidity
- Price Oracle Sentinel Check
The Oracle Sentinel can temporarily disable borrowing on Aave. This mechanism is designed for Layer 2 environments to handle sequencer downtime. When the sequencer goes offline, it cannot process off-chain transactions. However, the oracle can relay a message from Layer 1 to Layer 2 to notify the Aave protocol of the outage. In response, Aave disables borrowing. Once the sequencer recovers, and after a predefined grace period, borrowing is re-enabled.
- Interest Rate Mode Validation
Only variable rate borrowing supported
- Borrow Cap Enforcement
- Calculates new total debt after borrow
- Converts to underlying amount for comparison
- Ensures borrow cap isn't exceeded
- EMode Compatibility
- User can only borrow assets compatible with their EMode category
- Bitmap defines which assets are borrowable in each EMode
- Siloed Borrowing Validation
- Case 1 - User in Siloed Mode: User can only borrow the same siloed asset they're already borrowing
- Case 2 - Asset is Siloed: If asset is siloed, user cannot borrow it if they already have other borrows
/// --- contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @notice Validates a borrow action. * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @param eModeCategories The configuration of all the efficiency mode categories * @param params Additional params needed for the validation */ function validateBorrow( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, DataTypes.ValidateBorrowParams memory params ) internal view { /// Basic non-zero Amount Check require(params.amountScaled != 0, Errors.InvalidAmount()); ValidateBorrowLocalVars memory vars; /// Amount Conversion vars.amount = params.amountScaled.getVTokenBalance(params.reserveCache.nextVariableBorrowIndex); /// Reserve Status Checks (vars.isActive, vars.isFrozen, vars.borrowingEnabled, vars.isPaused) = params .reserveCache .reserveConfiguration .getFlags(); require(vars.isActive, Errors.ReserveInactive()); require(!vars.isPaused, Errors.ReservePaused()); require(!vars.isFrozen, Errors.ReserveFrozen()); require(vars.borrowingEnabled, Errors.BorrowingNotEnabled()); /// Liquidity Availability Check require( IERC20(params.reserveCache.aTokenAddress).totalSupply() >= vars.amount, Errors.InvalidAmount() ); /// Price Oracle Sentinel Check require( params.priceOracleSentinel == address(0) || IPriceOracleSentinel(params.priceOracleSentinel).isBorrowAllowed(), Errors.PriceOracleSentinelCheckFailed() ); // Interest Rate Mode Validation require( params.interestRateMode == DataTypes.InterestRateMode.VARIABLE, Errors.InvalidInterestRateModeSelected() ); /// Borrow Cap Enforcement vars.reserveDecimals = params.reserveCache.reserveConfiguration.getDecimals(); vars.borrowCap = params.reserveCache.reserveConfiguration.getBorrowCap(); unchecked { vars.assetUnit = 10 ** vars.reserveDecimals; } if (vars.borrowCap != 0) { vars.totalDebt = (params.reserveCache.currScaledVariableDebt + params.amountScaled) .getVTokenBalance(params.reserveCache.nextVariableBorrowIndex); unchecked { require(vars.totalDebt <= vars.borrowCap * vars.assetUnit, Errors.BorrowCapExceeded()); } } /// EMode Compatibility if (params.userEModeCategory != 0) { require( EModeConfiguration.isReserveEnabledOnBitmap( eModeCategories[params.userEModeCategory].borrowableBitmap, reservesData[params.asset].id ), Errors.NotBorrowableInEMode() ); } /// Siloed Borrowing Validation if (params.userConfig.isBorrowingAny()) { (vars.siloedBorrowingEnabled, vars.siloedBorrowingAddress) = params .userConfig .getSiloedBorrowingState(reservesData, reservesList); if (vars.siloedBorrowingEnabled) { require(vars.siloedBorrowingAddress == params.asset, Errors.SiloedBorrowingViolation()); } else { require( !params.reserveCache.reserveConfiguration.getSiloedBorrowing(), Errors.SiloedBorrowingViolation() ); } } }
increaseIsolatedDebtIfIsolated
increaseIsolatedDebtIfIsolated function increases the isolated debt whenever user borrows against isolated collateral asset/// --- contracts/protocol/libraries/logic/IsolationModeLogic.sol --- /** * @notice increases the isolated debt whenever user borrows against isolated collateral asset * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @param userConfig The user configuration mapping * @param reserveCache The cached data of the reserve * @param borrowAmount The amount being borrowed */ function increaseIsolatedDebtIfIsolated( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, DataTypes.UserConfigurationMap storage userConfig, DataTypes.ReserveCache memory reserveCache, uint256 borrowAmount ) internal { ( bool isolationModeActive, address isolationModeCollateralAddress, uint256 isolationModeDebtCeiling ) = userConfig.getIsolationModeState(reservesData, reservesList); if (isolationModeActive) { // check that the asset being borrowed is borrowable in isolation mode AND // the total exposure is no bigger than the collateral debt ceiling require( reserveCache.reserveConfiguration.getBorrowableInIsolation(), Errors.AssetNotBorrowableInIsolation() ); uint128 nextIsolationModeTotalDebt = reservesData[isolationModeCollateralAddress] .isolationModeTotalDebt + convertToIsolatedDebtUnits(reserveCache, borrowAmount); require(nextIsolationModeTotalDebt <= isolationModeDebtCeiling, Errors.DebtCeilingExceeded()); setIsolationModeTotalDebt( reservesData[isolationModeCollateralAddress], isolationModeCollateralAddress, nextIsolationModeTotalDebt ); } } /** * @notice Sets the isolation mode total debt of the given asset to a certain value * @param reserveData The state of the reserve * @param isolationModeCollateralAddress The address of the isolation mode collateral * @param newIsolationModeTotalDebt The new isolation mode total debt */ function setIsolationModeTotalDebt( DataTypes.ReserveData storage reserveData, address isolationModeCollateralAddress, uint128 newIsolationModeTotalDebt ) internal { reserveData.isolationModeTotalDebt = newIsolationModeTotalDebt; emit IPool.IsolationModeTotalDebtUpdated( isolationModeCollateralAddress, newIsolationModeTotalDebt ); }
getIsolationModeState
getIsolationModeState returns the Isolation mode state of the user.If user has only one collateral, and the collateral is isolation asset, then user automatically enters into isolation mode, they can only borrow borrowable asset under isolation mode.
/// --- contracts/protocol/libraries/configuration/UserConfiguration.sol --- /** * @notice Returns the Isolation Mode state of the user * @param self The configuration object * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @return True if the user is in isolation mode, false otherwise * @return The address of the only asset used as collateral * @return The debt ceiling of the reserve */ function getIsolationModeState( DataTypes.UserConfigurationMap memory self, mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList ) internal view returns (bool, address, uint256) { if (isUsingAsCollateralOne(self)) { uint256 assetId = _getFirstAssetIdByMask(self, COLLATERAL_MASK); address assetAddress = reservesList[assetId]; uint256 ceiling = reservesData[assetAddress].configuration.getDebtCeiling(); if (ceiling != 0) { return (true, assetAddress, ceiling); } } return (false, address(0), 0); }
getBorrowableInIsolation
return whether this reserve is allowed to be borrowed with isolated asset as collateral. Typically, stablecoin is allowed to be borrwed against high risk isolated asset.
/// --- contracts/protocol/libraries/configuration/ReserveConfiguration.sol --- /** * @notice Gets the borrowable in isolation flag for the reserve. * @dev If the returned flag is true, the asset is borrowable against isolated collateral. Assets borrowed with * isolated collateral is accounted for in the isolated collateral's total debt exposure. * @dev Only assets of the same family (eg USD stablecoins) should be borrowable in isolation mode to keep * consistency in the debt ceiling calculations. * @param self The reserve configuration * @return The borrowable in isolation flag */ function getBorrowableInIsolation( DataTypes.ReserveConfigurationMap memory self ) internal pure returns (bool) { return (self.data & BORROWABLE_IN_ISOLATION_MASK) != 0; }
getSiloedBorrowingState
/// --- contracts/protocol/libraries/configuration/UserConfiguration.sol --- /** * @notice Returns the siloed borrowing state for the user * @param self The configuration object * @param reservesData The data of all the reserves * @param reservesList The reserve list * @return True if the user has borrowed a siloed asset, false otherwise * @return The address of the only borrowed asset */ function getSiloedBorrowingState( DataTypes.UserConfigurationMap memory self, mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList ) internal view returns (bool, address) { if (isBorrowingOne(self)) { uint256 assetId = _getFirstAssetIdByMask(self, BORROWING_MASK); address assetAddress = reservesList[assetId]; if (reservesData[assetAddress].configuration.getSiloedBorrowing()) { return (true, assetAddress); } } return (false, address(0)); }
getSiloedBorrowing
/// --- contracts/protocol/libraries/configuration/ReserveConfiguration.sol --- /** * @notice Gets the siloed borrowing flag for the reserve. * @dev When this flag is set to true, users borrowing this asset will not be allowed to borrow any other asset. * @param self The reserve configuration * @return The siloed borrowing flag */ function getSiloedBorrowing( DataTypes.ReserveConfigurationMap memory self ) internal pure returns (bool) { return (self.data & SILOED_BORROWING_MASK) != 0; }
Repay
repay function handles the repayment of borrowed assets./// --- contracts/protocol/pool/Pool.sol --- /// @inheritdoc IPool function repay( address asset, uint256 amount, uint256 interestRateMode, address onBehalfOf ) public virtual override returns (uint256) { return BorrowLogic.executeRepay( _reserves, _reservesList, _eModeCategories, _usersConfig[onBehalfOf], DataTypes.ExecuteRepayParams({ asset: asset, user: _msgSender(), interestRateStrategyAddress: RESERVE_INTEREST_RATE_STRATEGY, amount: amount, interestRateMode: DataTypes.InterestRateMode(interestRateMode), onBehalfOf: onBehalfOf, useATokens: false, oracle: ADDRESSES_PROVIDER.getPriceOracle(), userEModeCategory: _usersEModeCategory[onBehalfOf] }) ); }
executeRepay
Steps:
- Reserve Setup
Use cache to minimize storage reads
- State Update
Accrues interest to current timestamp, updates liquidity and borrow indices
- User’s Debt Calculation
- Repayment Validation
- Non-zero amount
- Valid interest rate mode (only variable supported in V3)
- Reserve is active and not paused
- User has existing debt
- Payback Amount Calculation
- Specific Amount with Underlying
- Max Repayment with aTokens: Uses user's entire aToken balance
- Cap to Actual Debt
- Prevents over-repayment
- Caps at user's actual outstanding debt
- Debt Token Burning
Burn debt token for
onBehalf account. (User can repay debt for other accounts)- Interest Rate Update
- Using underlying: Adds
paybackAmountto reserve liquidity - Using aTokens: No liquidity change (internal balance transfer)
- User Configuration Update
Updates user configuration bitmap: Clears borrowing flag if user has no more debt for this asset
- Isolation Mode Debt Reduction
Reduces isolated debt counter if user is using isolated collateral asset
- Repayment Execution
- aToken Repayment (
params.useATokens = true) - Auto-disable collateral: If aToken balance becomes zero
- Health factor validation: Only if user has other borrows
- Underlying Asset Repayment (
params.useATokens = false) - Direct transfer of underlying tokens to aToken contract from user
Question: It seems there’s no check to ensure the
user and onBehalf accounts are the same when useATokens is set to true. If the repayment is made using aTokens, the protocol should validate the user’s health factor and update the user’s collateral status, rather than applying these checks to the onBehalf account. Otherwise, this leads to errors./** * @notice Implements the repay feature. Repaying transfers the underlying back to the aToken and clears the * equivalent amount of debt for the user by burning the corresponding debt token. For isolated positions, it also * reduces the isolated debt. * @dev Emits the `Repay()` event * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @param onBehalfOfConfig The user configuration mapping that tracks the supplied/borrowed assets * @param params The additional parameters needed to execute the repay function * @return The actual amount being repaid */ function executeRepay( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, DataTypes.UserConfigurationMap storage onBehalfOfConfig, DataTypes.ExecuteRepayParams memory params ) external returns (uint256) { /// Reserve Setup DataTypes.ReserveData storage reserve = reservesData[params.asset]; DataTypes.ReserveCache memory reserveCache = reserve.cache(); /// State Update reserve.updateState(reserveCache); /// User’s Debt Calculation uint256 userDebtScaled = IVariableDebtToken(reserveCache.variableDebtTokenAddress) .scaledBalanceOf(params.onBehalfOf); uint256 userDebt = userDebtScaled.getVTokenBalance(reserveCache.nextVariableBorrowIndex); /// Repayment Validation ValidationLogic.validateRepay( params.user, reserveCache, params.amount, params.interestRateMode, params.onBehalfOf, userDebtScaled ); /// Payback Amount Calculation uint256 paybackAmount = params.amount; if (params.useATokens && params.amount == type(uint256).max) { // Allows a user to repay with aTokens without leaving dust from interest. paybackAmount = IAToken(reserveCache.aTokenAddress) .scaledBalanceOf(params.user) .getATokenBalance(reserveCache.nextLiquidityIndex); } if (paybackAmount > userDebt) { paybackAmount = userDebt; } /// Debt Token Burning bool noMoreDebt; (noMoreDebt, reserveCache.nextScaledVariableDebt) = IVariableDebtToken( reserveCache.variableDebtTokenAddress ).burn({ from: params.onBehalfOf, scaledAmount: paybackAmount.getVTokenBurnScaledAmount(reserveCache.nextVariableBorrowIndex), index: reserveCache.nextVariableBorrowIndex }); /// Interest Rate Update reserve.updateInterestRatesAndVirtualBalance( reserveCache, params.asset, params.useATokens ? 0 : paybackAmount, 0, params.interestRateStrategyAddress ); /// User Configuration Update if (noMoreDebt) { onBehalfOfConfig.setBorrowing(reserve.id, false); } /// Isolation Mode Debt Reduction IsolationModeLogic.reduceIsolatedDebtIfIsolated( reservesData, reservesList, onBehalfOfConfig, reserveCache, paybackAmount ); /// Repayment Execution // in case of aToken repayment the sender must always repay on behalf of itself if (params.useATokens) { // As aToken.burn rounds up the burned shares, we ensure at least an equivalent of >= paybackAmount is burned. bool zeroBalanceAfterBurn = IAToken(reserveCache.aTokenAddress).burn({ from: params.user, receiverOfUnderlying: reserveCache.aTokenAddress, amount: paybackAmount, scaledAmount: paybackAmount.getATokenBurnScaledAmount(reserveCache.nextLiquidityIndex), index: reserveCache.nextLiquidityIndex }); if (onBehalfOfConfig.isUsingAsCollateral(reserve.id)) { if (zeroBalanceAfterBurn) { onBehalfOfConfig.setUsingAsCollateral(reserve.id, params.asset, params.user, false); } if (onBehalfOfConfig.isBorrowingAny()) { ValidationLogic.validateHealthFactor( reservesData, reservesList, eModeCategories, onBehalfOfConfig, params.user, params.userEModeCategory, params.oracle ); } } } else { IERC20(params.asset).safeTransferFrom(params.user, reserveCache.aTokenAddress, paybackAmount); } emit IPool.Repay( params.asset, params.onBehalfOf, params.user, paybackAmount, params.useATokens ); return paybackAmount; }
validateRepay
validateRepay validates a repay action./// --- contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @notice Validates a repay action. * @param user The user initiating the repayment * @param reserveCache The cached data of the reserve * @param amountSent The amount sent for the repayment. Can be an actual value or type(uint256).max * @param onBehalfOf The address of the user sender is repaying for * @param debtScaled The borrow scaled balance of the user */ function validateRepay( address user, DataTypes.ReserveCache memory reserveCache, uint256 amountSent, DataTypes.InterestRateMode interestRateMode, address onBehalfOf, uint256 debtScaled ) internal pure { require(amountSent != 0, Errors.InvalidAmount()); require( interestRateMode == DataTypes.InterestRateMode.VARIABLE, Errors.InvalidInterestRateModeSelected() ); require( amountSent != type(uint256).max || user == onBehalfOf, Errors.NoExplicitAmountToRepayOnBehalf() ); (bool isActive, , , bool isPaused) = reserveCache.reserveConfiguration.getFlags(); require(isActive, Errors.ReserveInactive()); require(!isPaused, Errors.ReservePaused()); require(debtScaled != 0, Errors.NoDebtOfSelectedType()); }
setUserUseReserveAsCollateral
setUserUseReserveAsCollateral enables users to manually activate or deactivate an asset as collateral, with comprehensive safety checks to ensure the operation doesn't make their position unsafe./// --- contracts/protocol/pool/Pool.sol --- function setUserUseReserveAsCollateral( address asset, bool useAsCollateral ) public virtual override { SupplyLogic.executeUseReserveAsCollateral( _reserves, _reservesList, _eModeCategories, _usersConfig[_msgSender()], _msgSender(), asset, useAsCollateral, ADDRESSES_PROVIDER.getPriceOracle(), _usersEModeCategory[_msgSender()] ); }
executeUseReserveAsCollateral
Steps:
- Reserve Data Retrieval
Caches reserve configuration for efficient access
- Basic Reserve Validation
- Reserve is active
- Reserve is not paused
- Early Return for No-Op
If asset is already in desired state, exit early
- Enabling as Collateral (
useAsCollateral = true) - User must have a positive balance of the asset, this is to prevent user enable zero balance assets as collateral (to save gas in other operations where user collateral calculation is needed)
- Collateral Eligibility Validation
- Collateral has Non-zero LTV
- isolation collateral mode check (If user is already in isolation mode, cannot add new collateral)
- Update User Configuration
Sets the collateral flag in user's configuration bitmap to mark that user has used that asset as collateral
- Disabling as Collateral (
useAsCollateral = false) - Update User Configuration: Clears the collateral flag in user's configuration
- Health Factor Validation
/// --- contracts/protocol/libraries/logic/SupplyLogic.sol --- /** * @notice Executes the 'set as collateral' feature. A user can choose to activate or deactivate an asset as * collateral at any point in time. Deactivating an asset as collateral is subjected to the usual health factor * checks to ensure collateralization. * @dev Emits the `ReserveUsedAsCollateralEnabled()` event if the asset can be activated as collateral. * @dev In case the asset is being deactivated as collateral, `ReserveUsedAsCollateralDisabled()` is emitted. * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @param eModeCategories The configuration of all the efficiency mode categories * @param userConfig The users configuration mapping that track the supplied/borrowed assets * @param user The user calling the method * @param asset The address of the asset being configured as collateral * @param useAsCollateral True if the user wants to set the asset as collateral, false otherwise * @param priceOracle The address of the price oracle * @param userEModeCategory The eMode category chosen by the user */ function executeUseReserveAsCollateral( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, DataTypes.UserConfigurationMap storage userConfig, address user, address asset, bool useAsCollateral, address priceOracle, uint8 userEModeCategory ) external { /// Reserve Data Retrieval DataTypes.ReserveData storage reserve = reservesData[asset]; DataTypes.ReserveConfigurationMap memory reserveConfigCached = reserve.configuration; /// Basic Reserve Validation ValidationLogic.validateSetUseReserveAsCollateral(reserveConfigCached); /// Early Return for No-Op if (useAsCollateral == userConfig.isUsingAsCollateral(reserve.id)) return; /// Enabling as Collateral (useAsCollateral = true) if (useAsCollateral) { // When enabeling a reserve as collateral, we want to ensure the user has at least some collateral require( IAToken(reserve.aTokenAddress).scaledBalanceOf(user) != 0, Errors.UnderlyingBalanceZero() ); require( ValidationLogic.validateUseAsCollateral( reservesData, reservesList, userConfig, reserveConfigCached ), Errors.UserInIsolationModeOrLtvZero() ); userConfig.setUsingAsCollateral(reserve.id, asset, user, true); } else { /// Disabling as Collateral (useAsCollateral = false) userConfig.setUsingAsCollateral(reserve.id, asset, user, false); ValidationLogic.validateHFAndLtvzero( reservesData, reservesList, eModeCategories, userConfig, asset, user, priceOracle, userEModeCategory ); } }
validateSetUseReserveAsCollateral
/// --- contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @notice Validates the action of setting an asset as collateral. * @param reserveConfig The config of the reserve */ function validateSetUseReserveAsCollateral( DataTypes.ReserveConfigurationMap memory reserveConfig ) internal pure { (bool isActive, , , bool isPaused) = reserveConfig.getFlags(); require(isActive, Errors.ReserveInactive()); require(!isPaused, Errors.ReservePaused()); }
validateUseAsCollateral
/// --- contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @notice Validates the action of activating the asset as collateral. * @dev Only possible if the asset has non-zero LTV and the user is not in isolation mode * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @param userConfig the user configuration * @param reserveConfig The reserve configuration * @return True if the asset can be activated as collateral, false otherwise */ function validateUseAsCollateral( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, DataTypes.UserConfigurationMap storage userConfig, DataTypes.ReserveConfigurationMap memory reserveConfig ) internal view returns (bool) { if (reserveConfig.getLtv() == 0) { return false; } if (!userConfig.isUsingAsCollateralAny()) { return true; } (bool isolationModeActive, , ) = userConfig.getIsolationModeState(reservesData, reservesList); return (!isolationModeActive && reserveConfig.getDebtCeiling() == 0); }
liquidationCall
liquidationCall allows liquidators to close undercollateralized positions by repaying debt in exchange for collateral assets, with bonus incentives and protocol safety mechanisms./// --- contracts/protocol/pool/Pool.sol --- /// @inheritdoc IPool function liquidationCall( address collateralAsset, address debtAsset, address borrower, uint256 debtToCover, bool receiveAToken ) public virtual override { LiquidationLogic.executeLiquidationCall( _reserves, _reservesList, _usersConfig, _eModeCategories, DataTypes.ExecuteLiquidationCallParams({ liquidator: _msgSender(), debtToCover: debtToCover, collateralAsset: collateralAsset, debtAsset: debtAsset, borrower: borrower, receiveAToken: receiveAToken, priceOracle: ADDRESSES_PROVIDER.getPriceOracle(), borrowerEModeCategory: _usersEModeCategory[borrower], priceOracleSentinel: ADDRESSES_PROVIDER.getPriceOracleSentinel(), interestRateStrategyAddress: RESERVE_INTEREST_RATE_STRATEGY }) ); }
executeLiquidationCall
Steps:
- Initial Setup
Creates efficient caches for both collateral and debt reserves for gas saving
- State Updates
Accrues interest to current timestamp for both reserves, updates interest rate indices
- User Account Health Calculation
- Total collateral value across all assets
- Total debt value across all assets
- Current health factor
Calculates:
- Get Debt and Collateral Amount
- Gets borrower's collateral balance for the liquidated asset
- Gets borrower's debt balance for the debt asset
- Liquidation Validation
- Liquidator ≠ Borrower (no self-liquidation)
- Both reserves active and not paused
- Price oracle sentinel allows liquidation
- Liquidation grace period has passed
- Health factor < 1.0 (liquidition threshold reached, unhealthy)
- Collateral is enabled by borrower
- Borrower has the debt
Validates:
- Liquidation Bonus Determination
- EMode
- Reserve Configuration
Bonus Sources:
- Price and Unit Setup
Get price and decimals of debt and collateral asset
- Base Currency Conversions of Debt and Collateral
- Debt: Uses ceiling rounding (conservative)
- Collateral: Uses floor rounding (conservative)
Calculate value of debt and collateral of borrower in base currency.
- Maximum Liquidatable Debt Calculation
- Health Factor > 0.95: Can liquidate up to 50% of total debt
- Health Factor ≤ 0.95: Can liquidate 100% of debt
- Small Positions: Full liquidation allowed regardless for liquidation efficiency
- Calculate Actual Debt and Collateral to Liquidate
- Calculate actual debt to liquidate based on calculated maximum liquidatable debt
- Calculate collateral can be liquidated based on debt to liquidate
- Dust Prevention Check
Prevents: Leaving economically insignificant amounts that are costly to liquidate
- Collateral Auto-Disable
Disables collateral if entire balance is liquidated
- Debt Token Burning
- Isolation Mode Debt Update
Updates isolated debt accounting for isolated collateral assets
- Collateral Transfer
- Receive aTokens (
params.receiveAToken = true) - Transfers aTokens directly to liquidator
- May automatically enable as collateral for liquidator
- Receive Underlying (
params.receiveAToken = false) - Burns aTokens and transfers underlying asset to liquidator
- Protocol Fee Transfer
- Bad Debt Burning
If borrower has no collateral at all after liquidation and borrower still has some borrowing. It iterates through all borrower's debts and burns them, creating corresponding protocol deficit.
- Debt Asset Transfer
Liquidator transfers the repaid debt assets to protocol
/// --- contracts/protocol/libraries/logic/LiquidationLogic.sol --- /** * @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; /** * @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 { /// Initial Setup 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(); vars.collateralReserveCache = collateralReserve.cache(); debtReserve.updateState(vars.debtReserveCache); /// State Updates collateralReserve.updateState(vars.collateralReserveCache); /// User Account Health Calculation ( vars.totalCollateralInBaseCurrency, vars.totalDebtInBaseCurrency, , , vars.healthFactor, ) = GenericLogic.calculateUserAccountData( reservesData, reservesList, eModeCategories, DataTypes.CalculateUserAccountDataParams({ userConfig: borrowerConfig, user: params.borrower, oracle: params.priceOracle, userEModeCategory: params.borrowerEModeCategory }) ); /// Get Debt and Collateral Amount vars.borrowerCollateralBalance = IAToken(vars.collateralReserveCache.aTokenAddress) .scaledBalanceOf(params.borrower) .getATokenBalance(vars.collateralReserveCache.nextLiquidityIndex); vars.borrowerReserveDebt = IVariableDebtToken(vars.debtReserveCache.variableDebtTokenAddress) .scaledBalanceOf(params.borrower) .getVTokenBalance(vars.debtReserveCache.nextVariableBorrowIndex); /// Liquidation Validation ValidationLogic.validateLiquidationCall( borrowerConfig, collateralReserve, debtReserve, DataTypes.ValidateLiquidationCallParams({ debtReserveCache: vars.debtReserveCache, totalDebt: vars.borrowerReserveDebt, healthFactor: vars.healthFactor, priceOracleSentinel: params.priceOracleSentinel, borrower: params.borrower, liquidator: params.liquidator }) ); /// Liquidation Bonus Determination 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(); } /// Price and Unit Setup 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(); /// Base Currency Conversions of Debt and Collateral vars.borrowerReserveDebtInBaseCurrency = MathUtils.mulDivCeil( vars.borrowerReserveDebt, vars.debtAssetPrice, vars.debtAssetUnit ); // @note floor rounding vars.borrowerReserveCollateralInBaseCurrency = (vars.borrowerCollateralBalance * vars.collateralAssetPrice) / vars.collateralAssetUnit; /// Maximum Liquidatable Debt Calculation // 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; } } /// Calculate Actual Debt and Collateral to Liquidate vars.actualDebtToLiquidate = params.debtToCover > maxLiquidatableDebt ? maxLiquidatableDebt : params.debtToCover; ( vars.actualCollateralToLiquidate, vars.actualDebtToLiquidate, vars.liquidationProtocolFeeAmount, vars.collateralToLiquidateInBaseCurrency ) = _calculateAvailableCollateralToLiquidate( vars.collateralReserveCache.reserveConfiguration, vars.collateralAssetPrice, vars.collateralAssetUnit, vars.debtAssetPrice, vars.debtAssetUnit, vars.actualDebtToLiquidate, vars.borrowerCollateralBalance, vars.liquidationBonus ); /// Dust Prevention Check // 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() ); } /// Collateral Auto-Disable // If the collateral being liquidated is equal to the user balance, // we set the currency as not being used as collateral anymore if ( vars.actualCollateralToLiquidate + vars.liquidationProtocolFeeAmount == vars.borrowerCollateralBalance ) { borrowerConfig.setUsingAsCollateral( collateralReserve.id, params.collateralAsset, params.borrower, false ); } /// Debt Token Burning bool hasNoCollateralLeft = vars.totalCollateralInBaseCurrency == vars.collateralToLiquidateInBaseCurrency; _burnDebtTokens( vars.debtReserveCache, debtReserve, borrowerConfig, params.borrower, params.debtAsset, vars.borrowerReserveDebt, vars.actualDebtToLiquidate, hasNoCollateralLeft, params.interestRateStrategyAddress ); /// Isolation Mode Debt Update // An asset can only be ceiled if it has no supply or if it was not a collateral previously. // Therefore we can be sure that no inconsistent state can be reached in which a user has multiple collaterals, with one being ceiled. // This allows for the implicit assumption that: if the asset was a collateral & the asset was ceiled, the user must have been in isolation. if (vars.collateralReserveCache.reserveConfiguration.getDebtCeiling() != 0) { // IsolationModeTotalDebt only discounts `actualDebtToLiquidate`, not the fully burned amount in case of deficit creation. // This is by design as otherwise the debt ceiling would render ineffective if a collateral asset faces bad debt events. // The governance can decide the raise the ceiling to discount manifested deficit. IsolationModeLogic.updateIsolatedDebt( reservesData, vars.debtReserveCache, vars.actualDebtToLiquidate, params.collateralAsset ); } /// Collateral Transfer if (params.receiveAToken) { _liquidateATokens(reservesData, reservesList, usersConfig, collateralReserve, params, vars); } 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); } /// Protocol Fee Transfer // 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 }); } /// Bad Debt Burning // 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); } /// Debt Asset Transfer // 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 ); }
validateLiquidationCall
validateLiquidationCall validates liquidatioin operation:- Liquidator ≠ Borrower (no self-liquidation)
- Both reserves active and not paused
- Price oracle sentinel allows liquidation
- Liquidation grace period has passed
- Health factor < 1.0 (liquidition threshold reached, unhealthy)
- Collateral is enabled by borrower
- Borrower has the debt
/// --- contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @notice Validates the liquidation action. * @param borrowerConfig The user configuration mapping * @param collateralReserve The reserve data of the collateral * @param debtReserve The reserve data of the debt * @param params Additional parameters needed for the validation */ function validateLiquidationCall( DataTypes.UserConfigurationMap storage borrowerConfig, DataTypes.ReserveData storage collateralReserve, DataTypes.ReserveData storage debtReserve, DataTypes.ValidateLiquidationCallParams memory params ) internal view { ValidateLiquidationCallLocalVars memory vars; require(params.borrower != params.liquidator, Errors.SelfLiquidation()); (vars.collateralReserveActive, , , vars.collateralReservePaused) = collateralReserve .configuration .getFlags(); (vars.principalReserveActive, , , vars.principalReservePaused) = params .debtReserveCache .reserveConfiguration .getFlags(); require(vars.collateralReserveActive && vars.principalReserveActive, Errors.ReserveInactive()); require(!vars.collateralReservePaused && !vars.principalReservePaused, Errors.ReservePaused()); require( params.priceOracleSentinel == address(0) || params.healthFactor < MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD || IPriceOracleSentinel(params.priceOracleSentinel).isLiquidationAllowed(), Errors.PriceOracleSentinelCheckFailed() ); require( collateralReserve.liquidationGracePeriodUntil < uint40(block.timestamp) && debtReserve.liquidationGracePeriodUntil < uint40(block.timestamp), Errors.LiquidationGraceSentinelCheckFailed() ); require( params.healthFactor < HEALTH_FACTOR_LIQUIDATION_THRESHOLD, Errors.HealthFactorNotBelowThreshold() ); vars.isCollateralEnabled = collateralReserve.configuration.getLiquidationThreshold() != 0 && borrowerConfig.isUsingAsCollateral(collateralReserve.id); //if collateral isn't enabled as collateral by user, it cannot be liquidated require(vars.isCollateralEnabled, Errors.CollateralCannotBeLiquidated()); require(params.totalDebt != 0, Errors.SpecifiedCurrencyNotBorrowedByUser()); } /// --- contracts/misc/PriceOracleSentinel.sol --- /// @inheritdoc IPriceOracleSentinel function isLiquidationAllowed() external view override returns (bool) { return _isUpAndGracePeriodPassed(); } /** * @notice Checks the sequencer oracle is healthy: is up and grace period passed. * @return True if the SequencerOracle is up and the grace period passed, false otherwise */ function _isUpAndGracePeriodPassed() internal view returns (bool) { (, int256 answer, uint256 startedAt, , ) = _sequencerOracle.latestRoundData(); return answer == 0 && block.timestamp - startedAt > _gracePeriod; }
_calculateAvailableCollateralToLiquidate
_calculateAvailableCollateralToLiquidate function determines how much collateral can be liquidated given a certain amount of debt to cover, taking into account the liquidation bonus and protocol fees, also the collateral balance of borrower./// --- contracts/protocol/libraries/logic/LiquidationLogic.sol --- /** * @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 * @return The collateral amount to liquidate in the base currency used by the price feed */ 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, 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.percentMul(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; } vars.collateralToLiquidateInBaseCurrency = (vars.collateralAmount * vars.collateralAssetPrice) / collateralAssetUnit; if (vars.liquidationProtocolFeePercentage != 0) { vars.bonusCollateral = vars.collateralAmount - vars.collateralAmount.percentDiv(liquidationBonus); vars.liquidationProtocolFee = vars.bonusCollateral.percentMul( vars.liquidationProtocolFeePercentage ); vars.collateralAmount -= vars.liquidationProtocolFee; } return ( vars.collateralAmount, vars.debtAmountNeeded, vars.liquidationProtocolFee, vars.collateralToLiquidateInBaseCurrency ); }
_burnDebtTokens
_burnDebtTokens burns debt repaid by liquidator. It also checks whether user is in deficit (no any collateral left to pay remaining debt), in such case, it also burns remaining debt and record deficit./// --- contracts/protocol/libraries/logic/LiquidationLogic.sol --- /** * @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 ); }
updateIsolatedDebt
updateIsolatedDebt updates the isolated debt whenever a position collateralized by an isolated asset is liquidated/// --- contracts/protocol/libraries/logic/IsolationModeLogic.sol --- /** * @notice updated the isolated debt whenever a position collateralized by an isolated asset is liquidated * @param reservesData The state of all the reserves * @param reserveCache The cached data of the reserve * @param repayAmount The amount being repaid * @param isolationModeCollateralAddress The address of the isolated collateral */ function updateIsolatedDebt( mapping(address => DataTypes.ReserveData) storage reservesData, DataTypes.ReserveCache memory reserveCache, uint256 repayAmount, address isolationModeCollateralAddress ) internal { uint128 isolationModeTotalDebt = reservesData[isolationModeCollateralAddress] .isolationModeTotalDebt; uint128 isolatedDebtRepaid = convertToIsolatedDebtUnits(reserveCache, repayAmount); // since the debt ceiling does not take into account the interest accrued, it might happen that amount // repaid > debt in isolation mode uint128 newIsolationModeTotalDebt = isolationModeTotalDebt > isolatedDebtRepaid ? isolationModeTotalDebt - isolatedDebtRepaid : 0; setIsolationModeTotalDebt( reservesData[isolationModeCollateralAddress], isolationModeCollateralAddress, newIsolationModeTotalDebt ); }
_liquidateATokens
_liquidateATokens liquidates the user aTokens by transferring them to the liquidator.Also it checks liquidator’s collateral status and decides whether to set the underlying asset of aToken as collateral of liquidator.
/// --- contracts/protocol/libraries/logic/LiquidationLogic.sol --- /** * @notice Liquidates the user aTokens by transferring them to the liquidator. * @dev The function also checks the state of the liquidator and activates the aToken as collateral * as in standard transfers if the isolation mode constraints are respected. * @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 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 _liquidateATokens( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, mapping(address => DataTypes.UserConfigurationMap) storage usersConfig, DataTypes.ReserveData storage collateralReserve, DataTypes.ExecuteLiquidationCallParams memory params, LiquidationCallLocalVars memory vars ) internal { uint256 liquidatorPreviousATokenBalance = IAToken(vars.collateralReserveCache.aTokenAddress) .scaledBalanceOf(params.liquidator); IAToken(vars.collateralReserveCache.aTokenAddress).transferOnLiquidation( params.borrower, params.liquidator, vars.actualCollateralToLiquidate, vars.actualCollateralToLiquidate.getATokenTransferScaledAmount( vars.collateralReserveCache.nextLiquidityIndex ), vars.collateralReserveCache.nextLiquidityIndex ); if (liquidatorPreviousATokenBalance == 0) { DataTypes.UserConfigurationMap storage liquidatorConfig = usersConfig[params.liquidator]; if ( ValidationLogic.validateAutomaticUseAsCollateral( params.liquidator, reservesData, reservesList, liquidatorConfig, vars.collateralReserveCache.reserveConfiguration, vars.collateralReserveCache.aTokenAddress ) ) { liquidatorConfig.setUsingAsCollateral( collateralReserve.id, params.collateralAsset, params.liquidator, true ); } } }
_burnBadDebt
_burnBadDebt burns all user’s bad debt (token), and record them in corresponding reserve’s deficit.Before each debt burning, it first updates state( interest rate indices of debt and liquidity), and updates interest rates(in function
_burnDebtTokens)/// --- contracts/protocol/libraries/logic/LiquidationLogic.sol --- /** * @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 { uint256 cachedBorrowerConfig = borrowerConfig.data; uint256 i = 0; bool isBorrowed = false; while (cachedBorrowerConfig != 0) { (cachedBorrowerConfig, isBorrowed, ) = UserConfiguration.getNextFlags(cachedBorrowerConfig); if (isBorrowed) { address reserveAddress = reservesList[i]; 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; } } }
flashLoan
flashLoan allows user to borrow and repay in a single transaction.The overall logic is same as V2, except in Aave V3, the treasury receives 100% of flash loan fees, and liquidity providers get none of it.
/// --- contracts/protocol/pool/Pool.sol --- function flashLoan( address receiverAddress, address[] calldata assets, uint256[] calldata amounts, uint256[] calldata interestRateModes, address onBehalfOf, bytes calldata params, uint16 referralCode ) public virtual override { DataTypes.FlashloanParams memory flashParams = DataTypes.FlashloanParams({ user: _msgSender(), receiverAddress: receiverAddress, assets: assets, amounts: amounts, interestRateModes: interestRateModes, interestRateStrategyAddress: RESERVE_INTEREST_RATE_STRATEGY, onBehalfOf: onBehalfOf, params: params, referralCode: referralCode, flashLoanPremium: _flashLoanPremium, addressesProvider: address(ADDRESSES_PROVIDER), pool: address(this), userEModeCategory: _usersEModeCategory[onBehalfOf], isAuthorizedFlashBorrower: IACLManager(ADDRESSES_PROVIDER.getACLManager()).isFlashBorrower( _msgSender() ) }); FlashLoanLogic.executeFlashLoan( _reserves, _reservesList, _eModeCategories, _usersConfig[onBehalfOf], flashParams ); }
executeFlashLoan
Steps:
- Initial Validation
- Checks if assets are active reserves
- Verifies amounts don't exceed available liquidity
- Ensures assets array length matches amounts array
- Fee Calculation & Asset Transfer
- Fee Waiver: Authorized flash borrowers pay 0 fee
- Virtual Balance Update: Temporarily reduces reserve balance to prevent reentrancy
- Asset Transfer: Direct transfer asset to receiver contract
- Receiver Execution
Call
executeOperation on receiver to execute arbitrary logic.The receiver must implement
IFlashLoanReceiver and return true for successful execution.- Post-Execution Handling
- Repay with Fee (InterestRateMode.NONE)
- Pulls
amount + premiumfrom receiver - Updates treasury accruals
- Updates interest rates
- Emits
FlashLoanevent - Convert loan to Debt (InterestRateMode.VARIABLE/STABLE)
- No premium paid
- Opens regular debt position for
onBehalfOf - Subject to normal borrowing constraints (health factor, isolation mode, etc.)
Repayment Process:
Debt Conversion:
/// --- contracts/protocol/libraries/logic/FlashLoanLogic.sol --- /** * @notice Implements the flashloan feature that allow users to access liquidity of the pool for one transaction * as long as the amount taken plus fee is returned or debt is opened. * @dev For authorized flashborrowers the fee is waived * @dev At the end of the transaction the pool will pull amount borrowed + fee from the receiver, * if the receiver have not approved the pool the transaction will revert. * @dev Emits the `FlashLoan()` event * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @param eModeCategories The configuration of all the efficiency mode categories * @param userConfig The user configuration mapping that tracks the supplied/borrowed assets * @param params The additional parameters needed to execute the flashloan function */ function executeFlashLoan( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, DataTypes.UserConfigurationMap storage userConfig, DataTypes.FlashloanParams memory params ) external { // The usual action flow (cache -> updateState -> validation -> changeState -> updateRates) // is altered to (validation -> user payload -> cache -> updateState -> changeState -> updateRates) for flashloans. // This is done to protect against reentrance and rate manipulation within the user specified payload. ValidationLogic.validateFlashloan(reservesData, params.assets, params.amounts); FlashLoanLocalVars memory vars; vars.totalPremiums = new uint256[](params.assets.length); vars.receiver = IFlashLoanReceiver(params.receiverAddress); vars.flashloanPremium = params.isAuthorizedFlashBorrower ? 0 : params.flashLoanPremium; for (uint256 i = 0; i < params.assets.length; i++) { vars.currentAmount = params.amounts[i]; vars.totalPremiums[i] = DataTypes.InterestRateMode(params.interestRateModes[i]) == DataTypes.InterestRateMode.NONE ? vars.currentAmount.percentMulCeil(vars.flashloanPremium) : 0; reservesData[params.assets[i]].virtualUnderlyingBalance -= vars.currentAmount.toUint128(); IAToken(reservesData[params.assets[i]].aTokenAddress).transferUnderlyingTo( params.receiverAddress, vars.currentAmount ); } require( vars.receiver.executeOperation( params.assets, params.amounts, vars.totalPremiums, params.user, params.params ), Errors.InvalidFlashloanExecutorReturn() ); for (uint256 i = 0; i < params.assets.length; i++) { vars.currentAsset = params.assets[i]; vars.currentAmount = params.amounts[i]; if ( DataTypes.InterestRateMode(params.interestRateModes[i]) == DataTypes.InterestRateMode.NONE ) { _handleFlashLoanRepayment( reservesData[vars.currentAsset], DataTypes.FlashLoanRepaymentParams({ user: params.user, asset: vars.currentAsset, interestRateStrategyAddress: params.interestRateStrategyAddress, receiverAddress: params.receiverAddress, amount: vars.currentAmount, totalPremium: vars.totalPremiums[i], referralCode: params.referralCode }) ); } else { // If the user chose to not return the funds, the system checks if there is enough collateral and // eventually opens a debt position BorrowLogic.executeBorrow( reservesData, reservesList, eModeCategories, userConfig, DataTypes.ExecuteBorrowParams({ asset: vars.currentAsset, interestRateStrategyAddress: params.interestRateStrategyAddress, user: params.user, onBehalfOf: params.onBehalfOf, amount: vars.currentAmount, interestRateMode: DataTypes.InterestRateMode(params.interestRateModes[i]), referralCode: params.referralCode, releaseUnderlying: false, oracle: IPoolAddressesProvider(params.addressesProvider).getPriceOracle(), userEModeCategory: IPool(params.pool).getUserEMode(params.onBehalfOf).toUint8(), priceOracleSentinel: IPoolAddressesProvider(params.addressesProvider) .getPriceOracleSentinel() }) ); // no premium is paid when taking on the flashloan as debt emit IPool.FlashLoan( params.receiverAddress, params.user, vars.currentAsset, vars.currentAmount, DataTypes.InterestRateMode(params.interestRateModes[i]), 0, params.referralCode ); } } }
validateFlashloan
/// --- contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @notice Validates a flashloan action. * @param reservesData The state of all the reserves * @param assets The assets being flash-borrowed * @param amounts The amounts for each asset being borrowed */ function validateFlashloan( mapping(address => DataTypes.ReserveData) storage reservesData, address[] memory assets, uint256[] memory amounts ) internal view { require(assets.length == amounts.length, Errors.InconsistentFlashloanParams()); for (uint256 i = 0; i < assets.length; i++) { for (uint256 j = i + 1; j < assets.length; j++) { require(assets[i] != assets[j], Errors.InconsistentFlashloanParams()); } validateFlashloanSimple(reservesData[assets[i]], amounts[i]); } } /** * @notice Validates a flashloan action. * @param reserve The state of the reserve */ function validateFlashloanSimple( DataTypes.ReserveData storage reserve, uint256 amount ) internal view { DataTypes.ReserveConfigurationMap memory configuration = reserve.configuration; require(!configuration.getPaused(), Errors.ReservePaused()); require(configuration.getActive(), Errors.ReserveInactive()); require(configuration.getFlashLoanEnabled(), Errors.FlashloanDisabled()); require(IERC20(reserve.aTokenAddress).totalSupply() >= amount, Errors.InvalidAmount()); }
_handleFlashLoanRepayment
_handleFlashLoanRepayment handles the repayment phase of flash loans where the user chooses to return the borrowed amount plus fees rather than converting it to debt.Steps:
- Loan Amount plus Fee Calculation
params.amount: Original flash loan amountparams.totalPremium: Flash loan fee calculated earlieramountPlusPremium: Total amount that must be pulled from the receiver
- Reserve State Update
- Creates a cache of current reserve state for gas optimization
- Updates the reserve's liquidity index and variable borrow index
- Accrues interest to the reserve since last interaction
- Treasury Fee Accrual
- Converts the premium from underlying asset units to aToken scaled units
- Uses
getATokenMintScaledAmount()to account for the current liquidity index - Adds to
accruedToTreasurywhich tracks protocol revenue in scaled aToken units
- Interest Rate & Virtual Balance Update
liquidityAdded = amountPlusPremium: Funds being returned to the poolliquidityTaken = 0: No additional funds being borrowed- Restores virtual balance that was reduced during flash loan issuance
- Recalculates interest rates based on new liquidity levels
- Updates utilization ratios and variable borrow rates
Parameters:
What this does:
- Funds Transfer
- Uses pull pattern rather than push pattern
- Receiver must have pre-approved the pool to withdraw
amount + premium - Transfers directly to aToken contract (where liquidity is stored)
- If transfer fails, entire transaction reverts
Critical Security Aspect:
/// --- contracts/protocol/libraries/logic/FlashLoanLogic.sol --- /** * @notice Handles repayment of flashloaned assets + premium * @dev Will pull the amount + premium from the receiver, so must have approved pool * @param reserve The state of the flashloaned reserve * @param params The additional parameters needed to execute the repayment function */ function _handleFlashLoanRepayment( DataTypes.ReserveData storage reserve, DataTypes.FlashLoanRepaymentParams memory params ) internal { uint256 amountPlusPremium = params.amount + params.totalPremium; DataTypes.ReserveCache memory reserveCache = reserve.cache(); reserve.updateState(reserveCache); reserve.accruedToTreasury += params .totalPremium .getATokenMintScaledAmount(reserveCache.nextLiquidityIndex) .toUint128(); reserve.updateInterestRatesAndVirtualBalance( reserveCache, params.asset, amountPlusPremium, 0, params.interestRateStrategyAddress ); IERC20(params.asset).safeTransferFrom( params.receiverAddress, reserveCache.aTokenAddress, amountPlusPremium ); emit IPool.FlashLoan( params.receiverAddress, params.user, params.asset, params.amount, DataTypes.InterestRateMode.NONE, params.totalPremium, params.referralCode ); }
flashLoanSimple
Compared to
flashLoan function, flashLoanSimple doesn’t provide option to create debt rather than repay loan. So this function saves gas compared to flashLoan if user just wants to borrow and repay./// --- contracts/protocol/pool/Pool.sol --- /// @inheritdoc IPool function flashLoanSimple( address receiverAddress, address asset, uint256 amount, bytes calldata params, uint16 referralCode ) public virtual override { DataTypes.FlashloanSimpleParams memory flashParams = DataTypes.FlashloanSimpleParams({ user: _msgSender(), receiverAddress: receiverAddress, asset: asset, interestRateStrategyAddress: RESERVE_INTEREST_RATE_STRATEGY, amount: amount, params: params, referralCode: referralCode, flashLoanPremium: _flashLoanPremium }); FlashLoanLogic.executeFlashLoanSimple(_reserves[asset], flashParams); }
executeFlashLoanSimple
/// --- contracts/protocol/libraries/logic/FlashLoanLogic.sol --- /** * @notice Implements the simple flashloan feature that allow users to access liquidity of ONE reserve for one * transaction as long as the amount taken plus fee is returned. * @dev Does not waive fee for approved flashborrowers nor allow taking on debt instead of repaying to save gas * @dev At the end of the transaction the pool will pull amount borrowed + fee from the receiver, * if the receiver have not approved the pool the transaction will revert. * @dev Emits the `FlashLoan()` event * @param reserve The state of the flashloaned reserve * @param params The additional parameters needed to execute the simple flashloan function */ function executeFlashLoanSimple( DataTypes.ReserveData storage reserve, DataTypes.FlashloanSimpleParams memory params ) external { // The usual action flow (cache -> updateState -> validation -> changeState -> updateRates) // is altered to (validation -> user payload -> cache -> updateState -> changeState -> updateRates) for flashloans. // This is done to protect against reentrance and rate manipulation within the user specified payload. ValidationLogic.validateFlashloanSimple(reserve, params.amount); IFlashLoanSimpleReceiver receiver = IFlashLoanSimpleReceiver(params.receiverAddress); uint256 totalPremium = params.amount.percentMulCeil(params.flashLoanPremium); reserve.virtualUnderlyingBalance -= params.amount.toUint128(); IAToken(reserve.aTokenAddress).transferUnderlyingTo(params.receiverAddress, params.amount); require( receiver.executeOperation( params.asset, params.amount, totalPremium, params.user, params.params ), Errors.InvalidFlashloanExecutorReturn() ); _handleFlashLoanRepayment( reserve, DataTypes.FlashLoanRepaymentParams({ user: params.user, asset: params.asset, interestRateStrategyAddress: params.interestRateStrategyAddress, receiverAddress: params.receiverAddress, amount: params.amount, totalPremium: totalPremium, referralCode: params.referralCode }) ); }
validateFlashloanSimple
/// --- contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @notice Validates a flashloan action. * @param reserve The state of the reserve */ function validateFlashloanSimple( DataTypes.ReserveData storage reserve, uint256 amount ) internal view { DataTypes.ReserveConfigurationMap memory configuration = reserve.configuration; require(!configuration.getPaused(), Errors.ReservePaused()); require(configuration.getActive(), Errors.ReserveInactive()); require(configuration.getFlashLoanEnabled(), Errors.FlashloanDisabled()); require(IERC20(reserve.aTokenAddress).totalSupply() >= amount, Errors.InvalidAmount()); }
eliminateReserveDeficit
eliminateReserveDeficit can only be called by aave’s unmbrella contract to reduce deficit by burning umbrella’s aToken (staked by users).Umbrella is Aave's upgraded Safety Module that provides automated protection against protocol bad debt while enabling users to earn rewards. Users stake their aTokens to contribute to protocol security, earning additional yields beyond their standard Aave supply returns in exchange for accepting slashing risk.
/// --- contracts/protocol/pool/Pool.sol --- /// @inheritdoc IPool function eliminateReserveDeficit( address asset, uint256 amount ) external override onlyUmbrella returns (uint256) { return LiquidationLogic.executeEliminateDeficit( _reserves, _usersConfig[_msgSender()], DataTypes.ExecuteEliminateDeficitParams({ user: _msgSender(), asset: asset, amount: amount, interestRateStrategyAddress: RESERVE_INTEREST_RATE_STRATEGY }) ); }
executeEliminateDeficit
/// --- contracts/protocol/libraries/logic/LiquidationLogic.sol --- /** * @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; }
setUserEMode
setUserEMode allows user to select to enter into a eMode.It requires that currently borrowed asset of user are all allowed in the chosen eMode.
/// --- contracts/protocol/pool/Pool.sol --- /// @inheritdoc IPool function setUserEMode(uint8 categoryId) external virtual override { EModeLogic.executeSetUserEMode( _reserves, _reservesList, _eModeCategories, _usersEModeCategory, _usersConfig[_msgSender()], _msgSender(), ADDRESSES_PROVIDER.getPriceOracle(), categoryId ); }
executeSetUserEMode
/// --- contracts/protocol/libraries/logic/EModeLogic.sol --- /** * @notice Updates the user efficiency mode category * @dev Will revert if user is borrowing non-compatible asset or change will drop HF < HEALTH_FACTOR_LIQUIDATION_THRESHOLD * @dev Emits the `UserEModeSet` event * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @param eModeCategories The configuration of all the efficiency mode categories * @param usersEModeCategory The state of all users efficiency mode category * @param userConfig The user configuration mapping that tracks the supplied/borrowed assets * @param user The selected user * @param oracle The address of the oracle * @param categoryId The selected eMode categoryId */ function executeSetUserEMode( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, mapping(address => uint8) storage usersEModeCategory, DataTypes.UserConfigurationMap storage userConfig, address user, address oracle, uint8 categoryId ) external { if (usersEModeCategory[user] == categoryId) return; ValidationLogic.validateSetUserEMode(eModeCategories, userConfig, categoryId); usersEModeCategory[user] = categoryId; ValidationLogic.validateHealthFactor( reservesData, reservesList, eModeCategories, userConfig, user, categoryId, oracle ); emit IPool.UserEModeSet(user, categoryId); }
validateSetUserEMode
/// --- contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @notice Validates the action of setting efficiency mode. * @param eModeCategories a mapping storing configurations for all efficiency mode categories * @param userConfig the user configuration * @param categoryId The id of the category */ function validateSetUserEMode( mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, DataTypes.UserConfigurationMap memory userConfig, uint8 categoryId ) internal view { DataTypes.EModeCategory storage eModeCategory = eModeCategories[categoryId]; // category is invalid if the liq threshold is not set require( categoryId == 0 || eModeCategory.liquidationThreshold != 0, Errors.InconsistentEModeCategory() ); // eMode can always be enabled if the user hasn't supplied anything if (userConfig.isEmpty()) { return; } // if user is trying to set another category than default we require that // either the user is not borrowing, or it's borrowing assets of categoryId if (categoryId != 0) { uint256 i = 0; bool isBorrowed = false; uint128 cachedBorrowableBitmap = eModeCategory.borrowableBitmap; uint256 cachedUserConfig = userConfig.data; unchecked { while (cachedUserConfig != 0) { (cachedUserConfig, isBorrowed, ) = UserConfiguration.getNextFlags(cachedUserConfig); if (isBorrowed) { require( EModeConfiguration.isReserveEnabledOnBitmap(cachedBorrowableBitmap, i), Errors.NotBorrowableInEMode() ); } ++i; } } } }
finalizeTransfer
finalizeTransfer checks whether the transfer opeartion is allowed, and update related states.- checks whether the aToken is not allowed to transfer
- checks whether
from’s portfolia is healthy after transfer
- If
fromtransfers all aToken out, it updates corresponding collateral flag offromto mark it’s not used anymore
- if
tohas no such aToken brfore, it checks whethertocan use this as collateral and update related collateral flag.
/// --- contracts/protocol/pool/Pool.sol --- /// @inheritdoc IPool function finalizeTransfer( address asset, address from, address to, uint256 scaledAmount, uint256 scaledBalanceFromBefore, uint256 scaledBalanceToBefore ) external virtual override { require(_msgSender() == _reserves[asset].aTokenAddress, Errors.CallerNotAToken()); SupplyLogic.executeFinalizeTransfer( _reserves, _reservesList, _eModeCategories, _usersConfig, DataTypes.FinalizeTransferParams({ asset: asset, from: from, to: to, scaledAmount: scaledAmount, scaledBalanceFromBefore: scaledBalanceFromBefore, scaledBalanceToBefore: scaledBalanceToBefore, oracle: ADDRESSES_PROVIDER.getPriceOracle(), fromEModeCategory: _usersEModeCategory[from] }) ); }
executeFinalizeTransfer
/// --- contracts/protocol/libraries/logic/SupplyLogic.sol --- /** * @notice Validates a transfer of aTokens. The sender is subjected to health factor validation to avoid * collateralization constraints violation. * @dev Emits the `ReserveUsedAsCollateralEnabled()` event for the `to` account, if the asset is being activated as * collateral. * @dev In case the `from` user transfers everything, `ReserveUsedAsCollateralDisabled()` is emitted for `from`. * @param reservesData The state of all the reserves * @param reservesList The addresses of all the active reserves * @param eModeCategories The configuration of all the efficiency mode categories * @param usersConfig The users configuration mapping that track the supplied/borrowed assets * @param params The additional parameters needed to execute the finalizeTransfer function */ function executeFinalizeTransfer( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories, mapping(address => DataTypes.UserConfigurationMap) storage usersConfig, DataTypes.FinalizeTransferParams memory params ) external { DataTypes.ReserveData storage reserve = reservesData[params.asset]; ValidationLogic.validateTransfer(reserve); uint256 reserveId = reserve.id; if (params.from != params.to && params.scaledAmount != 0) { DataTypes.UserConfigurationMap storage fromConfig = usersConfig[params.from]; if (fromConfig.isUsingAsCollateral(reserveId)) { if (params.scaledBalanceFromBefore == params.scaledAmount) { fromConfig.setUsingAsCollateral(reserveId, params.asset, params.from, false); } if (fromConfig.isBorrowingAny()) { ValidationLogic.validateHFAndLtvzero( reservesData, reservesList, eModeCategories, usersConfig[params.from], params.asset, params.from, params.oracle, params.fromEModeCategory ); } } if (params.scaledBalanceToBefore == 0) { DataTypes.UserConfigurationMap storage toConfig = usersConfig[params.to]; if ( ValidationLogic.validateAutomaticUseAsCollateral( params.from, reservesData, reservesList, toConfig, reserve.configuration, reserve.aTokenAddress ) ) { toConfig.setUsingAsCollateral(reserveId, params.asset, params.to, true); } } } }
validateTransfer
/// --- contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @notice Validates a transfer action. * @param reserve The reserve object */ function validateTransfer(DataTypes.ReserveData storage reserve) internal view { require(!reserve.configuration.getPaused(), Errors.ReservePaused()); }
AToken
mint
/// --- contracts/protocol/tokenization/AToken.sol --- /// @inheritdoc IAToken function mint( address caller, address onBehalfOf, uint256 scaledAmount, uint256 index ) external virtual override onlyPool returns (bool) { return _mintScaled({ caller: caller, onBehalfOf: onBehalfOf, amountScaled: scaledAmount, index: index, getTokenBalance: TokenMath.getATokenBalance }); } /** * @notice Implements the basic logic to mint a scaled balance token. * @param caller The address performing the mint * @param onBehalfOf The address of the user that will receive the scaled tokens * @param amountScaled The amountScaled of tokens getting minted * @param index The next liquidity index of the reserve * @param getTokenBalance The function to get the balance of the token * @return `true` if the the previous balance of the user was 0 */ function _mintScaled( address caller, address onBehalfOf, uint256 amountScaled, uint256 index, function(uint256, uint256) internal pure returns (uint256) getTokenBalance ) internal returns (bool) { require(amountScaled != 0, Errors.InvalidMintAmount()); uint256 scaledBalance = super.balanceOf(onBehalfOf); uint256 nextBalance = getTokenBalance(amountScaled + scaledBalance, index); uint256 previousBalance = getTokenBalance(scaledBalance, _userState[onBehalfOf].additionalData); uint256 balanceIncrease = getTokenBalance(scaledBalance, index) - previousBalance; _userState[onBehalfOf].additionalData = index.toUint128(); _mint(onBehalfOf, amountScaled.toUint120()); uint256 amountToMint = nextBalance - previousBalance; emit Transfer(address(0), onBehalfOf, amountToMint); emit Mint(caller, onBehalfOf, amountToMint, balanceIncrease, index); return (scaledBalance == 0); } /// --- contracts/protocol/libraries/helpers/TokenMath.sol --- /** * @notice Calculates the actual aToken balance from a scaled balance and the current liquidityIndex. * The balance is rounded down to prevent overaccounting. * @param scaledAmount The scaled aToken balance. * @param liquidityIndex The current aToken liquidityIndex. * @return The actual aToken balance. */ function getATokenBalance( uint256 scaledAmount, uint256 liquidityIndex ) internal pure returns (uint256) { return scaledAmount.rayMulFloor(liquidityIndex); }
burn
/// --- contracts/protocol/tokenization/AToken.sol --- /// @inheritdoc IAToken function burn( address from, address receiverOfUnderlying, uint256 amount, uint256 scaledAmount, uint256 index ) external virtual override onlyPool returns (bool) { bool zeroBalanceAfterBurn = _burnScaled({ user: from, target: receiverOfUnderlying, amountScaled: scaledAmount, index: index, getTokenBalance: TokenMath.getATokenBalance }); if (receiverOfUnderlying != address(this)) { IERC20(_underlyingAsset).safeTransfer(receiverOfUnderlying, amount); } return zeroBalanceAfterBurn; } /** * @notice Implements the basic logic to burn a scaled balance token. * @dev In some instances, a burn transaction will emit a mint event * if the amount to burn is less than the interest that the user accrued * @param user The user which debt is burnt * @param target The address that will receive the underlying, if any * @param amountScaled The scaled amount getting burned * @param index The variable debt index of the reserve * @param getTokenBalance The function to get the balance of the token * @return `true` if the the new balance of the user is 0 */ function _burnScaled( address user, address target, uint256 amountScaled, uint256 index, function(uint256, uint256) internal pure returns (uint256) getTokenBalance ) internal returns (bool) { require(amountScaled != 0, Errors.InvalidBurnAmount()); uint256 scaledBalance = super.balanceOf(user); uint256 nextBalance = getTokenBalance(scaledBalance - amountScaled, index); uint256 previousBalance = getTokenBalance(scaledBalance, _userState[user].additionalData); uint256 balanceIncrease = getTokenBalance(scaledBalance, index) - previousBalance; _userState[user].additionalData = index.toUint128(); _burn(user, amountScaled.toUint120()); if (nextBalance > previousBalance) { uint256 amountToMint = nextBalance - previousBalance; emit Transfer(address(0), user, amountToMint); emit Mint(user, user, amountToMint, balanceIncrease, index); } else { uint256 amountToBurn = previousBalance - nextBalance; emit Transfer(user, address(0), amountToBurn); emit Burn(user, target, amountToBurn, balanceIncrease, index); } return scaledBalance - amountScaled == 0; }
transferUnderlyingTo
transferUnderlyingTo function transfers underlying aToken asset to target./// --- contracts/protocol/tokenization/AToken.sol --- /// @inheritdoc IAToken function transferUnderlyingTo(address target, uint256 amount) external virtual override onlyPool { IERC20(_underlyingAsset).safeTransfer(target, amount); }
transferOnLiquidation
/// --- contracts/protocol/tokenization/AToken.sol --- /// @inheritdoc IAToken function transferOnLiquidation( address from, address to, uint256 amount, uint256 scaledAmount, uint256 index ) external virtual override onlyPool { _transfer({ sender: from, recipient: to, amount: amount, scaledAmount: scaledAmount.toUint120(), index: index }); } /** * @notice Overrides the parent _transfer to force validated transfer() and transferFrom() * @param from The source address * @param to The destination address * @param amount The amount getting transferred */ function _transfer(address from, address to, uint120 amount) internal virtual override { address underlyingAsset = _underlyingAsset; uint256 index = POOL.getReserveNormalizedIncome(underlyingAsset); uint256 scaledBalanceFromBefore = super.balanceOf(from); uint256 scaledBalanceToBefore = super.balanceOf(to); uint256 scaledAmount = uint256(amount).getATokenTransferScaledAmount(index); _transfer({ sender: from, recipient: to, amount: amount, scaledAmount: scaledAmount.toUint120(), index: index }); POOL.finalizeTransfer({ asset: underlyingAsset, from: from, to: to, scaledAmount: scaledAmount, scaledBalanceFromBefore: scaledBalanceFromBefore, scaledBalanceToBefore: scaledBalanceToBefore }); }
VariableDebtToken
mint
/// --- contracts/protocol/tokenization/VariableDebtToken.sol --- /// @inheritdoc IVariableDebtToken function mint( address user, address onBehalfOf, uint256 amount, uint256 scaledAmount, uint256 index ) external virtual override onlyPool returns (uint256) { uint256 scaledBalanceOfUser = super.balanceOf(user); if (user != onBehalfOf) { // This comment explains the logic behind the borrow allowance spent calculation. // // Problem: // Simply decreasing the allowance by the input `amount` is not ideal for scaled-balance tokens. // Due to rounding, the actual increase in the user's debt (`debt_increase`) can be slightly // larger than the input `amount`. // // Definitions: // - `amount`: The unscaled amount to be borrowed, passed as the `amount` argument. // - `debt_increase`: The actual unscaled debt increase for the user. // - `allowance_spent`: The unscaled amount deducted from the delegatee's borrow allowance. Equivalent to `debt_increase`. // // Solution: // To handle this, `allowance_spent` must be exactly equal to `debt_increase`. // We calculate `debt_increase` precisely by simulating the effect of the borrow on the user's balance. // By passing `debt_increase` to `_decreaseBorrowAllowance`, we ensure `allowance_spent` is as close as possible to `debt_increase`. // // Backward Compatibility & Guarantees: // This implementation is backward-compatible and secure. The `_decreaseBorrowAllowance` function has a critical feature: // 1. It REQUIRES the borrow allowance to be >= `amount` (the user's requested borrow amount). // 2. The amount consumed from the allowance is `debt_increase`, but it is capped at the `currentAllowance`. // This means if a user has a borrow allowance of 100 wei and `borrow` is called with an `amount` of 100, the call will succeed // even if the calculated `debt_increase` is 101 wei. In that specific scenario, the allowance consumed will be 100 wei (since that is the `currentAllowance`), // and the transaction will not revert. But if the allowance is 101 wei, then the allowance consumed will be 101 wei. // // uint256 debt_increase = balanceAfter - balanceBefore = (scaledBalanceOfUser + scaledAmount).getVTokenBalance(index) - scaledBalanceOfUser.getVTokenBalance(index); // Due to limitations of the solidity compiler, the calculation is inlined for gas efficiency. _decreaseBorrowAllowance( onBehalfOf, user, amount, (scaledBalanceOfUser + scaledAmount).getVTokenBalance(index) - scaledBalanceOfUser.getVTokenBalance(index) ); } _mintScaled({ caller: user, onBehalfOf: onBehalfOf, amountScaled: scaledAmount, index: index, getTokenBalance: TokenMath.getVTokenBalance }); return scaledTotalSupply(); } /** * @notice Decreases the borrow allowance of a user on the specific debt token. * @param delegator The address delegating the borrowing power * @param delegatee The address receiving the delegated borrowing power * @param amount The minimum amount to subtract from the current allowance * @param correctedAmount The maximum amount to subtract from the current allowance */ function _decreaseBorrowAllowance( address delegator, address delegatee, uint256 amount, uint256 correctedAmount ) internal { uint256 oldBorrowAllowance = _borrowAllowances[delegator][delegatee]; if (oldBorrowAllowance < amount) { revert InsufficientBorrowAllowance(delegatee, oldBorrowAllowance, amount); } uint256 consumption = oldBorrowAllowance >= correctedAmount ? correctedAmount : oldBorrowAllowance; uint256 newAllowance = oldBorrowAllowance - consumption; _borrowAllowances[delegator][delegatee] = newAllowance; emit BorrowAllowanceDelegated(delegator, delegatee, _underlyingAsset, newAllowance); } /** * @notice Implements the basic logic to mint a scaled balance token. * @param caller The address performing the mint * @param onBehalfOf The address of the user that will receive the scaled tokens * @param amountScaled The amountScaled of tokens getting minted * @param index The next liquidity index of the reserve * @param getTokenBalance The function to get the balance of the token * @return `true` if the the previous balance of the user was 0 */ function _mintScaled( address caller, address onBehalfOf, uint256 amountScaled, uint256 index, function(uint256, uint256) internal pure returns (uint256) getTokenBalance ) internal returns (bool) { require(amountScaled != 0, Errors.InvalidMintAmount()); uint256 scaledBalance = super.balanceOf(onBehalfOf); uint256 nextBalance = getTokenBalance(amountScaled + scaledBalance, index); uint256 previousBalance = getTokenBalance(scaledBalance, _userState[onBehalfOf].additionalData); uint256 balanceIncrease = getTokenBalance(scaledBalance, index) - previousBalance; _userState[onBehalfOf].additionalData = index.toUint128(); _mint(onBehalfOf, amountScaled.toUint120()); uint256 amountToMint = nextBalance - previousBalance; emit Transfer(address(0), onBehalfOf, amountToMint); emit Mint(caller, onBehalfOf, amountToMint, balanceIncrease, index); return (scaledBalance == 0); }
Data
/// --- src/contracts/protocol/libraries/types/DataTypes.sol --- struct ReserveData { //stores the reserve configuration ReserveConfigurationMap configuration; //the liquidity index. Expressed in ray uint128 liquidityIndex; //the current supply rate. Expressed in ray uint128 currentLiquidityRate; //variable borrow index. Expressed in ray uint128 variableBorrowIndex; //the current variable borrow rate. Expressed in ray uint128 currentVariableBorrowRate; /// @notice reused `__deprecatedStableBorrowRate` storage from pre 3.2 // the current accumulate deficit in underlying tokens uint128 deficit; //timestamp of last update uint40 lastUpdateTimestamp; //the id of the reserve. Represents the position in the list of the active reserves uint16 id; //timestamp until when liquidations are not allowed on the reserve, if set to past liquidations will be allowed uint40 liquidationGracePeriodUntil; //aToken address address aTokenAddress; // DEPRECATED on v3.2.0 address __deprecatedStableDebtTokenAddress; //variableDebtToken address address variableDebtTokenAddress; // DEPRECATED on v3.4.0, should use the `RESERVE_INTEREST_RATE_STRATEGY` variable from the Pool contract address __deprecatedInterestRateStrategyAddress; //the current treasury balance, scaled uint128 accruedToTreasury; // In aave 3.3.0 this storage slot contained the `unbacked` uint128 virtualUnderlyingBalance; //the outstanding debt borrowed against this asset in isolation mode uint128 isolationModeTotalDebt; //the amount of underlying accounted for by the protocol // DEPRECATED on v3.4.0. Moved into the same slot as accruedToTreasury for optimized storage access. uint128 __deprecatedVirtualUnderlyingBalance; } struct ReserveConfigurationMap { //bit 0-15: LTV //bit 16-31: Liq. threshold //bit 32-47: Liq. bonus //bit 48-55: Decimals //bit 56: reserve is active //bit 57: reserve is frozen //bit 58: borrowing is enabled //bit 59: DEPRECATED: stable rate borrowing enabled //bit 60: asset is paused //bit 61: borrowing in isolation mode is enabled //bit 62: siloed borrowing enabled //bit 63: flashloaning enabled //bit 64-79: reserve factor //bit 80-115: borrow cap in whole tokens, borrowCap == 0 => no cap //bit 116-151: supply cap in whole tokens, supplyCap == 0 => no cap //bit 152-167: liquidation protocol fee //bit 168-175: DEPRECATED: eMode category //bit 176-211: DEPRECATED: unbacked mint cap //bit 212-251: debt ceiling for isolation mode with (ReserveConfiguration::DEBT_CEILING_DECIMALS) decimals //bit 252: DEPRECATED: virtual accounting is enabled for the reserve //bit 253-255 unused uint256 data; }