LendingPool
deposit
LendingPool.deposit function allows user to deposit liquidity into protocol and earn liquidity fee. It calculates corresponding AToken represents liquidity to user, and update interest and liquidity index and newest stable, variable and liquidity interest rate.Steps:
- Access Control & State Validation
whenNotPausedmodifier ensures protocol is active
- Validation
- Checks deposit amount is non-zero
- Verifies reserve is active
- Ensures reserve is not frozen
Validates deposit conditions
- State Updates
reserve.updateStateupdates reserve indices and accrues interest - Gets current scaled variable debt
- Updates liquidity and borrow indices based on time elapsed and rates
- Mints protocol fees to treasury (protocol fee is also represented in AToken)
- Calls the reserve's interest rate strategy contract to calcualte rate based on current utilization ratio
- Updates
currentLiquidityRate,currentStableBorrowRate,currentVariableBorrowRate
reserve.updateInterestRates recalculates interest rates after liquidity change- Receives Liquidity From Tx Sender
- Mint AToken to User
- Scales the deposit amount by the current liquidity index
- Mints the scaled amount to the user
- Returns whether this is the user's first deposit
AToken represents claim to underlying liquidity. It automatically calculates accumulated interest.
AToken.mint mints aTokens to the depositor- Update User Collateral Configuration
- Uses bit manipulation to set the collateral flag for the specific reserve
- Each reserve uses 2 bits: one for borrowing, one for collateral
Sets collateral usage in user's bitmap
/// --- v2/contract/protocol-v2/contracts/protocol/lendingpool/LendingPool.sol --- /** * @dev Deposits an `amount` of underlying asset into the reserve, receiving in return overlying aTokens. * - E.g. User deposits 100 USDC and gets in return 100 aUSDC * @param asset The address of the underlying asset to deposit * @param amount The amount to be deposited * @param onBehalfOf The address that will receive the aTokens, same as msg.sender if the user * wants to receive them on his own wallet, or a different address if the beneficiary of aTokens * is a different wallet * @param referralCode Code used to register the integrator originating the operation, for potential rewards. * 0 if the action is executed directly by the user, without any middle-man **/ function deposit( address asset, uint256 amount, address onBehalfOf, uint16 referralCode ) external override whenNotPaused { DataTypes.ReserveData storage reserve = _reserves[asset]; ValidationLogic.validateDeposit(reserve, amount); address aToken = reserve.aTokenAddress; reserve.updateState(); reserve.updateInterestRates(asset, aToken, amount, 0); IERC20(asset).safeTransferFrom(msg.sender, aToken, amount); bool isFirstDeposit = IAToken(aToken).mint(onBehalfOf, amount, reserve.liquidityIndex); if (isFirstDeposit) { _usersConfig[onBehalfOf].setUsingAsCollateral(reserve.id, true); emit ReserveUsedAsCollateralEnabled(asset, onBehalfOf); } emit Deposit(asset, msg.sender, onBehalfOf, amount, referralCode); } /// --- contract/protocol-v2/contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @dev Validates a deposit action * @param reserve The reserve object on which the user is depositing * @param amount The amount to be deposited */ function validateDeposit(DataTypes.ReserveData storage reserve, uint256 amount) external view { (bool isActive, bool isFrozen, , ) = reserve.configuration.getFlags(); require(amount != 0, Errors.VL_INVALID_AMOUNT); require(isActive, Errors.VL_NO_ACTIVE_RESERVE); require(!isFrozen, Errors.VL_RESERVE_FROZEN); }
Withdraw
The
withdraw function allows users to redeem their aTokens for the underlying asset, effectively removing liquidity from the protocol while accounting for accrued interest and maintaining proper collateralization ratios.Steps:
- Reserve Data Retrieval
- Gets the reserve configuration and aToken address
- Uses storage pointer for gas efficiency
- User Balance Check and Amount Calculation
- Specific amount: Withdraw exact amount
type(uint256).max: Withdraw entire balance (convenience feature)
Flexible Withdrawal Options:
- Comprehensive Withdrawal Validation
- Basic Validation:
- Amount ≠ 0
- Amount ≤ user balance
- Reserve is active
- Health Factor Validation:
What
validateWithdraw Checks:Calls
GenericLogic.balanceDecreaseAllowed() to check withdrawal doesn't make user's position unhealthy (refer)- State and Interest Rate Updates
- Updates liquidity and variable borrow indices
- Mints protocol fees to treasury
- Recalculates rates based on new liquidity (less available)
- Typically increases borrowing rates due to reduced liquidity
updateState():updateInterestRates():- Collateral Management
If withdrawing entire balance, updates user configuration bitmap to disable this asset as collateral
- aToken Burning and Asset Transfer
- Calculates Scaled Amount:
- Burns aTokens: Reduces user's aToken balance
- Transfers Underlying: Sends actual asset to
toaddress specified by user
What
burn() Does:/// --- contract/protocol-v2/contracts/protocol/lendingpool/LendingPool.sol --- /** * @dev Withdraws an `amount` of underlying asset from the reserve, burning the equivalent aTokens owned * E.g. User has 100 aUSDC, calls withdraw() and receives 100 USDC, burning the 100 aUSDC * @param asset The address of the underlying asset to withdraw * @param amount The underlying amount to be withdrawn * - Send the value type(uint256).max in order to withdraw the whole aToken balance * @param to Address that will receive the underlying, same as msg.sender if the user * wants to receive it on his own wallet, or a different address if the beneficiary is a * different wallet * @return The final amount withdrawn **/ function withdraw( address asset, uint256 amount, address to ) external override whenNotPaused returns (uint256) { /// Reserve Data Retrieval DataTypes.ReserveData storage reserve = _reserves[asset]; address aToken = reserve.aTokenAddress; /// User Balance Check and Amount Calculation uint256 userBalance = IAToken(aToken).balanceOf(msg.sender); uint256 amountToWithdraw = amount; if (amount == type(uint256).max) { amountToWithdraw = userBalance; } /// Comprehensive Withdrawal Validation ValidationLogic.validateWithdraw( asset, amountToWithdraw, userBalance, _reserves, _usersConfig[msg.sender], _reservesList, _reservesCount, _addressesProvider.getPriceOracle() ); /// State and Interest Rate Updates reserve.updateState(); reserve.updateInterestRates(asset, aToken, 0, amountToWithdraw); /// Collateral Management if (amountToWithdraw == userBalance) { _usersConfig[msg.sender].setUsingAsCollateral(reserve.id, false); emit ReserveUsedAsCollateralDisabled(asset, msg.sender); } /// aToken Burning and Asset Transfer IAToken(aToken).burn(msg.sender, to, amountToWithdraw, reserve.liquidityIndex); emit Withdraw(asset, msg.sender, to, amountToWithdraw); return amountToWithdraw; } /** * @dev Validates a withdraw action * @param reserveAddress The address of the reserve * @param amount The amount to be withdrawn * @param userBalance The balance of the user * @param reservesData The reserves state * @param userConfig The user configuration * @param reserves The addresses of the reserves * @param reservesCount The number of reserves * @param oracle The price oracle */ function validateWithdraw( address reserveAddress, uint256 amount, uint256 userBalance, mapping(address => DataTypes.ReserveData) storage reservesData, DataTypes.UserConfigurationMap storage userConfig, mapping(uint256 => address) storage reserves, uint256 reservesCount, address oracle ) external view { require(amount != 0, Errors.VL_INVALID_AMOUNT); require(amount <= userBalance, Errors.VL_NOT_ENOUGH_AVAILABLE_USER_BALANCE); (bool isActive, , , ) = reservesData[reserveAddress].configuration.getFlags(); require(isActive, Errors.VL_NO_ACTIVE_RESERVE); require( GenericLogic.balanceDecreaseAllowed( reserveAddress, msg.sender, amount, reservesData, userConfig, reserves, reservesCount, oracle ), Errors.VL_TRANSFER_NOT_ALLOWED ); }
Borrow
The
borrow function allows users to borrow assets from the protocol against their collateral, with comprehensive risk checks to ensure the borrowing position remains healthy.- Note that
msg.senderborrow on behalf ofonBehalfOf, which meansmsg.senderreceives underlying asset andonBehalfOfreceives debt token. The collateral health chech is ononBehalfOfrather thanmsg.sender. During debt token mint phase, debt token contract checks whethermsg.senderhas enough debt allowance fromonBehalfOf.
Parameters:
asset: The cryptocurrency to borrow (USDC, ETH, etc.)
amount: Borrow amount
interestRateMode: 1 for Stable, 2 for Variable
referralCode: Partner tracking
onBehalfOf: Can borrow for another address (credit delegation)
Steps:
- Reserve Data Retrieval
- Gets reserve configuration from storage mapping
- Uses storage pointer for gas efficiency
- Internal Borrow Execution
call
_executeBorrow to execute borrow logic./// --- contract/protocol-v2/contracts/protocol/lendingpool/LendingPool.sol --- struct ExecuteBorrowParams { address asset; address user; address onBehalfOf; uint256 amount; uint256 interestRateMode; address aTokenAddress; uint16 referralCode; bool releaseUnderlying; } /** * @dev Allows users to borrow a specific `amount` of the reserve underlying asset, provided that the borrower * already deposited enough collateral, or he was given enough allowance by a credit delegator on the * corresponding debt token (StableDebtToken or VariableDebtToken) * - E.g. User borrows 100 USDC passing as `onBehalfOf` his own address, receiving the 100 USDC in his wallet * and 100 stable/variable debt tokens, depending on the `interestRateMode` * @param asset The address of the underlying asset to borrow * @param amount The amount to be borrowed * @param interestRateMode The interest rate mode at which the user wants to borrow: 1 for Stable, 2 for Variable * @param referralCode Code used to register the integrator originating the operation, for potential rewards. * 0 if the action is executed directly by the user, without any middle-man * @param onBehalfOf Address of the user who will receive the debt. Should be the address of the borrower itself * calling the function if he wants to borrow against his own collateral, or the address of the credit delegator * if he has been given credit delegation allowance **/ function borrow( address asset, uint256 amount, uint256 interestRateMode, uint16 referralCode, address onBehalfOf ) external override whenNotPaused { DataTypes.ReserveData storage reserve = _reserves[asset]; _executeBorrow( ExecuteBorrowParams( asset, msg.sender, onBehalfOf, amount, interestRateMode, reserve.aTokenAddress, referralCode, true ) ); }
_executeBorrow
Steps:
- Parameter Setup and Oracle Usage
Use oracle to get asset price, and converts borrow amount to ETH value for collateral calculations
- Comprehensive Borrow Validation
use function
validateBorrow to check borrow constraint.- Reserve State Update
- Debt Token Minting
- Stable Debt Token: Fixed interest rate, recorded at borrow time
- Variable Debt Token: Floating rate, uses index for interest accrual
- User Configuration Update
For user’s first borrow, update bitmap to enable borrow tracking for this reserve
- Interest Rate Recalculation
Borrowing reduces available liquidity, recalculate interest rate.
- Asset Transfer to Borrower
Transfers borrowed asset from aToken contract to user.
/// --- contract/protocol-v2/contracts/protocol/lendingpool/LendingPool.sol --- function _executeBorrow(ExecuteBorrowParams memory vars) internal { /// Parameter Setup and Oracle Usage DataTypes.ReserveData storage reserve = _reserves[vars.asset]; DataTypes.UserConfigurationMap storage userConfig = _usersConfig[vars.onBehalfOf]; address oracle = _addressesProvider.getPriceOracle(); uint256 amountInETH = IPriceOracleGetter(oracle).getAssetPrice(vars.asset).mul(vars.amount).div( 10**reserve.configuration.getDecimals() ); /// Comprehensive Borrow Validation ValidationLogic.validateBorrow( vars.asset, reserve, vars.onBehalfOf, vars.amount, amountInETH, vars.interestRateMode, _maxStableRateBorrowSizePercent, _reserves, userConfig, _reservesList, _reservesCount, oracle ); /// Reserve State Update reserve.updateState(); uint256 currentStableRate = 0; /// Debt Token Minting bool isFirstBorrowing = false; if (DataTypes.InterestRateMode(vars.interestRateMode) == DataTypes.InterestRateMode.STABLE) { currentStableRate = reserve.currentStableBorrowRate; isFirstBorrowing = IStableDebtToken(reserve.stableDebtTokenAddress).mint( vars.user, vars.onBehalfOf, vars.amount, currentStableRate ); } else { isFirstBorrowing = IVariableDebtToken(reserve.variableDebtTokenAddress).mint( vars.user, vars.onBehalfOf, vars.amount, reserve.variableBorrowIndex ); } /// User Configuration Update if (isFirstBorrowing) { userConfig.setBorrowing(reserve.id, true); } /// Interest Rate Recalculation reserve.updateInterestRates( vars.asset, vars.aTokenAddress, 0, vars.releaseUnderlying ? vars.amount : 0 ); /// Asset Transfer to Borrower if (vars.releaseUnderlying) { IAToken(vars.aTokenAddress).transferUnderlyingTo(vars.user, vars.amount); } emit Borrow( vars.asset, vars.user, vars.onBehalfOf, vars.amount, vars.interestRateMode, DataTypes.InterestRateMode(vars.interestRateMode) == DataTypes.InterestRateMode.STABLE ? currentStableRate : reserve.currentVariableBorrowRate, vars.referralCode ); }
validateBorrow
The
validateBorrow function is the core risk engine of Aave's lending protocol, performing comprehensive checks to ensure borrowing operations maintain protocol solvency and user safety.Steps:
- Local Variables Declaration
Temporary storage for gas optimization and readability
- Reserve Status Checks
- Active: Reserve must be initialized and active
- Not Frozen: Emergency freeze protection
- Non-zero Amount: Prevent empty transactions
- Borrowing Enabled: Reserve-specific borrowing permission
- Interest Rate Mode Validation
Prevents invalid mode selection
- User Account Health Analysis
- Use function
calculateUserAccountDatato get user’s profile. - checks user has collateral and healthy portfolia.
- Collateral Sufficiency Check
Check user has enough collateral after borrow based on LTV.
- Stable Rate Borrowing Specific Checks
- It prevents such a scenario: user deposits reserve A and borrow reserve A in a smaller amount, as this is a meanningless operation.
- Stable Rate Borrowing Limit
/// --- contract/protocol-v2/contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @dev Validates a borrow action * @param asset The address of the asset to borrow * @param reserve The reserve state from which the user is borrowing * @param userAddress The address of the user * @param amount The amount to be borrowed * @param amountInETH The amount to be borrowed, in ETH * @param interestRateMode The interest rate mode at which the user is borrowing * @param maxStableLoanPercent The max amount of the liquidity that can be borrowed at stable rate, in percentage * @param reservesData The state of all the reserves * @param userConfig The state of the user for the specific reserve * @param reserves The addresses of all the active reserves * @param oracle The price oracle */ function validateBorrow( address asset, DataTypes.ReserveData storage reserve, address userAddress, uint256 amount, uint256 amountInETH, uint256 interestRateMode, uint256 maxStableLoanPercent, mapping(address => DataTypes.ReserveData) storage reservesData, DataTypes.UserConfigurationMap storage userConfig, mapping(uint256 => address) storage reserves, uint256 reservesCount, address oracle ) external view { /// Local Variables Declaration ValidateBorrowLocalVars memory vars; /// Reserve Status Checks (vars.isActive, vars.isFrozen, vars.borrowingEnabled, vars.stableRateBorrowingEnabled) = reserve .configuration .getFlags(); require(vars.isActive, Errors.VL_NO_ACTIVE_RESERVE); require(!vars.isFrozen, Errors.VL_RESERVE_FROZEN); require(amount != 0, Errors.VL_INVALID_AMOUNT); require(vars.borrowingEnabled, Errors.VL_BORROWING_NOT_ENABLED); /// validate interest rate mode require( uint256(DataTypes.InterestRateMode.VARIABLE) == interestRateMode || uint256(DataTypes.InterestRateMode.STABLE) == interestRateMode, Errors.VL_INVALID_INTEREST_RATE_MODE_SELECTED ); /// User Account Health Analysis ( vars.userCollateralBalanceETH, vars.userBorrowBalanceETH, vars.currentLtv, vars.currentLiquidationThreshold, vars.healthFactor ) = GenericLogic.calculateUserAccountData( userAddress, reservesData, userConfig, reserves, reservesCount, oracle ); require(vars.userCollateralBalanceETH > 0, Errors.VL_COLLATERAL_BALANCE_IS_0); require( vars.healthFactor > GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD, Errors.VL_HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD ); /// Collateral Sufficiency Check //add the current already borrowed amount to the amount requested to calculate the total collateral needed. vars.amountOfCollateralNeededETH = vars.userBorrowBalanceETH.add(amountInETH).percentDiv( vars.currentLtv ); //LTV is calculated in percentage require( vars.amountOfCollateralNeededETH <= vars.userCollateralBalanceETH, Errors.VL_COLLATERAL_CANNOT_COVER_NEW_BORROW ); /// Stable Rate Borrowing Specific Checks /** * Following conditions need to be met if the user is borrowing at a stable rate: * 1. Reserve must be enabled for stable rate borrowing * 2. Users cannot borrow from the reserve if their collateral is (mostly) the same currency * they are borrowing, to prevent abuses. * 3. Users will be able to borrow only a portion of the total available liquidity **/ if (interestRateMode == uint256(DataTypes.InterestRateMode.STABLE)) { //check if the borrow mode is stable and if stable rate borrowing is enabled on this reserve require(vars.stableRateBorrowingEnabled, Errors.VL_STABLE_BORROWING_NOT_ENABLED); require( !userConfig.isUsingAsCollateral(reserve.id) || reserve.configuration.getLtv() == 0 || amount > IERC20(reserve.aTokenAddress).balanceOf(userAddress), Errors.VL_COLLATERAL_SAME_AS_BORROWING_CURRENCY ); vars.availableLiquidity = IERC20(asset).balanceOf(reserve.aTokenAddress); //calculate the max available loan size in stable rate mode as a percentage of the //available liquidity uint256 maxLoanSizeStable = vars.availableLiquidity.percentMul(maxStableLoanPercent); require(amount <= maxLoanSizeStable, Errors.VL_AMOUNT_BIGGER_THAN_MAX_LOAN_SIZE_STABLE); } }
Repay
The
repay function is used to repay a borrowed amount of an asset. The function allows the user to repay either a stable or variable rate debt. User can also repay for other user (onBehalfOf)Steps:
- State Setup
- Reserve data retrieval: Gets the specific reserve configuration
- Debt calculation: Fetches current stable and variable debt amounts of user
- Interest rate mode: Converts uint to enum for type safety
- Validation Repay
- Check reserve is active
- Check amount to repay is not zero
- Check corresponding debt amount is not zero
- Payback Amount Calculation
- If
amount == type(uint256).max: Repay entire debt of specified type - If
amount < full debt: Repay partial amount - Handles both full and partial repayments
- Reserve State Update
- Updates liquidity and variable borrow indexes to accrues interest to current timestamp
- Debt Token Burning
- Interest Rates Update
- Borrowing State Management
- If user repays all debt (both stable and variable), disable borrowing flag
- This optimizes future health factor calculations
- Funds Transfer
- Transfer funds: From repayer to aToken contract
- Handle repayment: aToken internal accounting update
/// --- contract/protocol-v2/contracts/protocol/lendingpool/LendingPool.sol --- /** * @notice Repays a borrowed `amount` on a specific reserve, burning the equivalent debt tokens owned * - E.g. User repays 100 USDC, burning 100 variable/stable debt tokens of the `onBehalfOf` address * @param asset The address of the borrowed underlying asset previously borrowed * @param amount The amount to repay * - Send the value type(uint256).max in order to repay the whole debt for `asset` on the specific `debtMode` * @param rateMode The interest rate mode at of the debt the user wants to repay: 1 for Stable, 2 for Variable * @param onBehalfOf Address of the user who will get his debt reduced/removed. Should be the address of the * user calling the function if he wants to reduce/remove his own debt, or the address of any other * other borrower whose debt should be removed * @return The final amount repaid **/ function repay( address asset, uint256 amount, uint256 rateMode, address onBehalfOf ) external override whenNotPaused returns (uint256) { /// State Setup DataTypes.ReserveData storage reserve = _reserves[asset]; (uint256 stableDebt, uint256 variableDebt) = Helpers.getUserCurrentDebt(onBehalfOf, reserve); DataTypes.InterestRateMode interestRateMode = DataTypes.InterestRateMode(rateMode); /// Validation Repay ValidationLogic.validateRepay( reserve, amount, interestRateMode, onBehalfOf, stableDebt, variableDebt ); /// Payback Amount Calculation uint256 paybackAmount = interestRateMode == DataTypes.InterestRateMode.STABLE ? stableDebt : variableDebt; if (amount < paybackAmount) { paybackAmount = amount; } /// Reserve State Update reserve.updateState(); /// Debt Token Burning if (interestRateMode == DataTypes.InterestRateMode.STABLE) { IStableDebtToken(reserve.stableDebtTokenAddress).burn(onBehalfOf, paybackAmount); } else { IVariableDebtToken(reserve.variableDebtTokenAddress).burn( onBehalfOf, paybackAmount, reserve.variableBorrowIndex ); } /// Interest Rates Update address aToken = reserve.aTokenAddress; reserve.updateInterestRates(asset, aToken, paybackAmount, 0); /// Borrowing State Management if (stableDebt.add(variableDebt).sub(paybackAmount) == 0) { _usersConfig[onBehalfOf].setBorrowing(reserve.id, false); } /// Funds Transfer IERC20(asset).safeTransferFrom(msg.sender, aToken, paybackAmount); IAToken(aToken).handleRepayment(msg.sender, paybackAmount); emit Repay(asset, onBehalfOf, msg.sender, paybackAmount); return paybackAmount; } /// --- contract/protocol-v2/contracts/protocol/libraries/helpers/Helpers.sol ---Validation Logic /** * @dev Fetches the user current stable and variable debt balances * @param user The user address * @param reserve The reserve data object * @return The stable and variable debt balance **/ function getUserCurrentDebt(address user, DataTypes.ReserveData storage reserve) internal view returns (uint256, uint256) { return ( IERC20(reserve.stableDebtTokenAddress).balanceOf(user), IERC20(reserve.variableDebtTokenAddress).balanceOf(user) ); }
validateRepay
/// --- contract/protocol-v2/contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @dev Validates a repay action * @param reserve The reserve state from which the user is repaying * @param amountSent The amount sent for the repayment. Can be an actual value or uint(-1) * @param onBehalfOf The address of the user msg.sender is repaying for * @param stableDebt The borrow balance of the user * @param variableDebt The borrow balance of the user */ function validateRepay( DataTypes.ReserveData storage reserve, uint256 amountSent, DataTypes.InterestRateMode rateMode, address onBehalfOf, uint256 stableDebt, uint256 variableDebt ) external view { bool isActive = reserve.configuration.getActive(); require(isActive, Errors.VL_NO_ACTIVE_RESERVE); require(amountSent > 0, Errors.VL_INVALID_AMOUNT); require( (stableDebt > 0 && DataTypes.InterestRateMode(rateMode) == DataTypes.InterestRateMode.STABLE) || (variableDebt > 0 && DataTypes.InterestRateMode(rateMode) == DataTypes.InterestRateMode.VARIABLE), Errors.VL_NO_DEBT_OF_SELECTED_TYPE ); require( amountSent != uint256(-1) || msg.sender == onBehalfOf, Errors.VL_NO_EXPLICIT_AMOUNT_TO_REPAY_ON_BEHALF ); }
swapBorrowRateMode
swapBorrowRateMode function allows a borrower to swap his debt between stable and variable mode, or viceversa./// --- contract/protocol-v2/contracts/protocol/lendingpool/LendingPool.sol --- /** * @dev Allows a borrower to swap his debt between stable and variable mode, or viceversa * @param asset The address of the underlying asset borrowed * @param rateMode The rate mode that the user wants to swap to **/ function swapBorrowRateMode(address asset, uint256 rateMode) external override whenNotPaused { DataTypes.ReserveData storage reserve = _reserves[asset]; (uint256 stableDebt, uint256 variableDebt) = Helpers.getUserCurrentDebt(msg.sender, reserve); DataTypes.InterestRateMode interestRateMode = DataTypes.InterestRateMode(rateMode); ValidationLogic.validateSwapRateMode( reserve, _usersConfig[msg.sender], stableDebt, variableDebt, interestRateMode ); reserve.updateState(); if (interestRateMode == DataTypes.InterestRateMode.STABLE) { IStableDebtToken(reserve.stableDebtTokenAddress).burn(msg.sender, stableDebt); IVariableDebtToken(reserve.variableDebtTokenAddress).mint( msg.sender, msg.sender, stableDebt, reserve.variableBorrowIndex ); } else { IVariableDebtToken(reserve.variableDebtTokenAddress).burn( msg.sender, variableDebt, reserve.variableBorrowIndex ); IStableDebtToken(reserve.stableDebtTokenAddress).mint( msg.sender, msg.sender, variableDebt, reserve.currentStableBorrowRate ); } reserve.updateInterestRates(asset, reserve.aTokenAddress, 0, 0); emit Swap(asset, msg.sender, rateMode); } /// --- contract/protocol-v2/contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @dev Validates a swap of borrow rate mode. * @param reserve The reserve state on which the user is swapping the rate * @param userConfig The user reserves configuration * @param stableDebt The stable debt of the user * @param variableDebt The variable debt of the user * @param currentRateMode The rate mode of the borrow */ function validateSwapRateMode( DataTypes.ReserveData storage reserve, DataTypes.UserConfigurationMap storage userConfig, uint256 stableDebt, uint256 variableDebt, DataTypes.InterestRateMode currentRateMode ) external view { (bool isActive, bool isFrozen, , bool stableRateEnabled) = reserve.configuration.getFlags(); require(isActive, Errors.VL_NO_ACTIVE_RESERVE); require(!isFrozen, Errors.VL_RESERVE_FROZEN); if (currentRateMode == DataTypes.InterestRateMode.STABLE) { require(stableDebt > 0, Errors.VL_NO_STABLE_RATE_LOAN_IN_RESERVE); } else if (currentRateMode == DataTypes.InterestRateMode.VARIABLE) { require(variableDebt > 0, Errors.VL_NO_VARIABLE_RATE_LOAN_IN_RESERVE); /** * user wants to swap to stable, before swapping we need to ensure that * 1. stable borrow rate is enabled on the reserve * 2. user is not trying to abuse the reserve by depositing * more collateral than he is borrowing, artificially lowering * the interest rate, borrowing at variable, and switching to stable **/ require(stableRateEnabled, Errors.VL_STABLE_BORROWING_NOT_ENABLED); require( !userConfig.isUsingAsCollateral(reserve.id) || reserve.configuration.getLtv() == 0 || stableDebt.add(variableDebt) > IERC20(reserve.aTokenAddress).balanceOf(msg.sender), Errors.VL_COLLATERAL_SAME_AS_BORROWING_CURRENCY ); } else { revert(Errors.VL_INVALID_INTEREST_RATE_MODE_SELECTED); } }
rebalanceStableBorrowRate
Similar to AAVE V1, V2 also allows permissionless stable debt rate update on certain situations:
- Usage ratio is above 95%
- the current deposit APY is below
REBALANCE_UP_THRESHOLD*maxVariableBorrowRate, which means that too much has been borrowed at a stable rate and depositors are not earning enough.
This design is to improve protocol’s stability, and protect LP’s benefit.
/// --- contract/protocol-v2/contracts/protocol/lendingpool/LendingPool.sol --- /** * @dev Rebalances the stable interest rate of a user to the current stable rate defined on the reserve. * - Users can be rebalanced if the following conditions are satisfied: * 1. Usage ratio is above 95% * 2. the current deposit APY is below REBALANCE_UP_THRESHOLD * maxVariableBorrowRate, which means that too much has been * borrowed at a stable rate and depositors are not earning enough * @param asset The address of the underlying asset borrowed * @param user The address of the user to be rebalanced **/ function rebalanceStableBorrowRate(address asset, address user) external override whenNotPaused { DataTypes.ReserveData storage reserve = _reserves[asset]; IERC20 stableDebtToken = IERC20(reserve.stableDebtTokenAddress); IERC20 variableDebtToken = IERC20(reserve.variableDebtTokenAddress); address aTokenAddress = reserve.aTokenAddress; uint256 stableDebt = IERC20(stableDebtToken).balanceOf(user); ValidationLogic.validateRebalanceStableBorrowRate( reserve, asset, stableDebtToken, variableDebtToken, aTokenAddress ); reserve.updateState(); IStableDebtToken(address(stableDebtToken)).burn(user, stableDebt); IStableDebtToken(address(stableDebtToken)).mint( user, user, stableDebt, reserve.currentStableBorrowRate ); reserve.updateInterestRates(asset, aTokenAddress, 0, 0); emit RebalanceStableBorrowRate(asset, user); } /// --- contract/protocol-v2/contracts/protocol/libraries/logic/ValidationLogic.sol --- uint256 public constant REBALANCE_UP_USAGE_RATIO_THRESHOLD = 0.95 * 1e27; //usage ratio of 95% /** * @dev Validates a stable borrow rate rebalance action * @param reserve The reserve state on which the user is getting rebalanced * @param reserveAddress The address of the reserve * @param stableDebtToken The stable debt token instance * @param variableDebtToken The variable debt token instance * @param aTokenAddress The address of the aToken contract */ function validateRebalanceStableBorrowRate( DataTypes.ReserveData storage reserve, address reserveAddress, IERC20 stableDebtToken, IERC20 variableDebtToken, address aTokenAddress ) external view { (bool isActive, , , ) = reserve.configuration.getFlags(); require(isActive, Errors.VL_NO_ACTIVE_RESERVE); //if the usage ratio is below 95%, no rebalances are needed uint256 totalDebt = stableDebtToken.totalSupply().add(variableDebtToken.totalSupply()).wadToRay(); uint256 availableLiquidity = IERC20(reserveAddress).balanceOf(aTokenAddress).wadToRay(); uint256 usageRatio = totalDebt == 0 ? 0 : totalDebt.rayDiv(availableLiquidity.add(totalDebt)); //if the liquidity rate is below REBALANCE_UP_THRESHOLD of the max variable APR at 95% usage, //then we allow rebalancing of the stable rate positions. uint256 currentLiquidityRate = reserve.currentLiquidityRate; uint256 maxVariableBorrowRate = IReserveInterestRateStrategy(reserve.interestRateStrategyAddress).getMaxVariableBorrowRate(); require( usageRatio >= REBALANCE_UP_USAGE_RATIO_THRESHOLD && currentLiquidityRate <= maxVariableBorrowRate.percentMul(REBALANCE_UP_LIQUIDITY_RATE_THRESHOLD), Errors.LP_INTEREST_RATE_REBALANCE_CONDITIONS_NOT_MET ); } /// --- contract/protocol-v2/contracts/protocol/lendingpool/DefaultReserveInterestRateStrategy.sol --- function getMaxVariableBorrowRate() external view override returns (uint256) { return _baseVariableBorrowRate.add(_variableRateSlope1).add(_variableRateSlope2); }
setUserUseReserveAsCollateral
Allows depositors to enable/disable a specific deposited asset as collateral.
Only allows user to disable collateral if user has enough collateral to support his/her debt after.
/// --- contract/protocol-v2/contracts/protocol/lendingpool/LendingPool.sol --- /** * @dev Allows depositors to enable/disable a specific deposited asset as collateral * @param asset The address of the underlying asset deposited * @param useAsCollateral `true` if the user wants to use the deposit as collateral, `false` otherwise **/ function setUserUseReserveAsCollateral(address asset, bool useAsCollateral) external override whenNotPaused { DataTypes.ReserveData storage reserve = _reserves[asset]; ValidationLogic.validateSetUseReserveAsCollateral( reserve, asset, useAsCollateral, _reserves, _usersConfig[msg.sender], _reservesList, _reservesCount, _addressesProvider.getPriceOracle() ); _usersConfig[msg.sender].setUsingAsCollateral(reserve.id, useAsCollateral); if (useAsCollateral) { emit ReserveUsedAsCollateralEnabled(asset, msg.sender); } else { emit ReserveUsedAsCollateralDisabled(asset, msg.sender); } } /// --- contract/protocol-v2/contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @dev Validates the action of setting an asset as collateral * @param reserve The state of the reserve that the user is enabling or disabling as collateral * @param reserveAddress The address of the reserve * @param reservesData The data of all the reserves * @param userConfig The state of the user for the specific reserve * @param reserves The addresses of all the active reserves * @param oracle The price oracle */ function validateSetUseReserveAsCollateral( DataTypes.ReserveData storage reserve, address reserveAddress, bool useAsCollateral, mapping(address => DataTypes.ReserveData) storage reservesData, DataTypes.UserConfigurationMap storage userConfig, mapping(uint256 => address) storage reserves, uint256 reservesCount, address oracle ) external view { uint256 underlyingBalance = IERC20(reserve.aTokenAddress).balanceOf(msg.sender); require(underlyingBalance > 0, Errors.VL_UNDERLYING_BALANCE_NOT_GREATER_THAN_0); require( useAsCollateral || GenericLogic.balanceDecreaseAllowed( reserveAddress, msg.sender, underlyingBalance, reservesData, userConfig, reserves, reservesCount, oracle ), Errors.VL_DEPOSIT_ALREADY_IN_USE ); }
liquidationCall
liquidationCall is called to liquidate a position that has a Health Factor below 1. The liquidator repays a portion of the debt and receives a proportion of the collateral, plus a bonus.LendingPool.liquidationCall delegates call to collateralManager.liquidationCall to liquidate.AAVE V2 liquidation logic is exactly same as AAVE V1 except: as AAVE V2 allows user to create both variable and stable debt as same time, liquidator first repays user’s variable debt, then stable debt.
Liquidator repays user’s debt in exchange for user’s collateral with bonus.
/// --- contract/protocol-v2/contracts/protocol/lendingpool/LendingPool.sol --- /** * @dev Function to liquidate a non-healthy position collateral-wise, with Health Factor below 1 * - The caller (liquidator) covers `debtToCover` amount of debt of the user getting liquidated, and receives * a proportionally amount of the `collateralAsset` plus a bonus to cover market risk * @param collateralAsset The address of the underlying asset used as collateral, to receive as result of the liquidation * @param debtAsset The address of the underlying borrowed asset to be repaid with the liquidation * @param user The address of the borrower getting liquidated * @param debtToCover The debt amount of borrowed `asset` the liquidator wants to cover * @param receiveAToken `true` if the liquidators wants to receive the collateral aTokens, `false` if he wants * to receive the underlying collateral asset directly **/ function liquidationCall( address collateralAsset, address debtAsset, address user, uint256 debtToCover, bool receiveAToken ) external override whenNotPaused { address collateralManager = _addressesProvider.getLendingPoolCollateralManager(); //solium-disable-next-line (bool success, bytes memory result) = collateralManager.delegatecall( abi.encodeWithSignature( 'liquidationCall(address,address,address,uint256,bool)', collateralAsset, debtAsset, user, debtToCover, receiveAToken ) ); require(success, Errors.LP_LIQUIDATION_CALL_FAILED); (uint256 returnCode, string memory returnMessage) = abi.decode(result, (uint256, string)); require(returnCode == 0, string(abi.encodePacked(returnMessage))); }
LendingPoolCollateralManager.liquidationCall handles liquidation logic.Steps:
- State Setup & Health Factor Calculation
- User Debt Calculation
- Liquidation Validation
- Active reserves check
- Health factor below liquidation threshold (1.0)
- Collateral enabled by user
- User has outstanding debt
Validation checks in
validateLiquidationCall:- Collateral Balance Check
- Maximum Liquidatable Debt Calculation
Close Factor Logic:
Maximum 50% of total debt can be liquidated in one call to prevents complete liquidation in volatile market conditions
- Actual Repay Amount Debt Calculation
- Collateral Availability Check
- Debt Burning Logic
- Interest Rates Update
- Collateral Transfer to Liquidator
- Option A: Receive aTokens
- Option B: Receive Underlying Asset
- Collateral State Update
- Debt Asset Transfer
/// --- contract/protocol-v2/contracts/protocol/lendingpool/LendingPoolCollateralManager.sol --- /** * @dev 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 proportionally amount of the `collateralAsset` plus a bonus to cover market risk * @param collateralAsset The address of the underlying asset used as collateral, to receive as result of the liquidation * @param debtAsset The address of the underlying borrowed asset to be repaid with the liquidation * @param user The address of the borrower getting liquidated * @param debtToCover The debt amount of borrowed `asset` the liquidator wants to cover * @param receiveAToken `true` if the liquidators wants to receive the collateral aTokens, `false` if he wants * to receive the underlying collateral asset directly **/ function liquidationCall( address collateralAsset, address debtAsset, address user, uint256 debtToCover, bool receiveAToken ) external override returns (uint256, string memory) { DataTypes.ReserveData storage collateralReserve = _reserves[collateralAsset]; DataTypes.ReserveData storage debtReserve = _reserves[debtAsset]; DataTypes.UserConfigurationMap storage userConfig = _usersConfig[user]; LiquidationCallLocalVars memory vars; (, , , , vars.healthFactor) = GenericLogic.calculateUserAccountData( user, _reserves, userConfig, _reservesList, _reservesCount, _addressesProvider.getPriceOracle() ); (vars.userStableDebt, vars.userVariableDebt) = Helpers.getUserCurrentDebt(user, debtReserve); (vars.errorCode, vars.errorMsg) = ValidationLogic.validateLiquidationCall( collateralReserve, debtReserve, userConfig, vars.healthFactor, vars.userStableDebt, vars.userVariableDebt ); if (Errors.CollateralManagerErrors(vars.errorCode) != Errors.CollateralManagerErrors.NO_ERROR) { return (vars.errorCode, vars.errorMsg); } vars.collateralAtoken = IAToken(collateralReserve.aTokenAddress); vars.userCollateralBalance = vars.collateralAtoken.balanceOf(user); vars.maxLiquidatableDebt = vars.userStableDebt.add(vars.userVariableDebt).percentMul( LIQUIDATION_CLOSE_FACTOR_PERCENT ); vars.actualDebtToLiquidate = debtToCover > vars.maxLiquidatableDebt ? vars.maxLiquidatableDebt : debtToCover; ( vars.maxCollateralToLiquidate, vars.debtAmountNeeded ) = _calculateAvailableCollateralToLiquidate( collateralReserve, debtReserve, collateralAsset, debtAsset, vars.actualDebtToLiquidate, vars.userCollateralBalance ); // If debtAmountNeeded < actualDebtToLiquidate, there isn't enough // collateral to cover the actual amount that is being liquidated, hence we liquidate // a smaller amount if (vars.debtAmountNeeded < vars.actualDebtToLiquidate) { vars.actualDebtToLiquidate = vars.debtAmountNeeded; } // If the liquidator reclaims the underlying asset, we make sure there is enough available liquidity in the // collateral reserve if (!receiveAToken) { uint256 currentAvailableCollateral = IERC20(collateralAsset).balanceOf(address(vars.collateralAtoken)); if (currentAvailableCollateral < vars.maxCollateralToLiquidate) { return ( uint256(Errors.CollateralManagerErrors.NOT_ENOUGH_LIQUIDITY), Errors.LPCM_NOT_ENOUGH_LIQUIDITY_TO_LIQUIDATE ); } } debtReserve.updateState(); if (vars.userVariableDebt >= vars.actualDebtToLiquidate) { IVariableDebtToken(debtReserve.variableDebtTokenAddress).burn( user, vars.actualDebtToLiquidate, debtReserve.variableBorrowIndex ); } else { // If the user doesn't have variable debt, no need to try to burn variable debt tokens if (vars.userVariableDebt > 0) { IVariableDebtToken(debtReserve.variableDebtTokenAddress).burn( user, vars.userVariableDebt, debtReserve.variableBorrowIndex ); } IStableDebtToken(debtReserve.stableDebtTokenAddress).burn( user, vars.actualDebtToLiquidate.sub(vars.userVariableDebt) ); } debtReserve.updateInterestRates( debtAsset, debtReserve.aTokenAddress, vars.actualDebtToLiquidate, 0 ); if (receiveAToken) { vars.liquidatorPreviousATokenBalance = IERC20(vars.collateralAtoken).balanceOf(msg.sender); vars.collateralAtoken.transferOnLiquidation(user, msg.sender, vars.maxCollateralToLiquidate); if (vars.liquidatorPreviousATokenBalance == 0) { DataTypes.UserConfigurationMap storage liquidatorConfig = _usersConfig[msg.sender]; liquidatorConfig.setUsingAsCollateral(collateralReserve.id, true); emit ReserveUsedAsCollateralEnabled(collateralAsset, msg.sender); } } else { collateralReserve.updateState(); collateralReserve.updateInterestRates( collateralAsset, address(vars.collateralAtoken), 0, vars.maxCollateralToLiquidate ); // Burn the equivalent amount of aToken, sending the underlying to the liquidator vars.collateralAtoken.burn( user, msg.sender, vars.maxCollateralToLiquidate, collateralReserve.liquidityIndex ); } // If the collateral being liquidated is equal to the user balance, // we set the currency as not being used as collateral anymore if (vars.maxCollateralToLiquidate == vars.userCollateralBalance) { userConfig.setUsingAsCollateral(collateralReserve.id, false); emit ReserveUsedAsCollateralDisabled(collateralAsset, user); } // Transfers the debt asset being repaid to the aToken, where the liquidity is kept IERC20(debtAsset).safeTransferFrom( msg.sender, debtReserve.aTokenAddress, vars.actualDebtToLiquidate ); emit LiquidationCall( collateralAsset, debtAsset, user, vars.actualDebtToLiquidate, vars.maxCollateralToLiquidate, msg.sender, receiveAToken ); return (uint256(Errors.CollateralManagerErrors.NO_ERROR), Errors.LPCM_NO_ERRORS); }
validateLiquidationCall
/// --- contract/protocol-v2/contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @dev Validates the liquidation action * @param collateralReserve The reserve data of the collateral * @param principalReserve The reserve data of the principal * @param userConfig The user configuration * @param userHealthFactor The user's health factor * @param userStableDebt Total stable debt balance of the user * @param userVariableDebt Total variable debt balance of the user **/ function validateLiquidationCall( DataTypes.ReserveData storage collateralReserve, DataTypes.ReserveData storage principalReserve, DataTypes.UserConfigurationMap storage userConfig, uint256 userHealthFactor, uint256 userStableDebt, uint256 userVariableDebt ) internal view returns (uint256, string memory) { if ( !collateralReserve.configuration.getActive() || !principalReserve.configuration.getActive() ) { return ( uint256(Errors.CollateralManagerErrors.NO_ACTIVE_RESERVE), Errors.VL_NO_ACTIVE_RESERVE ); } if (userHealthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD) { return ( uint256(Errors.CollateralManagerErrors.HEALTH_FACTOR_ABOVE_THRESHOLD), Errors.LPCM_HEALTH_FACTOR_NOT_BELOW_THRESHOLD ); } bool isCollateralEnabled = collateralReserve.configuration.getLiquidationThreshold() > 0 && userConfig.isUsingAsCollateral(collateralReserve.id); //if collateral isn't enabled as collateral by user, it cannot be liquidated if (!isCollateralEnabled) { return ( uint256(Errors.CollateralManagerErrors.COLLATERAL_CANNOT_BE_LIQUIDATED), Errors.LPCM_COLLATERAL_CANNOT_BE_LIQUIDATED ); } if (userStableDebt == 0 && userVariableDebt == 0) { return ( uint256(Errors.CollateralManagerErrors.CURRRENCY_NOT_BORROWED), Errors.LPCM_SPECIFIED_CURRENCY_NOT_BORROWED_BY_USER ); } return (uint256(Errors.CollateralManagerErrors.NO_ERROR), Errors.LPCM_NO_ERRORS); }
_calculateAvailableCollateralToLiquidate
/// --- contract/protocol-v2/contracts/protocol/lendingpool/LendingPoolCollateralManager.sol --- /** * @dev Calculates how much of a specific collateral can be liquidated, given * a certain amount of debt asset. * - This function needs to be called after all the checks to validate the liquidation have been performed, * otherwise it might fail. * @param collateralReserve The data of the collateral reserve * @param debtReserve The data of the debt reserve * @param collateralAsset The address of the underlying asset used as collateral, to receive as result of the liquidation * @param debtAsset The address of the underlying borrowed asset to be repaid with the liquidation * @param debtToCover The debt amount of borrowed `asset` the liquidator wants to cover * @param userCollateralBalance The collateral balance for the specific `collateralAsset` of the user being liquidated * @return collateralAmount: The maximum amount that is possible to liquidate given all the liquidation constraints * (user balance, close factor) * debtAmountNeeded: The amount to repay with the liquidation **/ function _calculateAvailableCollateralToLiquidate( DataTypes.ReserveData storage collateralReserve, DataTypes.ReserveData storage debtReserve, address collateralAsset, address debtAsset, uint256 debtToCover, uint256 userCollateralBalance ) internal view returns (uint256, uint256) { uint256 collateralAmount = 0; uint256 debtAmountNeeded = 0; IPriceOracleGetter oracle = IPriceOracleGetter(_addressesProvider.getPriceOracle()); AvailableCollateralToLiquidateLocalVars memory vars; vars.collateralPrice = oracle.getAssetPrice(collateralAsset); vars.debtAssetPrice = oracle.getAssetPrice(debtAsset); (, , vars.liquidationBonus, vars.collateralDecimals, ) = collateralReserve .configuration .getParams(); vars.debtAssetDecimals = debtReserve.configuration.getDecimals(); // This is the maximum possible amount of the selected collateral that can be liquidated, given the // max amount of liquidatable debt vars.maxAmountCollateralToLiquidate = vars .debtAssetPrice .mul(debtToCover) .mul(10**vars.collateralDecimals) .percentMul(vars.liquidationBonus) .div(vars.collateralPrice.mul(10**vars.debtAssetDecimals)); if (vars.maxAmountCollateralToLiquidate > userCollateralBalance) { collateralAmount = userCollateralBalance; debtAmountNeeded = vars .collateralPrice .mul(collateralAmount) .mul(10**vars.debtAssetDecimals) .div(vars.debtAssetPrice.mul(10**vars.collateralDecimals)) .percentDiv(vars.liquidationBonus); } else { collateralAmount = vars.maxAmountCollateralToLiquidate; debtAmountNeeded = debtToCover; } return (collateralAmount, debtAmountNeeded); }
flashLoan
The function allows users to perform flash loans by borrowing assets from the pool without collateral, under the condition that the borrowed amount plus a fee is returned within the same transaction.
The logic is same with AAVE V1 except:
- allows borrower to borrow a list of assets
- allows borrower to open debt rather than pay the borrow immediately.
- pull asset plus fee back from receiver. This design doesn’t rely on liquidity snapshot to check whether borrower has returned fund, so it’s more flexible. Based on this design, it can deletes reentrancy guard and allows borrower to execute complicated flashloan logic using V2 contract.
As in V1, if user executes flashloan and deposits the borrowed asset as liquidity, the flashloan check passes as it only relies on snapshots of liquidity of V1’s
LendingPoolCore contract before and after flashloan operation. In such case, the protocol is compromised(borrower in fact doesn’t return fund), that’s the reason why it needs reentrancy guard to prevent user to interact with V1 contract to execute operations like liquidity deposit.In V2, it pulls fund back from receiver, ensures fund and fee is returned rather than rely on liquidity snapshot. In this design, it can allow borrower to interact with V2 protocol’s other functions.
/// --- contract/protocol-v2/contracts/protocol/lendingpool/LendingPool.sol --- /** * @dev Allows smartcontracts to access the liquidity of the pool within one transaction, * as long as the amount taken plus a fee is returned. * IMPORTANT There are security concerns for developers of flashloan receiver contracts that must be kept into consideration. * For further details please visit https://developers.aave.com * @param receiverAddress The address of the contract receiving the funds, implementing the IFlashLoanReceiver interface * @param assets The addresses of the assets being flash-borrowed * @param amounts The amounts amounts being flash-borrowed * @param modes Types of the debt to open if the flash loan is not returned: * 0 -> Don't open any debt, just revert if funds can't be transferred from the receiver * 1 -> Open debt at stable rate for the value of the amount flash-borrowed to the `onBehalfOf` address * 2 -> Open debt at variable rate for the value of the amount flash-borrowed to the `onBehalfOf` address * @param onBehalfOf The address that will receive the debt in the case of using on `modes` 1 or 2 * @param params Variadic packed params to pass to the receiver as extra information * @param referralCode Code used to register the integrator originating the operation, for potential rewards. * 0 if the action is executed directly by the user, without any middle-man **/ function flashLoan( address receiverAddress, address[] calldata assets, uint256[] calldata amounts, uint256[] calldata modes, address onBehalfOf, bytes calldata params, uint16 referralCode ) external override whenNotPaused { FlashLoanLocalVars memory vars; ValidationLogic.validateFlashloan(assets, amounts); address[] memory aTokenAddresses = new address[](assets.length); uint256[] memory premiums = new uint256[](assets.length); vars.receiver = IFlashLoanReceiver(receiverAddress); for (vars.i = 0; vars.i < assets.length; vars.i++) { aTokenAddresses[vars.i] = _reserves[assets[vars.i]].aTokenAddress; premiums[vars.i] = amounts[vars.i].mul(_flashLoanPremiumTotal).div(10000); IAToken(aTokenAddresses[vars.i]).transferUnderlyingTo(receiverAddress, amounts[vars.i]); } require( vars.receiver.executeOperation(assets, amounts, premiums, msg.sender, params), Errors.LP_INVALID_FLASH_LOAN_EXECUTOR_RETURN ); for (vars.i = 0; vars.i < assets.length; vars.i++) { vars.currentAsset = assets[vars.i]; vars.currentAmount = amounts[vars.i]; vars.currentPremium = premiums[vars.i]; vars.currentATokenAddress = aTokenAddresses[vars.i]; vars.currentAmountPlusPremium = vars.currentAmount.add(vars.currentPremium); if (DataTypes.InterestRateMode(modes[vars.i]) == DataTypes.InterestRateMode.NONE) { _reserves[vars.currentAsset].updateState(); _reserves[vars.currentAsset].cumulateToLiquidityIndex( IERC20(vars.currentATokenAddress).totalSupply(), vars.currentPremium ); _reserves[vars.currentAsset].updateInterestRates( vars.currentAsset, vars.currentATokenAddress, vars.currentAmountPlusPremium, 0 ); IERC20(vars.currentAsset).safeTransferFrom( receiverAddress, vars.currentATokenAddress, vars.currentAmountPlusPremium ); } else { // If the user chose to not return the funds, the system checks if there is enough collateral and // eventually opens a debt position _executeBorrow( ExecuteBorrowParams( vars.currentAsset, msg.sender, onBehalfOf, vars.currentAmount, modes[vars.i], vars.currentATokenAddress, referralCode, false ) ); } emit FlashLoan( receiverAddress, msg.sender, vars.currentAsset, vars.currentAmount, vars.currentPremium, referralCode ); } }
validateFlashloan
/// --- contract/protocol-v2/contracts/protocol/libraries/logic/ValidationLogic.sol --- /** * @dev Validates a flashloan action * @param assets The assets being flashborrowed * @param amounts The amounts for each asset being borrowed **/ function validateFlashloan(address[] memory assets, uint256[] memory amounts) internal pure { require(assets.length == amounts.length, Errors.VL_INCONSISTENT_FLASHLOAN_PARAMS); }
ReserveLogic
updateState
Updates reserve indices and accrues interest
- Gets current scaled variable debt
- Updates liquidity and borrow indices based on time elapsed and rates
- Mints protocol fees to treasury
IVariableDebtToken.scaledTotalSupply returns scaled supply of debt token (just raw erc20 supply)/// --- contract/protocol-v2/contracts/protocol/libraries/logic/ReserveLogic.sol --- /** * @dev Updates the liquidity cumulative index and the variable borrow index. * @param reserve the reserve object **/ function updateState(DataTypes.ReserveData storage reserve) internal { uint256 scaledVariableDebt = IVariableDebtToken(reserve.variableDebtTokenAddress).scaledTotalSupply(); uint256 previousVariableBorrowIndex = reserve.variableBorrowIndex; uint256 previousLiquidityIndex = reserve.liquidityIndex; uint40 lastUpdatedTimestamp = reserve.lastUpdateTimestamp; (uint256 newLiquidityIndex, uint256 newVariableBorrowIndex) = _updateIndexes( reserve, scaledVariableDebt, previousLiquidityIndex, previousVariableBorrowIndex, lastUpdatedTimestamp ); _mintToTreasury( reserve, scaledVariableDebt, previousVariableBorrowIndex, newLiquidityIndex, newVariableBorrowIndex, lastUpdatedTimestamp ); }
_updateIndexes
The
_updateIndexes function is responsible for accruing interest and updating reserve indices based on the time elapsed since the last update. It calculates new liquidity and variable borrow indices by applying interest rates over time, ensuring depositors earn interest and borrowers accrue debt correctly.- parameter
timestampis the timestamp of last time when reserve indices got updated.
Steps:
- Initial Setup
- Stores current rates and initializes new indices with old values
- If no time has passed or rates are zero, indices remain unchanged
- Liquidity Index Update (Linear Interest)
- Linear Interest Calculation: Uses
calculateLinearInterestfor liquidity depositors - Time-Based Accrual: Interest accrues proportionally to time elapsed
- Overflow Protection: Ensices index fits in uint128
- Variable Borrow Index Update (Compounded Interest)
- Compounded Interest: Uses
calculateCompoundedInterestfor borrowers (interest on interest) - Conditional Update: Only updates if there's actual variable debt
- Different Models: Borrowers pay compounded interest while depositors earn linear interest
- Timestamp Update
- Records current block timestamp for next interest calculation
- Uses uint40 to save storage space (will overflow in year 231,800)
Mathematical Foundation
newLiquidityIndex = oldLiquidityIndex × (1 + rate × time)
newVariableBorrowIndex = oldVariableBorrowIndex × (1 + rate/secondsPerYear)^time
/// --- contract/protocol-v2/contracts/protocol/libraries/logic/ReserveLogic.sol --- /** * @dev Updates the reserve indexes and the timestamp of the update * @param reserve The reserve reserve to be updated * @param scaledVariableDebt The scaled variable debt * @param liquidityIndex The last stored liquidity index * @param variableBorrowIndex The last stored variable borrow index **/ function _updateIndexes( DataTypes.ReserveData storage reserve, uint256 scaledVariableDebt, uint256 liquidityIndex, uint256 variableBorrowIndex, uint40 timestamp ) internal returns (uint256, uint256) { /// Initial Setup uint256 currentLiquidityRate = reserve.currentLiquidityRate; uint256 newLiquidityIndex = liquidityIndex; uint256 newVariableBorrowIndex = variableBorrowIndex; /// Liquidity Index Update (Linear Interest) //only cumulating if there is any income being produced if (currentLiquidityRate > 0) { uint256 cumulatedLiquidityInterest = MathUtils.calculateLinearInterest(currentLiquidityRate, timestamp); newLiquidityIndex = cumulatedLiquidityInterest.rayMul(liquidityIndex); require(newLiquidityIndex <= type(uint128).max, Errors.RL_LIQUIDITY_INDEX_OVERFLOW); reserve.liquidityIndex = uint128(newLiquidityIndex); /// Variable Borrow Index Update (Compounded Interest) //as the liquidity rate might come only from stable rate loans, we need to ensure //that there is actual variable debt before accumulating if (scaledVariableDebt != 0) { uint256 cumulatedVariableBorrowInterest = MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp); newVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul(variableBorrowIndex); require( newVariableBorrowIndex <= type(uint128).max, Errors.RL_VARIABLE_BORROW_INDEX_OVERFLOW ); reserve.variableBorrowIndex = uint128(newVariableBorrowIndex); } } /// Timestamp Update //solium-disable-next-line reserve.lastUpdateTimestamp = uint40(block.timestamp); return (newLiquidityIndex, newVariableBorrowIndex); }
_mintToTreasury
The
_mintToTreasury function calculates and mints the protocol's revenue share from interest accruals to the treasury. It distributes a portion of the interest earned (determined by the reserveFactor) to the protocol treasury as aTokens.Steps:
- Reserve Factor Check
reserveFactoris a percentage (e.g., 1000 = 10%) of interest that goes to treasury- If zero, protocol takes no fee - all interest goes to depositors
Purpose: Early exit if no treasury fee is configured
- Stable Debt Data Retrieval
principalStableDebt: Original borrowed amounts without interestcurrentStableDebt: Current debt including accrued interestavgStableRate: Weighted average interest rate of all stable loansstableSupplyUpdatedTimestamp: Last time stable debt was updated
Gets from StableDebtToken:
- Variable Debt Calculations
scaledVariableDebt: Debt amount divided by index- Converts scaled debt to actual debt using old and new indices
- Stable Debt Interest Calculation
- Determines interest accrued on stable loans since last update
- Applies compounded interest to principal debt
- Gets what the stable debt should have been at last reserve update (last reserve update time can be later than last stable debt update time, as stable debt is not effected during certain reserve related operations like deposit and withdraw. These operations has no effect on debt.)
- Total Accrued Interest Calculation
Interest = (Current Total Debt) - (Previous Total Debt)
This represents the total interest earned by depositors during the period.
- Treasury Share Calculation
- If
reserveFactor = 1000(10%), protocol takes 10% of interest - Remaining 90% goes to depositors via liquidity index increase
Protocol Fee = Total Interest × Reserve Factor
- Treasury Minting
- Mints aTokens representing the protocol's share
- Uses current liquidity index for proper scaling
- Treasury can redeem these for underlying assets
Mints aTokens to treasury:
Note the liquidity interest rate index is updated based on liquidity rate normally in
_updateIndexes, _mintToTreasury mints treasury share aToken based on accrued interest fee, and liquidity providers’ interest fee is calculated based on liquidity interest rate index. This can work because liquidity rate calculation has already considered this process, it ensures this calculation workflow can calculate correct treasury fee based on reserve factor and user can got remaining interest fee. So the liquidity rate in fact is not the true liquidity rate, it’s smaller than the true liquidity rate based on which debt generates interest./** * @dev Mints part of the repaid interest to the reserve treasury as a function of the reserveFactor for the * specific asset. * @param reserve The reserve reserve to be updated * @param scaledVariableDebt The current scaled total variable debt * @param previousVariableBorrowIndex The variable borrow index before the last accumulation of the interest * @param newLiquidityIndex The new liquidity index * @param newVariableBorrowIndex The variable borrow index after the last accumulation of the interest **/ function _mintToTreasury( DataTypes.ReserveData storage reserve, uint256 scaledVariableDebt, uint256 previousVariableBorrowIndex, uint256 newLiquidityIndex, uint256 newVariableBorrowIndex, uint40 timestamp ) internal { MintToTreasuryLocalVars memory vars; /// Reserve Factor Check vars.reserveFactor = reserve.configuration.getReserveFactor(); if (vars.reserveFactor == 0) { return; } /// Stable Debt Data Retrieval //fetching the principal, total stable debt and the avg stable rate ( vars.principalStableDebt, vars.currentStableDebt, vars.avgStableRate, vars.stableSupplyUpdatedTimestamp ) = IStableDebtToken(reserve.stableDebtTokenAddress).getSupplyData(); /// Variable Debt Calculations //calculate the last principal variable debt vars.previousVariableDebt = scaledVariableDebt.rayMul(previousVariableBorrowIndex); //calculate the new total supply after accumulation of the index vars.currentVariableDebt = scaledVariableDebt.rayMul(newVariableBorrowIndex); /// Stable Debt Interest Calculation //calculate the stable debt until the last timestamp update vars.cumulatedStableInterest = MathUtils.calculateCompoundedInterest( vars.avgStableRate, vars.stableSupplyUpdatedTimestamp, timestamp ); vars.previousStableDebt = vars.principalStableDebt.rayMul(vars.cumulatedStableInterest); /// Total Accrued Interest Calculation //debt accrued is the sum of the current debt minus the sum of the debt at the last update vars.totalDebtAccrued = vars .currentVariableDebt .add(vars.currentStableDebt) .sub(vars.previousVariableDebt) .sub(vars.previousStableDebt); /// Treasury Share Calculation vars.amountToMint = vars.totalDebtAccrued.percentMul(vars.reserveFactor); /// Treasury Minting if (vars.amountToMint != 0) { IAToken(reserve.aTokenAddress).mintToTreasury(vars.amountToMint, newLiquidityIndex); } }
updateInterestRates
The
updateInterestRates function is responsible for recalculating and updating all interest rates (liquidity, stable borrow, variable borrow) for a reserve based on current market conditions, utilization rates, and protocol activity.Steps:
- Local Variables Declaration
- Stable Debt Data Retrieval
- Total stable debt: Sum of all outstanding stable rate borrows
- Average stable rate: Weighted average interest rate across all stable loans
- Variable Debt Calculation
scaledTotalSupply(): Variable debt divided by current index (remains constant, this is just the raw erc20 debt token supply)- Multiplies by
variableBorrowIndexto get actual current debt including interest
- Interest Rate Strategy Call
reserveAddress,aTokenAddress: Identify the specific reserveliquidityAdded/Taken: Recent protocol activity affecting supply- Debt totals: Current borrowing levels
avgStableRate: Current cost of stable borrowingreserveFactor: Protocol fee percentage
Key Parameters Passed:
- Rate Assignment
- Stores new rates for future interest calculations
- These rates will be used in
_updateIndexes()for interest accrual
/// --- contract/protocol-v2/contracts/protocol/libraries/logic/ReserveLogic.sol --- /** * @dev Updates the reserve current stable borrow rate, the current variable borrow rate and the current liquidity rate * @param reserve The address of the reserve to be updated * @param liquidityAdded The amount of liquidity added to the protocol (deposit or repay) in the previous action * @param liquidityTaken The amount of liquidity taken from the protocol (redeem or borrow) **/ function updateInterestRates( DataTypes.ReserveData storage reserve, address reserveAddress, address aTokenAddress, uint256 liquidityAdded, uint256 liquidityTaken ) internal { /// Local Variables Declaration UpdateInterestRatesLocalVars memory vars; /// Stable Debt Data Retrieval vars.stableDebtTokenAddress = reserve.stableDebtTokenAddress; (vars.totalStableDebt, vars.avgStableRate) = IStableDebtToken(vars.stableDebtTokenAddress) .getTotalSupplyAndAvgRate(); /// Variable Debt Calculation //calculates the total variable debt locally using the scaled total supply instead //of totalSupply(), as it's noticeably cheaper. Also, the index has been //updated by the previous updateState() call vars.totalVariableDebt = IVariableDebtToken(reserve.variableDebtTokenAddress) .scaledTotalSupply() .rayMul(reserve.variableBorrowIndex); /// Interest Rate Strategy Call ( vars.newLiquidityRate, vars.newStableRate, vars.newVariableRate ) = IReserveInterestRateStrategy(reserve.interestRateStrategyAddress).calculateInterestRates( reserveAddress, aTokenAddress, liquidityAdded, liquidityTaken, vars.totalStableDebt, vars.totalVariableDebt, vars.avgStableRate, reserve.configuration.getReserveFactor() ); require(vars.newLiquidityRate <= type(uint128).max, Errors.RL_LIQUIDITY_RATE_OVERFLOW); require(vars.newStableRate <= type(uint128).max, Errors.RL_STABLE_BORROW_RATE_OVERFLOW); require(vars.newVariableRate <= type(uint128).max, Errors.RL_VARIABLE_BORROW_RATE_OVERFLOW); /// Rate Assignment reserve.currentLiquidityRate = uint128(vars.newLiquidityRate); reserve.currentStableBorrowRate = uint128(vars.newStableRate); reserve.currentVariableBorrowRate = uint128(vars.newVariableRate); emit ReserveDataUpdated( reserveAddress, vars.newLiquidityRate, vars.newStableRate, vars.newVariableRate, reserve.liquidityIndex, reserve.variableBorrowIndex ); }
AToken
- AToken represents claim to underlying reserve asset.
- AToken contract also manages those reserve asset.
- AToken balance is scaled.
TokenBalance * interestRateIndex = RealBalance (liquidity+interest fee)mintToTreasury
/// --- contract/protocol-v2/contracts/protocol/tokenization/AToken.sol --- /** * @dev Mints aTokens to the reserve treasury * - Only callable by the LendingPool * @param amount The amount of tokens getting minted * @param index The new liquidity index of the reserve */ function mintToTreasury(uint256 amount, uint256 index) external override onlyLendingPool { if (amount == 0) { return; } address treasury = _treasury; // Compared to the normal mint, we don't check for rounding errors. // The amount to mint can easily be very small since it is a fraction of the interest ccrued. // In that case, the treasury will experience a (very small) loss, but it // wont cause potentially valid transactions to fail. _mint(treasury, amount.rayDiv(index)); emit Transfer(address(0), treasury, amount); emit Mint(treasury, amount, index); }
burn
/// --- contract/protocol-v2/contracts/protocol/tokenization/AToken.sol --- /** * @dev Burns aTokens from `user` and sends the equivalent amount of underlying to `receiverOfUnderlying` * - Only callable by the LendingPool, as extra state updates there need to be managed * @param user The owner of the aTokens, getting them burned * @param receiverOfUnderlying The address that will receive the underlying * @param amount The amount being burned * @param index The new liquidity index of the reserve **/ function burn( address user, address receiverOfUnderlying, uint256 amount, uint256 index ) external override onlyLendingPool { uint256 amountScaled = amount.rayDiv(index); require(amountScaled != 0, Errors.CT_INVALID_BURN_AMOUNT); _burn(user, amountScaled); IERC20(_underlyingAsset).safeTransfer(receiverOfUnderlying, amount); emit Transfer(user, address(0), amount); emit Burn(user, receiverOfUnderlying, amount, index); }
transferUnderlyingTo
transferUnderlyingTo function transfers amount of underlying asset to target./** * @dev Transfers the underlying asset to `target`. Used by the LendingPool to transfer * assets in borrow(), withdraw() and flashLoan() * @param target The recipient of the aTokens * @param amount The amount getting transferred * @return The amount transferred **/ function transferUnderlyingTo(address target, uint256 amount) external override onlyLendingPool returns (uint256) { IERC20(_underlyingAsset).safeTransfer(target, amount); return amount; }
handleRepayment
Called when user repays debt. Not implemented yet.
/// --- contract/protocol-v2/contracts/protocol/tokenization/AToken.sol --- /** * @dev Invoked to execute actions on the aToken side after a repayment. * @param user The user executing the repayment * @param amount The amount getting repaid **/ function handleRepayment(address user, uint256 amount) external override onlyLendingPool {}
DefaultReserveInterestRateStrategy
calculateInterestRates
calculateInterestRates function calculates newest variable debt interest rate and liquidity interest rate based on reserve status.- It first gets reserve balance of corresponding AToken contract, then calcualtes current total reserve liquidity based on liquidity update (passed as parameters).
- Then it calls another
calculateInterestRatesfunction to calculate rates.
/// --- contract/protocol-v2/contracts/protocol/lendingpool/DefaultReserveInterestRateStrategy.sol --- /** * @dev Calculates the interest rates depending on the reserve's state and configurations * @param reserve The address of the reserve * @param liquidityAdded The liquidity added during the operation * @param liquidityTaken The liquidity taken during the operation * @param totalStableDebt The total borrowed from the reserve a stable rate * @param totalVariableDebt The total borrowed from the reserve at a variable rate * @param averageStableBorrowRate The weighted average of all the stable rate loans * @param reserveFactor The reserve portion of the interest that goes to the treasury of the market * @return The liquidity rate, the stable borrow rate and the variable borrow rate **/ function calculateInterestRates( address reserve, address aToken, uint256 liquidityAdded, uint256 liquidityTaken, uint256 totalStableDebt, uint256 totalVariableDebt, uint256 averageStableBorrowRate, uint256 reserveFactor ) external view override returns ( uint256, uint256, uint256 ) { uint256 availableLiquidity = IERC20(reserve).balanceOf(aToken); //avoid stack too deep availableLiquidity = availableLiquidity.add(liquidityAdded).sub(liquidityTaken); return calculateInterestRates( reserve, availableLiquidity, totalStableDebt, totalVariableDebt, averageStableBorrowRate, reserveFactor ); }
calculateInterestRates function implements Aave's two-slope interest rate model that dynamically adjusts rates based on reserve utilization. It calculates three key rates:- Liquidity Rate: What depositors earn
- Stable Borrow Rate: Fixed-rate borrowing cost
- Variable Borrow Rate: Floating-rate borrowing cost
This rate calculation is exactly same with the V1 implementation.
/// --- contract/protocol-v2/contracts/protocol/lendingpool/DefaultReserveInterestRateStrategy.sol --- /** * @dev Calculates the interest rates depending on the reserve's state and configurations. * NOTE This function is kept for compatibility with the previous DefaultInterestRateStrategy interface. * New protocol implementation uses the new calculateInterestRates() interface * @param reserve The address of the reserve * @param availableLiquidity The liquidity available in the corresponding aToken * @param totalStableDebt The total borrowed from the reserve a stable rate * @param totalVariableDebt The total borrowed from the reserve at a variable rate * @param averageStableBorrowRate The weighted average of all the stable rate loans * @param reserveFactor The reserve portion of the interest that goes to the treasury of the market * @return The liquidity rate, the stable borrow rate and the variable borrow rate **/ function calculateInterestRates( address reserve, uint256 availableLiquidity, uint256 totalStableDebt, uint256 totalVariableDebt, uint256 averageStableBorrowRate, uint256 reserveFactor ) public view override returns ( uint256, uint256, uint256 ) { CalcInterestRatesLocalVars memory vars; vars.totalDebt = totalStableDebt.add(totalVariableDebt); vars.currentVariableBorrowRate = 0; vars.currentStableBorrowRate = 0; vars.currentLiquidityRate = 0; vars.utilizationRate = vars.totalDebt == 0 ? 0 : vars.totalDebt.rayDiv(availableLiquidity.add(vars.totalDebt)); vars.currentStableBorrowRate = ILendingRateOracle(addressesProvider.getLendingRateOracle()) .getMarketBorrowRate(reserve); if (vars.utilizationRate > OPTIMAL_UTILIZATION_RATE) { uint256 excessUtilizationRateRatio = vars.utilizationRate.sub(OPTIMAL_UTILIZATION_RATE).rayDiv(EXCESS_UTILIZATION_RATE); vars.currentStableBorrowRate = vars.currentStableBorrowRate.add(_stableRateSlope1).add( _stableRateSlope2.rayMul(excessUtilizationRateRatio) ); vars.currentVariableBorrowRate = _baseVariableBorrowRate.add(_variableRateSlope1).add( _variableRateSlope2.rayMul(excessUtilizationRateRatio) ); } else { vars.currentStableBorrowRate = vars.currentStableBorrowRate.add( _stableRateSlope1.rayMul(vars.utilizationRate.rayDiv(OPTIMAL_UTILIZATION_RATE)) ); vars.currentVariableBorrowRate = _baseVariableBorrowRate.add( vars.utilizationRate.rayMul(_variableRateSlope1).rayDiv(OPTIMAL_UTILIZATION_RATE) ); } vars.currentLiquidityRate = _getOverallBorrowRate( totalStableDebt, totalVariableDebt, vars .currentVariableBorrowRate, averageStableBorrowRate ) .rayMul(vars.utilizationRate) .percentMul(PercentageMath.PERCENTAGE_FACTOR.sub(reserveFactor)); return ( vars.currentLiquidityRate, vars.currentStableBorrowRate, vars.currentVariableBorrowRate ); } /** * @dev Calculates the overall borrow rate as the weighted average between the total variable debt and total stable debt * @param totalStableDebt The total borrowed from the reserve a stable rate * @param totalVariableDebt The total borrowed from the reserve at a variable rate * @param currentVariableBorrowRate The current variable borrow rate of the reserve * @param currentAverageStableBorrowRate The current weighted average of all the stable rate loans * @return The weighted averaged borrow rate **/ function _getOverallBorrowRate( uint256 totalStableDebt, uint256 totalVariableDebt, uint256 currentVariableBorrowRate, uint256 currentAverageStableBorrowRate ) internal pure returns (uint256) { uint256 totalDebt = totalStableDebt.add(totalVariableDebt); if (totalDebt == 0) return 0; uint256 weightedVariableRate = totalVariableDebt.wadToRay().rayMul(currentVariableBorrowRate); uint256 weightedStableRate = totalStableDebt.wadToRay().rayMul(currentAverageStableBorrowRate); uint256 overallBorrowRate = weightedVariableRate.add(weightedStableRate).rayDiv(totalDebt.wadToRay()); return overallBorrowRate; }
GenericLogic
balanceDecreaseAllowed
The
balanceDecreaseAllowed function is a critical risk management component that determines if a user can safely withdraw or reduce their collateral position without making their health factor fall below the liquidation threshold (1.0).Steps:
- Early Exit Conditions
- User isn't borrowing anything (no debt = no risk)
- Asset isn't being used as collateral (withdrawal doesn't affect borrowing capacity)
Allowed Withdrawl Without Check:
- Reserve Parameter Extraction
liquidationThreshold: Minimum collateral value percentage before liquidation (e.g., 80% = 8000)decimals: Asset precision for price conversion- If liquidation threshold is 0, asset can't be used as collateral → withdrawal always allowed
- Current User Position Calculation
totalCollateralInETH: Total value of all collateral assets in ETHtotalDebtInETH: Total value of all borrowed assets in ETHavgLiquidationThreshold: Weighted average of all collateral thresholds
- No Debt Check
- If user has no debt, withdrawal is always safe
- No risk of liquidation without outstanding loans
- Withdrawal Amount Conversion to ETH
- Converts withdrawal amount from asset units to ETH value
- Uses oracle for current market price
- Zero Collateral Check
- Prevents complete collateral removal while having debt
- Would result in immediate liquidation (health factor = 0)
- Recalculated Liquidation Threshold
This calculation is exactly same with V1.
- Health Factor Calculation and Check
/// --- contract/protocol-v2/contracts/protocol/libraries/logic/GenericLogic.sol --- /** * @dev Checks if a specific balance decrease is allowed * (i.e. doesn't bring the user borrow position health factor under HEALTH_FACTOR_LIQUIDATION_THRESHOLD) * @param asset The address of the underlying asset of the reserve * @param user The address of the user * @param amount The amount to decrease * @param reservesData The data of all the reserves * @param userConfig The user configuration * @param reserves The list of all the active reserves * @param oracle The address of the oracle contract * @return true if the decrease of the balance is allowed **/ function balanceDecreaseAllowed( address asset, address user, uint256 amount, mapping(address => DataTypes.ReserveData) storage reservesData, DataTypes.UserConfigurationMap calldata userConfig, mapping(uint256 => address) storage reserves, uint256 reservesCount, address oracle ) external view returns (bool) { /// Early Exit Conditions if (!userConfig.isBorrowingAny() || !userConfig.isUsingAsCollateral(reservesData[asset].id)) { return true; } balanceDecreaseAllowedLocalVars memory vars; /// Reserve Parameter Extraction (, vars.liquidationThreshold, , vars.decimals, ) = reservesData[asset] .configuration .getParams(); if (vars.liquidationThreshold == 0) { return true; } /// Current User Position Calculation ( vars.totalCollateralInETH, vars.totalDebtInETH, , vars.avgLiquidationThreshold, ) = calculateUserAccountData(user, reservesData, userConfig, reserves, reservesCount, oracle); /// No Debt Check if (vars.totalDebtInETH == 0) { return true; } /// Withdrawal Amount Conversion to ETH vars.amountToDecreaseInETH = IPriceOracleGetter(oracle).getAssetPrice(asset).mul(amount).div( 10**vars.decimals ); vars.collateralBalanceAfterDecrease = vars.totalCollateralInETH.sub(vars.amountToDecreaseInETH); /// Zero Collateral Check //if there is a borrow, there can't be 0 collateral if (vars.collateralBalanceAfterDecrease == 0) { return false; } /// Recalculated Liquidation Threshold vars.liquidationThresholdAfterDecrease = vars .totalCollateralInETH .mul(vars.avgLiquidationThreshold) .sub(vars.amountToDecreaseInETH.mul(vars.liquidationThreshold)) .div(vars.collateralBalanceAfterDecrease); /// Health Factor Calculation and Check uint256 healthFactorAfterDecrease = calculateHealthFactorFromBalances( vars.collateralBalanceAfterDecrease, vars.totalDebtInETH, vars.liquidationThresholdAfterDecrease ); return healthFactorAfterDecrease >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD; } /** * @dev Used to validate if a user has been borrowing from any reserve * @param self The configuration object * @return True if the user has been borrowing any reserve, false otherwise **/ function isBorrowingAny(DataTypes.UserConfigurationMap memory self) internal pure returns (bool) { return self.data & BORROWING_MASK != 0; } /** * @dev Used to validate if a user has been using the reserve as collateral * @param self The configuration object * @param reserveIndex The index of the reserve in the bitmap * @return True if the user has been using a reserve as collateral, false otherwise **/ function isUsingAsCollateral(DataTypes.UserConfigurationMap memory self, uint256 reserveIndex) internal pure returns (bool) { require(reserveIndex < 128, Errors.UL_INVALID_INDEX); return (self.data >> (reserveIndex * 2 + 1)) & 1 != 0; }
calculateUserAccountData
calculateUserAccountData calculates user’s status(same as V1), including:- total collateral in ETH
- total Debt in ETH
- average Ltv (Loan to Value)
- average liquidation threshold
- health factor
/// --- contract/protocol-v2/contracts/protocol/libraries/logic/GenericLogic.sol --- /** * @dev Calculates the user data across the reserves. * this includes the total liquidity/collateral/borrow balances in ETH, * the average Loan To Value, the average Liquidation Ratio, and the Health factor. * @param user The address of the user * @param reservesData Data of all the reserves * @param userConfig The configuration of the user * @param reserves The list of the available reserves * @param oracle The price oracle address * @return The total collateral and total debt of the user in ETH, the avg ltv, liquidation threshold and the HF **/ function calculateUserAccountData( address user, mapping(address => DataTypes.ReserveData) storage reservesData, DataTypes.UserConfigurationMap memory userConfig, mapping(uint256 => address) storage reserves, uint256 reservesCount, address oracle ) internal view returns ( uint256, uint256, uint256, uint256, uint256 ) { CalculateUserAccountDataVars memory vars; if (userConfig.isEmpty()) { return (0, 0, 0, 0, uint256(-1)); } for (vars.i = 0; vars.i < reservesCount; vars.i++) { if (!userConfig.isUsingAsCollateralOrBorrowing(vars.i)) { continue; } vars.currentReserveAddress = reserves[vars.i]; DataTypes.ReserveData storage currentReserve = reservesData[vars.currentReserveAddress]; (vars.ltv, vars.liquidationThreshold, , vars.decimals, ) = currentReserve .configuration .getParams(); vars.tokenUnit = 10**vars.decimals; vars.reserveUnitPrice = IPriceOracleGetter(oracle).getAssetPrice(vars.currentReserveAddress); if (vars.liquidationThreshold != 0 && userConfig.isUsingAsCollateral(vars.i)) { vars.compoundedLiquidityBalance = IERC20(currentReserve.aTokenAddress).balanceOf(user); uint256 liquidityBalanceETH = vars.reserveUnitPrice.mul(vars.compoundedLiquidityBalance).div(vars.tokenUnit); vars.totalCollateralInETH = vars.totalCollateralInETH.add(liquidityBalanceETH); vars.avgLtv = vars.avgLtv.add(liquidityBalanceETH.mul(vars.ltv)); vars.avgLiquidationThreshold = vars.avgLiquidationThreshold.add( liquidityBalanceETH.mul(vars.liquidationThreshold) ); } if (userConfig.isBorrowing(vars.i)) { vars.compoundedBorrowBalance = IERC20(currentReserve.stableDebtTokenAddress).balanceOf( user ); vars.compoundedBorrowBalance = vars.compoundedBorrowBalance.add( IERC20(currentReserve.variableDebtTokenAddress).balanceOf(user) ); vars.totalDebtInETH = vars.totalDebtInETH.add( vars.reserveUnitPrice.mul(vars.compoundedBorrowBalance).div(vars.tokenUnit) ); } } vars.avgLtv = vars.totalCollateralInETH > 0 ? vars.avgLtv.div(vars.totalCollateralInETH) : 0; vars.avgLiquidationThreshold = vars.totalCollateralInETH > 0 ? vars.avgLiquidationThreshold.div(vars.totalCollateralInETH) : 0; vars.healthFactor = calculateHealthFactorFromBalances( vars.totalCollateralInETH, vars.totalDebtInETH, vars.avgLiquidationThreshold ); return ( vars.totalCollateralInETH, vars.totalDebtInETH, vars.avgLtv, vars.avgLiquidationThreshold, vars.healthFactor ); } /** * @dev Calculates the health factor from the corresponding balances * @param totalCollateralInETH The total collateral in ETH * @param totalDebtInETH The total debt in ETH * @param liquidationThreshold The avg liquidation threshold * @return The health factor calculated from the balances provided **/ function calculateHealthFactorFromBalances( uint256 totalCollateralInETH, uint256 totalDebtInETH, uint256 liquidationThreshold ) internal pure returns (uint256) { if (totalDebtInETH == 0) return uint256(-1); return (totalCollateralInETH.percentMul(liquidationThreshold)).wadDiv(totalDebtInETH); }
StableDebtToken
mint
The
mint function is called when a user borrows at a stable interest rate. It handles the creation of new stable debt tokens while maintaining weighted average interest rates to track accrued interest.parameters:
onlyLendingPool: Only the LendingPool contract can call this function
- Parameters:
user: The address initiating the borrowonBehalfOf: The address that will own the debt (could be different for credit delegation)amount: The principal amount being borrowedrate: The stable interest rate at time of borrowing
Steps:
- Credit Delegation Handling
- User A grants borrowing allowance to User B
- User B borrows on behalf of User A. (Debt token is then minted to user A)
- Allowance is decreased accordingly
In AAVE V2, it allows user to grant credit to other users.
For example:
- Interest Accrual Calculation
Calculate user’s previous principal, interest from last update time, current principal + interest.
- Supply and Rate Variables Setup
previousSupply: Total debt before new borrowcurrentAvgStableRate: Current market average stable ratenextSupply: New total debt after borrowamountInRay: Borrow amount converted to Ray (27 decimals) for precision
- Weighted Average Rate Calculation
Average rate is necessary to calculate rate accrual.
- Rate Safety Check
Prevents rate overflow (rates stored as uint128)
- Updates user's personal stable rate
- Timestamp Updates
- Records current block timestamp for both user and total supply
- Used for future interest accrual calculations
- Global Average Rate Update
Calculate Market-Wide weighted average stable interest rate and update, necessary for interest calculation.
- Debt Token Minting
burn
The
burn function is called by the LendingPool when a user repays their stable debt. It reduces the user's debt by the specified amount and updates the total supply and average stable rate accordingly.Steps:
- User Debt Calculation
- Supply and Rate Variables Setup
- Edge Case: Repaying More Than Total Supply
- Last borrower repays slightly more than available due to rounding errors
- Protocol resets to clean state
- Normal Case: Partial Supply Reduction
- User State Update
- Full repayment: Reset user's rate and timestamp
- Partial repayment: Update timestamp for future interest accrual
- Debt Adjustment
- Scenario 1: Interest > Repayment Amount
- Scenario 2: Repayment Amount ≥ Interest
Adjust debt based on accumulated interest and repay amount.
mint additional debt token
burn diff debt token
/// --- contract/protocol-v2/contracts/protocol/tokenization/StableDebtToken.sol --- /** * @dev Burns debt of `user` * @param user The address of the user getting his debt burned * @param amount The amount of debt tokens getting burned **/ function burn(address user, uint256 amount) external override onlyLendingPool { /// User Debt Calculation (, uint256 currentBalance, uint256 balanceIncrease) = _calculateBalanceIncrease(user); /// Supply and Rate Variables Setup uint256 previousSupply = totalSupply(); uint256 newAvgStableRate = 0; uint256 nextSupply = 0; uint256 userStableRate = _usersStableRate[user]; /// Edge Case: Repaying More Than Total Supply // Since the total supply and each single user debt accrue separately, // there might be accumulation errors so that the last borrower repaying // mght actually try to repay more than the available debt supply. // In this case we simply set the total supply and the avg stable rate to 0 if (previousSupply <= amount) { _avgStableRate = 0; _totalSupply = 0; } else { /// Normal Case: Partial Supply Reduction nextSupply = _totalSupply = previousSupply.sub(amount); uint256 firstTerm = _avgStableRate.rayMul(previousSupply.wadToRay()); uint256 secondTerm = userStableRate.rayMul(amount.wadToRay()); // For the same reason described above, when the last user is repaying it might // happen that user rate * user balance > avg rate * total supply. In that case, // we simply set the avg rate to 0 if (secondTerm >= firstTerm) { newAvgStableRate = _avgStableRate = _totalSupply = 0; } else { newAvgStableRate = _avgStableRate = firstTerm.sub(secondTerm).rayDiv(nextSupply.wadToRay()); } } /// User State Update if (amount == currentBalance) { _usersStableRate[user] = 0; _timestamps[user] = 0; } else { //solium-disable-next-line _timestamps[user] = uint40(block.timestamp); } //solium-disable-next-line _totalSupplyTimestamp = uint40(block.timestamp); /// Debt Adjustment if (balanceIncrease > amount) { uint256 amountToMint = balanceIncrease.sub(amount); _mint(user, amountToMint, previousSupply); emit Mint( user, user, amountToMint, currentBalance, balanceIncrease, userStableRate, newAvgStableRate, nextSupply ); } else { uint256 amountToBurn = amount.sub(balanceIncrease); _burn(user, amountToBurn, previousSupply); emit Burn(user, amountToBurn, currentBalance, balanceIncrease, newAvgStableRate, nextSupply); } emit Transfer(user, address(0), amount); }
VariableDebtToken
/// --- contract/protocol-v2/contracts/protocol/tokenization/StableDebtToken.sol --- /** * @dev Mints debt token to the `onBehalfOf` address. * - Only callable by the LendingPool * - The resulting rate is the weighted average between the rate of the new debt * and the rate of the previous debt * @param user The address receiving the borrowed underlying, being the delegatee in case * of credit delegate, or same as `onBehalfOf` otherwise * @param onBehalfOf The address receiving the debt tokens * @param amount The amount of debt tokens to mint * @param rate The rate of the debt being minted **/ function mint( address user, address onBehalfOf, uint256 amount, uint256 rate ) external override onlyLendingPool returns (bool) { MintLocalVars memory vars; /// Credit Delegation Handling if (user != onBehalfOf) { _decreaseBorrowAllowance(onBehalfOf, user, amount); } /// Interest Accrual Calculation (, uint256 currentBalance, uint256 balanceIncrease) = _calculateBalanceIncrease(onBehalfOf); /// Supply and Rate Variables Setup vars.previousSupply = totalSupply(); vars.currentAvgStableRate = _avgStableRate; vars.nextSupply = _totalSupply = vars.previousSupply.add(amount); vars.amountInRay = amount.wadToRay(); /// Weighted Average Rate Calculation vars.newStableRate = _usersStableRate[onBehalfOf] .rayMul(currentBalance.wadToRay()) .add(vars.amountInRay.rayMul(rate)) .rayDiv(currentBalance.add(amount).wadToRay()); /// Rate Safety Check require(vars.newStableRate <= type(uint128).max, Errors.SDT_STABLE_DEBT_OVERFLOW); /// Updates user's personal stable rate _usersStableRate[onBehalfOf] = vars.newStableRate; /// Timestamp Updates //solium-disable-next-line _totalSupplyTimestamp = _timestamps[onBehalfOf] = uint40(block.timestamp); /// Global Average Rate Update // Calculates the updated average stable rate vars.currentAvgStableRate = _avgStableRate = vars .currentAvgStableRate .rayMul(vars.previousSupply.wadToRay()) .add(rate.rayMul(vars.amountInRay)) .rayDiv(vars.nextSupply.wadToRay()); /// Debt Token Minting _mint(onBehalfOf, amount.add(balanceIncrease), vars.previousSupply); emit Transfer(address(0), onBehalfOf, amount); emit Mint( user, onBehalfOf, amount, currentBalance, balanceIncrease, vars.newStableRate, vars.currentAvgStableRate, vars.nextSupply ); return currentBalance == 0; } /** * @dev Calculates the increase in balance since the last user interaction * @param user The address of the user for which the interest is being accumulated * @return The previous principal balance, the new principal balance and the balance increase **/ function _calculateBalanceIncrease(address user) internal view returns ( uint256, uint256, uint256 ) { uint256 previousPrincipalBalance = super.balanceOf(user); if (previousPrincipalBalance == 0) { return (0, 0, 0); } // Calculation of the accrued interest since the last accumulation uint256 balanceIncrease = balanceOf(user).sub(previousPrincipalBalance); return ( previousPrincipalBalance, previousPrincipalBalance.add(balanceIncrease), balanceIncrease ); } /** * @dev Mints stable debt tokens to an user * @param account The account receiving the debt tokens * @param amount The amount being minted * @param oldTotalSupply the total supply before the minting event **/ function _mint( address account, uint256 amount, uint256 oldTotalSupply ) internal { uint256 oldAccountBalance = _balances[account]; _balances[account] = oldAccountBalance.add(amount); if (address(_incentivesController) != address(0)) { _incentivesController.handleAction(account, oldTotalSupply, oldAccountBalance); } }
VariableDebtToken
mint
The
mint function is called when a user borrows at a variable interest rate. It handles the creation of new variable debt token.The difference against
StableDebtToken is, variable debt doesn’t need to know average debt rate as they share same variable rate. And it mints scaled debt token to onBehalfOf. Based on scaled debt token amount and variable rate index, protocol can calculate accrued interest easily./// --- contract/protocol-v2/contracts/protocol/tokenization/VariableDebtToken.sol --- /** * @dev Mints debt token to the `onBehalfOf` address * - Only callable by the LendingPool * @param user The address receiving the borrowed underlying, being the delegatee in case * of credit delegate, or same as `onBehalfOf` otherwise * @param onBehalfOf The address receiving the debt tokens * @param amount The amount of debt being minted * @param index The variable debt index of the reserve * @return `true` if the the previous balance of the user is 0 **/ function mint( address user, address onBehalfOf, uint256 amount, uint256 index ) external override onlyLendingPool returns (bool) { if (user != onBehalfOf) { _decreaseBorrowAllowance(onBehalfOf, user, amount); } uint256 previousBalance = super.balanceOf(onBehalfOf); uint256 amountScaled = amount.rayDiv(index); require(amountScaled != 0, Errors.CT_INVALID_MINT_AMOUNT); _mint(onBehalfOf, amountScaled); emit Transfer(address(0), onBehalfOf, amount); emit Mint(user, onBehalfOf, amount, index); return previousBalance == 0; }
burn
VariableDebtToken.burn calculates the scaled debt token based on current interest rate index, and burn it./// --- contract/protocol-v2/contracts/protocol/tokenization/VariableDebtToken.sol --- /** * @dev Burns user variable debt * - Only callable by the LendingPool * @param user The user whose debt is getting burned * @param amount The amount getting burned * @param index The variable debt index of the reserve **/ function burn(address user, uint256 amount, uint256 index) external override onlyLendingPool { uint256 amountScaled = amount.rayDiv(index); require(amountScaled != 0, Errors.CT_INVALID_BURN_AMOUNT); _burn(user, amountScaled); emit Transfer(user, address(0), amount); emit Burn(user, amount, index); }
Data
ReserveData
/// --- v2/contract/protocol-v2/contracts/protocol/libraries/types/DataTypes.sol --- struct ReserveData { //stores the reserve configuration ReserveConfigurationMap configuration; //the liquidity index. Expressed in ray uint128 liquidityIndex; //variable borrow index. Expressed in ray uint128 variableBorrowIndex; //the current supply rate. Expressed in ray uint128 currentLiquidityRate; //the current variable borrow rate. Expressed in ray uint128 currentVariableBorrowRate; //the current stable borrow rate. Expressed in ray uint128 currentStableBorrowRate; uint40 lastUpdateTimestamp; //tokens addresses address aTokenAddress; address stableDebtTokenAddress; address variableDebtTokenAddress; //address of the interest rate strategy address interestRateStrategyAddress; //the id of the reserve. Represents the position in the list of the active reserves uint8 id; }
ReserveConfigurationMap
/// --- v2/contract/protocol-v2/contracts/protocol/libraries/types/DataTypes.sol --- 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: stable rate borrowing enabled //bit 60-63: reserved //bit 64-79: reserve factor uint256 data; } /// --- contract/protocol-v2/contracts/protocol/libraries/configuration/ReserveConfiguration.sol --- /** * @dev Gets the configuration flags of the reserve * @param self The reserve configuration * @return The state flags representing active, frozen, borrowing enabled, stableRateBorrowing enabled **/ function getFlags(DataTypes.ReserveConfigurationMap storage self) internal view returns ( bool, bool, bool, bool ) { uint256 dataLocal = self.data; return ( (dataLocal & ~ACTIVE_MASK) != 0, (dataLocal & ~FROZEN_MASK) != 0, (dataLocal & ~BORROWING_MASK) != 0, (dataLocal & ~STABLE_BORROWING_MASK) != 0 ); } /** * @dev Gets the reserve factor of the reserve * @param self The reserve configuration * @return The reserve factor **/ function getReserveFactor(DataTypes.ReserveConfigurationMap storage self) internal view returns (uint256) { return (self.data & ~RESERVE_FACTOR_MASK) >> RESERVE_FACTOR_START_BIT_POSITION; }