Reserve Data
Aave V1’s
ReserveData
struct is the heart of the lending pool. It holds the dynamic state for each underlying asset (ETH, DAI, USDC, …)/// --- v1/contract/aave-protocol/contracts/libraries/CoreLibrary.sol --- struct ReserveData { /** * @dev refer to the whitepaper, section 1.1 basic concepts for a formal description of these properties. **/ //the liquidity index. Expressed in ray uint256 lastLiquidityCumulativeIndex; //the current supply rate. Expressed in ray uint256 currentLiquidityRate; //the total borrows of the reserve at a stable rate. Expressed in the currency decimals uint256 totalBorrowsStable; //the total borrows of the reserve at a variable rate. Expressed in the currency decimals uint256 totalBorrowsVariable; //the current variable borrow rate. Expressed in ray uint256 currentVariableBorrowRate; //the current stable borrow rate. Expressed in ray uint256 currentStableBorrowRate; //the current average stable borrow rate (weighted average of all the different stable rate loans). Expressed in ray uint256 currentAverageStableBorrowRate; //variable borrow index. Expressed in ray uint256 lastVariableBorrowCumulativeIndex; //the ltv of the reserve. Expressed in percentage (0-100) uint256 baseLTVasCollateral; //the liquidation threshold of the reserve. Expressed in percentage (0-100) uint256 liquidationThreshold; //the liquidation bonus of the reserve. Expressed in percentage uint256 liquidationBonus; //the decimals of the reserve asset uint256 decimals; /** * @dev address of the aToken representing the asset **/ address aTokenAddress; /** * @dev address of the interest rate strategy contract **/ address interestRateStrategyAddress; uint40 lastUpdateTimestamp; // borrowingEnabled = true means users can borrow from this reserve bool borrowingEnabled; // usageAsCollateralEnabled = true means users can use this reserve as collateral bool usageAsCollateralEnabled; // isStableBorrowRateEnabled = true means users can borrow at a stable rate bool isStableBorrowRateEnabled; // isActive = true means the reserve has been activated and properly configured bool isActive; // isFreezed = true means the reserve only allows repays and redeems, but not deposits, new borrowings or rate swap bool isFreezed; }
1. Liquidity Index
lastLiquidityCumulativeIndex
- Tracks how much depositors’ balances should grow due to interest.
- Expressed in ray (1e27).
- Starts at 1e27; increases over time as borrowers pay interest.
- Example:
- Alice deposits 100 DAI at index =
1e27
. - After 1 year, index =
1.05e27
(5% yield). - Alice’s balance becomes
100 * (1.05e27 / 1e27) = 105 DAI
.
2. Current Liquidity Rate
currentLiquidityRate
- Instantaneous APY depositors are earning, expressed in ray.
- Determined by the interest rate model given utilization (borrowed/total liquidity).
- Example:
- 80% utilization →
currentLiquidityRate = 5% (0.05e27)
- Means: depositors will accrue ~5% per year if rates stay constant.
3. Total Borrows
totalBorrowsStable
- Sum of all outstanding borrows at a stable rate.
totalBorrowsVariable
- Sum of all outstanding borrows at a variable rate.
- Example:
- Bob borrows 200 DAI stable, Carol borrows 100 DAI variable.
totalBorrowsStable = 200
,totalBorrowsVariable = 100
.- Total borrows =
300
.
4. Borrow Rates
currentVariableBorrowRate
- The rate new variable loans will pay.
currentStableBorrowRate
- The “base” stable rate for new loans.
currentAverageStableBorrowRate
- Weighted average of all outstanding stable borrow positions.
- Example:
- Bob borrowed 200 DAI stable at 4%.
- Dave borrows 300 DAI stable at 6%.
- Average stable =
(200*4% + 300*6%) / (200+300) = 5.2%
.
5. Borrow Index
lastVariableBorrowCumulativeIndex
- Similar to liquidity index, but for variable debt growth.
- Each borrower’s debt balance grows according to this index.
- Example:
- Carol borrows 100 DAI at index =
1e27
. - After 1 year, index =
1.1e27
. - Carol’s debt =
100 * (1.1e27 / 1e27) = 110 DAI
.
6. Collateral Parameters
baseLTVasCollateral
- Max loan-to-value when using this asset as collateral.
- Example: DAI baseLTV = 75%.
- Alice deposits 100 DAI → can borrow up to 75 DAI worth.
liquidationThreshold
- The max % of debt-to-collateral value before liquidation triggers.
- Example: threshold = 80%. If Alice’s borrow value rises above 80% of her collateral, she can be liquidated.
liquidationBonus
- Discount for liquidators.
- Example: 105% → liquidators can buy Alice’s collateral at a 5% discount.
7. Decimals
decimals
- Number of decimals of the token (DAI = 18, USDC = 6).
- Ensures calculations scale properly.
8. Addresses
aTokenAddress
- The aToken representing user deposits in this reserve.
- Example: Alice deposits 100 DAI → she receives 100 aDAI.
interestRateStrategyAddress
- Contract that defines the interest rate curve (utilization → rate).
9. Bookkeeping Flags
lastUpdateTimestamp
- Last block timestamp when reserve indexes were updated.
- Needed to compute linear/compounded interest.
borrowingEnabled
- Whether borrowing from this reserve is allowed.
usageAsCollateralEnabled
- Whether this asset can be posted as collateral.
isStableBorrowRateEnabled
- Whether stable-rate borrowing is allowed.
isActive
- Whether the reserve is live.
isFreezed
- Reserve is frozen (only repay/withdraw allowed).
How it all works together (Flow Example)
- Alice deposits 1,000 DAI
- Liquidity increases.
aTokenAddress
mints 1,000 aDAI.- Liquidity index =
1e27
.
- Bob borrows 500 DAI (variable rate)
totalBorrowsVariable
= 500.- Utilization = 500 / 1000 = 50%.
interestRateStrategy
sets:- Variable borrow rate = 4%
- Liquidity rate = 2%.
- Bob’s debt tracked via
lastVariableBorrowCumulativeIndex
.
- Time passes
- Interest accrues:
- Bob’s debt grows using variable borrow index.
- Alice’s aDAI grows using liquidity index.
- If price drops
- Alice’s 1,000 DAI collateral → worth less in USD.
- If borrow/collateral ratio >
liquidationThreshold
, liquidation triggers. - Liquidator repays some of Bob’s debt, gets Alice’s DAI at
liquidationBonus
.
So
ReserveData
is basically a state snapshot of the lending pool for one token:- Tracks interest accrual (indexes, rates).
- Tracks credit/debt totals (stable/variable).
- Tracks collateral rules (LTV, thresholds, bonus).
- Tracks permissions & config (is it borrowable, collateral, frozen).
Lending Pool
Deposit
Liquidity provider calls
deposit
function to deposit reserve into aave protocol. And they get corresponding aToken in return whose dynamic balance equals liquidity user owns including interest it generates.Process:
- Check the reserve asset status
- Reserve asset is active
- Reserve asset is not freezed
Reserve asset status is recorded in
LendingPoolCore
- Check the amount of reserve asset to deposit is greater than zero
- Check whether it’s the first time user deposits this reserve asset
- Get corresponding aToken address from
LendingPoolCore
contract. - By checking whether corrsponding aToken balance is 0.
- Update reserve status in LendingPoolCore contract. Refer
- update cumulative variable borrow interest rate index and liquidity rate of reserve
- updates the reserve current stable borrow rate Rf, the current variable borrow rate Rv and the current liquidity rate Rl based on reserve interest strategy.
- initialize user’s state if it’s the first time user deposits (mark user’s deposit can be used as collateral)
- Mint corresponding aToken to user based on exchange ratio.
- based on newest cumulative liquidity rate and the indexed rate of user, calculate newly generated interest since last time upate.
- mint interest and new deposit to user.
- it also updates balance of user to whom this user redirects balance.
aToken balance represents the liquidity user provides (includes the interest generated)
aToken is a rebate token, whose
balanceOf
function is dynamic. balanceOf
function return balance plus generated interests dynamically calculated based on cumulative liquidity interest rate and user’s latest indexed interest rate.- Transfer reserve from user to LendingPoolCore contract.
- By calling
LendingPoolCore
contract transferToReserve method.
- Emit Event.
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPool.sol --- /** * @dev deposits The underlying asset into the reserve. A corresponding amount of the overlying asset (aTokens) * is minted. * @param _reserve the address of the reserve * @param _amount the amount to be deposited * @param _referralCode integrators are assigned a referral code and can potentially receive rewards. **/ function deposit(address _reserve, uint256 _amount, uint16 _referralCode) external payable nonReentrant /// --- Check the reserve asset status --- onlyActiveReserve(_reserve) onlyUnfreezedReserve(_reserve) /// --- Check the amount of reserve asset to deposit is greater than zero --- onlyAmountGreaterThanZero(_amount) { AToken aToken = AToken(core.getReserveATokenAddress(_reserve)); /// --- Check whether it’s the first time user deposits this reserve asset --- bool isFirstDeposit = aToken.balanceOf(msg.sender) == 0; /// --- Update reserve status in LendingPoolCore contract. --- core.updateStateOnDeposit(_reserve, msg.sender, _amount, isFirstDeposit); /// minting AToken to user 1:1 with the specific exchange rate aToken.mintOnDeposit(msg.sender, _amount); //transfer to the core contract core.transferToReserve.value(msg.value)(_reserve, msg.sender, _amount); //solium-disable-next-line emit Deposit(_reserve, msg.sender, _amount, _referralCode, block.timestamp); } /** * @dev functions affected by this modifier can only be invoked if the reserve is active * @param _reserve the address of the reserve **/ modifier onlyActiveReserve(address _reserve) { requireReserveActiveInternal(_reserve); _; } /** * @dev internal function to save on code size for the onlyActiveReserve modifier **/ function requireReserveActiveInternal(address _reserve) internal view { require(core.getReserveIsActive(_reserve), "Action requires an active reserve"); } /** * @dev functions affected by this modifier can only be invoked if the reserve is not freezed. * A freezed reserve only allows redeems, repays, rebalances and liquidations. * @param _reserve the address of the reserve **/ modifier onlyUnfreezedReserve(address _reserve) { requireReserveNotFreezedInternal(_reserve); _; } /** * @notice internal function to save on code size for the onlyUnfreezedReserve modifier **/ function requireReserveNotFreezedInternal(address _reserve) internal view { require(!core.getReserveIsFreezed(_reserve), "Action requires an unfreezed reserve"); } /** * @dev functions affected by this modifier can only be invoked if the provided _amount input parameter * is not zero. * @param _amount the amount provided **/ modifier onlyAmountGreaterThanZero(uint256 _amount) { requireAmountGreaterThanZeroInternal(_amount); _; } /** * @notice internal function to save on code size for the onlyAmountGreaterThanZero modifier **/ function requireAmountGreaterThanZeroInternal(uint256 _amount) internal pure { require(_amount > 0, "Amount must be greater than 0"); } /// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- mapping(address => CoreLibrary.ReserveData) internal reserves; /** * @dev returns true if the reserve is active * @param _reserve the reserve address * @return true if the reserve is active, false otherwise **/ function getReserveIsActive(address _reserve) external view returns (bool) { CoreLibrary.ReserveData storage reserve = reserves[_reserve]; return reserve.isActive; } /** * @notice returns if a reserve is freezed * @param _reserve the reserve for which the information is needed * @return true if the reserve is freezed, false otherwise **/ function getReserveIsFreezed(address _reserve) external view returns (bool) { CoreLibrary.ReserveData storage reserve = reserves[_reserve]; return reserve.isFreezed; } /// --- v1/contract/aave-protocol/contracts/libraries/CoreLibrary.sol --- struct ReserveData { /** * @dev refer to the whitepaper, section 1.1 basic concepts for a formal description of these properties. **/ //the liquidity index. Expressed in ray uint256 lastLiquidityCumulativeIndex; //the current supply rate. Expressed in ray uint256 currentLiquidityRate; //the total borrows of the reserve at a stable rate. Expressed in the currency decimals uint256 totalBorrowsStable; //the total borrows of the reserve at a variable rate. Expressed in the currency decimals uint256 totalBorrowsVariable; //the current variable borrow rate. Expressed in ray uint256 currentVariableBorrowRate; //the current stable borrow rate. Expressed in ray uint256 currentStableBorrowRate; //the current average stable borrow rate (weighted average of all the different stable rate loans). Expressed in ray uint256 currentAverageStableBorrowRate; //variable borrow index. Expressed in ray uint256 lastVariableBorrowCumulativeIndex; //the ltv of the reserve. Expressed in percentage (0-100) uint256 baseLTVasCollateral; //the liquidation threshold of the reserve. Expressed in percentage (0-100) uint256 liquidationThreshold; //the liquidation bonus of the reserve. Expressed in percentage uint256 liquidationBonus; //the decimals of the reserve asset uint256 decimals; /** * @dev address of the aToken representing the asset **/ address aTokenAddress; /** * @dev address of the interest rate strategy contract **/ address interestRateStrategyAddress; uint40 lastUpdateTimestamp; // borrowingEnabled = true means users can borrow from this reserve bool borrowingEnabled; // usageAsCollateralEnabled = true means users can use this reserve as collateral bool usageAsCollateralEnabled; // isStableBorrowRateEnabled = true means users can borrow at a stable rate bool isStableBorrowRateEnabled; // isActive = true means the reserve has been activated and properly configured bool isActive; // isFreezed = true means the reserve only allows repays and redeems, but not deposits, new borrowings or rate swap bool isFreezed; }
RedeemUnderlying
redeemUnderlying
is invoked by the reserve’s aToken contract to withdraw _amount
of the underlying asset from the pool and send it to _user
. It exists because aTokens are claim tokens: users burn aTokens and the LendingPool executes the actual underlying transfer.Steps:
- Access control and modifiers
- only the correct aToken contract for
_reserve
can call this function - the reserve is currently active
- the amount is non-zero
The function is guarded by
onlyOverlyingAToken(_reserve)
, onlyActiveReserve(_reserve)
, and onlyAmountGreaterThanZero(_amount)
. These ensure:- Check available liquidity
prevents attempts to redeem more underlying than the reserve actually holds
- Update core accounting for the redeem
core.updateStateOnRedeem
is called to update protocol bookkeeping before the transfer. This updates reserve-level state (rate checkpoint, liquidity and borrow rate) and user-level flags (e.g., whether the user still has collateral). The boolean (_aTokenBalanceAfterRedeem == 0)
signals if the user emptied their aToken balance so core can reset user state/storage if needed.- Transfer the underlying to the user
core.transferToUser
performs the actual token transfer from the protocol to the recipient.Why only the aToken contract can call this function?
onlyOverlyingAToken
requires msg.sender
to be the aToken contract mapped to _reserve
. The normal flow is: user calls aToken.redeem
(which accrues interest, burns aTokens, computes remaining aToken balance) → aToken calls lendingPool.redeemUnderlying
to actually withdraw underlying. This ordering ensures accounting in the aToken and lending pool stay synchronized./// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPool.sol --- /** * @dev Redeems the underlying amount of assets requested by _user. * This function is executed by the overlying aToken contract in response to a redeem action. * @param _reserve the address of the reserve * @param _user the address of the user performing the action * @param _amount the underlying amount to be redeemed **/ function redeemUnderlying( address _reserve, address payable _user, uint256 _amount, uint256 _aTokenBalanceAfterRedeem ) external nonReentrant onlyOverlyingAToken(_reserve) onlyActiveReserve(_reserve) onlyAmountGreaterThanZero(_amount) { /// Check available liquidity uint256 currentAvailableLiquidity = core.getReserveAvailableLiquidity(_reserve); require( currentAvailableLiquidity >= _amount, "There is not enough liquidity available to redeem" ); /// Update core accounting for the redeem core.updateStateOnRedeem(_reserve, _user, _amount, _aTokenBalanceAfterRedeem == 0); /// Transfer the underlying to the user core.transferToUser(_reserve, _user, _amount); //solium-disable-next-line emit RedeemUnderlying(_reserve, _user, _amount, block.timestamp); } /// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev gets the available liquidity in the reserve. The available liquidity is the balance of the core contract * @param _reserve the reserve address * @return the available liquidity **/ function getReserveAvailableLiquidity(address _reserve) public view returns (uint256) { uint256 balance = 0; if (_reserve == EthAddressLib.ethAddress()) { balance = address(this).balance; } else { balance = IERC20(_reserve).balanceOf(address(this)); } return balance; }
Borrow
The
borrow
function allows users to borrow a specific amount of the reserve currency, provided that the borrower has deposited enough collateral.Steps:
- Initial Checks and Validation
- Modifiers:
nonReentrant
: Prevents reentrancy attacksonlyActiveReserve
: Ensures the reserve is activeonlyUnfreezedReserve
: Ensures the reserve isn't frozenonlyAmountGreaterThanZero
: Ensures borrow amount > 0- Basic Validation:
- Checks borrowing is enabled for the reserve (
core.isReserveBorrowingEnabled
) - Validates interest rate mode (must be stable or variable)
- Liquidity Check
- Gets available liquidity from core (
core.getReserveAvailableLiquidity
) - Ensures sufficient liquidity exists for the borrow amount
core.getReserveAvailableLiquidity
just returns the balance of reserve of LendingPoolCore
contract- User Data Calculation
- Iterates through all user reserves
- Calculates:
- Total collateral balance in ETH
- Total borrow balance in ETH
- Total fees in ETH
- Current LTV ratio
- Liquidation threshold
- Health factor
- Uses price oracle to convert assets to ETH values
Calls
dataProvider.calculateUserGlobalData
which:- Health and Collateral Checks
- Requires positive collateral balance
- Requires health factor above liquidation threshold
- Calculates origination fee using
feeProvider.calculateLoanOriginationFee
(refer) - Calculates required collateral using
dataProvider.calculateCollateralNeededInETH
- Ensures user has sufficient collateral
- Stable Rate Specific Checks
- Checks if user can borrow at stable rate (
core.isUserAllowedToBorrowAtStable
)(refer) - Verifies borrow amount doesn't exceed stable rate borrow limit
For stable rate borrows:
- State Updates
- Gets current user borrow balances (
getUserBorrowBalances
) - Updates reserve state:
- Updates cumulative indexes
- Adjusts total borrows based on rate mode
- Updates user state:
- Sets stable rate or variable rate index
- Increases principal balance and origination fee
- Updates reserve interest rates
Calls
core.updateStateOnBorrow
which:- Fund Transfer and Event
- Transfers borrowed amount to user (
core.transferToUser
) - Emits Borrow event
Notes:
- About the
amountOfCollateralNeededETH
calculation, it uses current user’s current borrow and collateral LTV to calculate instead of base LTV set in protocol.
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPool.sol --- /** * @dev data structures for local computations in the borrow() method. */ struct BorrowLocalVars { uint256 principalBorrowBalance; uint256 currentLtv; uint256 currentLiquidationThreshold; uint256 borrowFee; uint256 requestedBorrowAmountETH; uint256 amountOfCollateralNeededETH; uint256 userCollateralBalanceETH; uint256 userBorrowBalanceETH; uint256 userTotalFeesETH; uint256 borrowBalanceIncrease; uint256 currentReserveStableRate; uint256 availableLiquidity; uint256 reserveDecimals; uint256 finalUserBorrowRate; CoreLibrary.InterestRateMode rateMode; bool healthFactorBelowThreshold; } /// --- v1/contract/aave-protocol/contracts/libraries/CoreLibrary.sol --- enum InterestRateMode {NONE, STABLE, VARIABLE} /// --- /** * @dev Allows users to borrow a specific amount of the reserve currency, provided that the borrower * already deposited enough collateral. * @param _reserve the address of the reserve * @param _amount the amount to be borrowed * @param _interestRateMode the interest rate mode at which the user wants to borrow. Can be 0 (STABLE) or 1 (VARIABLE) **/ function borrow( address _reserve, uint256 _amount, uint256 _interestRateMode, uint16 _referralCode ) external nonReentrant onlyActiveReserve(_reserve) onlyUnfreezedReserve(_reserve) onlyAmountGreaterThanZero(_amount) { // Usage of a memory struct of vars to avoid "Stack too deep" errors due to local variables BorrowLocalVars memory vars; /// Initial Checks and Validation //check that the reserve is enabled for borrowing require(core.isReserveBorrowingEnabled(_reserve), "Reserve is not enabled for borrowing"); //validate interest rate mode require( uint256(CoreLibrary.InterestRateMode.VARIABLE) == _interestRateMode || uint256(CoreLibrary.InterestRateMode.STABLE) == _interestRateMode, "Invalid interest rate mode selected" ); //cast the rateMode to coreLibrary.interestRateMode vars.rateMode = CoreLibrary.InterestRateMode(_interestRateMode); //check that the amount is available in the reserve vars.availableLiquidity = core.getReserveAvailableLiquidity(_reserve); require( vars.availableLiquidity >= _amount, "There is not enough liquidity available in the reserve" ); /// User Data Calculation ( , vars.userCollateralBalanceETH, vars.userBorrowBalanceETH, vars.userTotalFeesETH, vars.currentLtv, vars.currentLiquidationThreshold, , vars.healthFactorBelowThreshold ) = dataProvider.calculateUserGlobalData(msg.sender); /// Health and Collateral Checks require(vars.userCollateralBalanceETH > 0, "The collateral balance is 0"); require( !vars.healthFactorBelowThreshold, "The borrower can already be liquidated so he cannot borrow more" ); //calculating fees vars.borrowFee = feeProvider.calculateLoanOriginationFee(msg.sender, _amount); require(vars.borrowFee > 0, "The amount to borrow is too small"); vars.amountOfCollateralNeededETH = dataProvider.calculateCollateralNeededInETH( _reserve, _amount, vars.borrowFee, vars.userBorrowBalanceETH, vars.userTotalFeesETH, vars.currentLtv ); require( vars.amountOfCollateralNeededETH <= vars.userCollateralBalanceETH, "There is not enough collateral to cover a new borrow" ); /// Stable Rate 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 relatively small, configurable amount of the total * liquidity **/ if (vars.rateMode == CoreLibrary.InterestRateMode.STABLE) { //check if the borrow mode is stable and if stable rate borrowing is enabled on this reserve require( core.isUserAllowedToBorrowAtStable(_reserve, msg.sender, _amount), "User cannot borrow the selected amount with a stable rate" ); //calculate the max available loan size in stable rate mode as a percentage of the //available liquidity uint256 maxLoanPercent = parametersProvider.getMaxStableRateBorrowSizePercent(); uint256 maxLoanSizeStable = vars.availableLiquidity.mul(maxLoanPercent).div(100); require( _amount <= maxLoanSizeStable, "User is trying to borrow too much liquidity at a stable rate" ); } /// State Updates //all conditions passed - borrow is accepted (vars.finalUserBorrowRate, vars.borrowBalanceIncrease) = core.updateStateOnBorrow( _reserve, msg.sender, _amount, vars.borrowFee, vars.rateMode ); //if we reached this point, we can transfer core.transferToUser(_reserve, msg.sender, _amount); emit Borrow( _reserve, msg.sender, _amount, _interestRateMode, vars.finalUserBorrowRate, vars.borrowFee, vars.borrowBalanceIncrease, _referralCode, //solium-disable-next-line block.timestamp ); } /// --- v1/contract/aave-protocol/contracts/configuration/LendingPoolParametersProvider.sol --- uint256 private constant MAX_STABLE_RATE_BORROW_SIZE_PERCENT = 25; /** * @dev returns the maximum stable rate borrow size, in percentage of the available liquidity. **/ function getMaxStableRateBorrowSizePercent() external pure returns (uint256) { return MAX_STABLE_RATE_BORROW_SIZE_PERCENT; }
LendingPoolCore.isReserveBorrowingEnabled
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev returns true if the reserve is enabled for borrowing * @param _reserve the reserve address * @return true if the reserve is enabled for borrowing, false otherwise **/ function isReserveBorrowingEnabled(address _reserve) external view returns (bool) { CoreLibrary.ReserveData storage reserve = reserves[_reserve]; return reserve.borrowingEnabled; }
core.getReserveAvailableLiquidity
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev gets the available liquidity in the reserve. The available liquidity is the balance of the core contract * @param _reserve the reserve address * @return the available liquidity **/ function getReserveAvailableLiquidity(address _reserve) public view returns (uint256) { uint256 balance = 0; if (_reserve == EthAddressLib.ethAddress()) { balance = address(this).balance; } else { balance = IERC20(_reserve).balanceOf(address(this)); } return balance; }
Repay
The
repay
function is a crucial function that allows users to repay their borrowed amounts, including both principal and accrued interest.Steps:
- Initial Setup and Validation
- Modifiers:
nonReentrant
: Prevents reentrancy attacksonlyActiveReserve
: Ensures the reserve is activeonlyAmountGreaterThanZero
: Ensures the repayment amount is positive- Local Variables: Uses a struct
RepayLocalVars
to avoid stack too deep errors
- Get User Borrow Data
- Retrieves the user's principal balance, compounded balance (principal + interest), and interest accrued
- Gets the user's outstanding origination fee
- Checks if the reserve is ETH
- Validation Checks
- Ensures the user has an outstanding loan
- Prevents repaying the full amount on behalf of another user without explicit amount
- Calculate Repayment Amount
- Defaults to repaying the full amount (principal + interest + fee)
- Allows partial repayment if a specific amount is provided
- ETH Value Validation
For ETH repayments, ensures sufficient ETH is sent with the transaction
- Fee-Only Repayment Case
- Handles the special case where the repayment amount only covers part of the origination fee
- Updates state accordingly and transfers funds to the fee collection address
- Normal Repayment Case
- Calculate Principal Repayment
- Update Core State
- Updates both reserve and user state in the core contract
- Handles interest accrual and balance adjustments
- Transfer Fees
- Transfer Principal (handles excess msg.value refund inside)
For the normal case where the repayment covers both fee and principal:
Separates the repayment into fee portion and principal portion
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPool.sol --- /** * @notice repays a borrow on the specific reserve, for the specified amount (or for the whole amount, if uint256(-1) is specified). * @dev the target user is defined by _onBehalfOf. If there is no repayment on behalf of another account, * _onBehalfOf must be equal to msg.sender. * @param _reserve the address of the reserve on which the user borrowed * @param _amount the amount to repay, or uint256(-1) if the user wants to repay everything * @param _onBehalfOf the address for which msg.sender is repaying. **/ struct RepayLocalVars { uint256 principalBorrowBalance; uint256 compoundedBorrowBalance; uint256 borrowBalanceIncrease; bool isETH; uint256 paybackAmount; uint256 paybackAmountMinusFees; uint256 currentStableRate; uint256 originationFee; } function repay(address _reserve, uint256 _amount, address payable _onBehalfOf) external payable nonReentrant onlyActiveReserve(_reserve) onlyAmountGreaterThanZero(_amount) { /// Initial Setup and Validation // Usage of a memory struct of vars to avoid "Stack too deep" errors due to local variables RepayLocalVars memory vars; /// Get User Borrow Data ( vars.principalBorrowBalance, vars.compoundedBorrowBalance, vars.borrowBalanceIncrease ) = core.getUserBorrowBalances(_reserve, _onBehalfOf); vars.originationFee = core.getUserOriginationFee(_reserve, _onBehalfOf); vars.isETH = EthAddressLib.ethAddress() == _reserve; /// Validation Checks require(vars.compoundedBorrowBalance > 0, "The user does not have any borrow pending"); require( _amount != UINT_MAX_VALUE || msg.sender == _onBehalfOf, "To repay on behalf of an user an explicit amount to repay is needed." ); /// Calculate Repayment Amount //default to max amount vars.paybackAmount = vars.compoundedBorrowBalance.add(vars.originationFee); if (_amount != UINT_MAX_VALUE && _amount < vars.paybackAmount) { vars.paybackAmount = _amount; } /// ETH Value Validation require( !vars.isETH || msg.value >= vars.paybackAmount, "Invalid msg.value sent for the repayment" ); /// Fee-Only Repayment Case //if the amount is smaller than the origination fee, just transfer the amount to the fee destination address if (vars.paybackAmount <= vars.originationFee) { /// Update Core State core.updateStateOnRepay( _reserve, _onBehalfOf, 0, vars.paybackAmount, vars.borrowBalanceIncrease, false ); /// Transfer Principal core.transferToFeeCollectionAddress.value(vars.isETH ? vars.paybackAmount : 0)( _reserve, _onBehalfOf, vars.paybackAmount, addressesProvider.getTokenDistributor() ); emit Repay( _reserve, _onBehalfOf, msg.sender, 0, vars.paybackAmount, vars.borrowBalanceIncrease, //solium-disable-next-line block.timestamp ); return; } /// Normal Repayment Case /// Calculate Principal Repayment vars.paybackAmountMinusFees = vars.paybackAmount.sub(vars.originationFee); core.updateStateOnRepay( _reserve, _onBehalfOf, vars.paybackAmountMinusFees, vars.originationFee, vars.borrowBalanceIncrease, vars.compoundedBorrowBalance == vars.paybackAmountMinusFees ); /// Transfer Fees //if the user didn't repay the origination fee, transfer the fee to the fee collection address if(vars.originationFee > 0) { core.transferToFeeCollectionAddress.value(vars.isETH ? vars.originationFee : 0)( _reserve, msg.sender, vars.originationFee, addressesProvider.getTokenDistributor() ); } //sending the total msg.value if the transfer is ETH. //the transferToReserve() function will take care of sending the //excess ETH back to the caller core.transferToReserve.value(vars.isETH ? msg.value.sub(vars.originationFee) : 0)( _reserve, msg.sender, vars.paybackAmountMinusFees ); emit Repay( _reserve, _onBehalfOf, msg.sender, vars.paybackAmountMinusFees, vars.originationFee, vars.borrowBalanceIncrease, //solium-disable-next-line block.timestamp ); }
Liquidation
users can invoke
liquidationCall
function to liquidate an undercollateralized position.LendingPool.liquidationCall
deleges liquidation call to LiquidationManager contract./// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPool.sol --- /** * @dev users can invoke this function to liquidate an undercollateralized position. * @param _reserve the address of the collateral to liquidated * @param _reserve the address of the principal reserve * @param _user the address of the borrower * @param _purchaseAmount the amount of principal that the liquidator wants to repay * @param _receiveAToken true if the liquidators wants to receive the aTokens, false if * he wants to receive the underlying asset directly **/ function liquidationCall( address _collateral, address _reserve, address _user, uint256 _purchaseAmount, bool _receiveAToken ) external payable nonReentrant onlyActiveReserve(_reserve) onlyActiveReserve(_collateral) { address liquidationManager = addressesProvider.getLendingPoolLiquidationManager(); //solium-disable-next-line (bool success, bytes memory result) = liquidationManager.delegatecall( abi.encodeWithSignature( "liquidationCall(address,address,address,uint256,bool)", _collateral, _reserve, _user, _purchaseAmount, _receiveAToken ) ); require(success, "Liquidation call failed"); (uint256 returnCode, string memory returnMessage) = abi.decode(result, (uint256, string)); if (returnCode != 0) { //error found revert(string(abi.encodePacked("Liquidation failed: ", returnMessage))); } }
liquidationCall
function in the LendingPoolLiquidationManager
contract is designed to allow liquidators to liquidate undercollateralized positions.Key Parameters:
_collateral
: The collateral asset to be liquidated.
_reserve
: The borrowed asset that needs repayment.
_user
: The borrower whose position is being liquidated.
_purchaseAmount
: The amount of the borrowed asset the liquidator wants to repay.
_receiveAToken
: Whether the liquidator wants to receive aTokens instead of the underlying asset.
Steps:
- Health Factor Check:
- Calls
dataProvider.calculateUserGlobalData
to check if the user's health factor is below the threshold. Reverts if not.
- Collateral Validation:
- Checks if the user has a positive balance of the specified collateral using
core.getUserUnderlyingAssetBalance
. - Verifies the collateral is enabled for the user via
core.isReserveUsageAsCollateralEnabled
andcore.isUserUseReserveAsCollateralEnabled
.
- Borrow Validation:
- Ensures the user has borrowed the specified currency using
core.getUserBorrowBalances
.
- Liquidation Amount Calculation:
- Computes the maximum amount that can be liquidated (50% of the borrowed balance due to
LIQUIDATION_CLOSE_FACTOR_PERCENT
). - Sets the actual amount to liquidate as the minimum between
_purchaseAmount
and the maximum allowed.
- Collateral Calculation:
- Calls
calculateAvailableCollateralToLiquidate
to determine how much collateral can be liquidated based on oracle prices and liquidation bonus.
- Origination Fee Handling:
- If the user has an origination fee, calculates additional collateral to cover the fee liquidation.
- Liquidity Check:
- If the liquidator does not want aTokens, checks if the reserve has enough underlying collateral assets.
- State Update:
- Calls
core.updateStateOnLiquidation
to update reserve and user data, including interest accruals and balance adjustments.
- Asset Transfers:
- Transfers collateral to the liquidator either as aTokens or underlying assets.
- Transfers the repaid borrowed asset from the liquidator to the reserve.
- If there's a fee, burns the corresponding aTokens and transfers the underlying collateral to the fee collector.
- Emit Events
Notes:
Seems there is a bug in
liquidationCall
function. The liquidationCall
function in the LendingPoolLiquidationManager
contract handles the liquidation process, including the liquidation of origination fees. When vars.feeLiquidated
is non-zero, it means that a portion of the user's collateral is used to cover the fee, which involves burning aTokens and transferring the underlying collateral to the fee collector. This transfer reduces the liquidity of the collateral reserve, regardless of whether the liquidator receives aTokens or underlying assets.However, in the
updateStateOnLiquidation
function of the LendingPoolCore
contract, the interest rates for the collateral reserve are only updated if the liquidator does not receive aTokens (i.e., when _liquidatorReceivesAToken
is false). This is because the code assumes that if the liquidator receives aTokens, no underlying collateral is withdrawn for the liquidator. But this assumption overlooks the fact that the fee payment always involves withdrawing underlying collateral for the fee collector, even when the liquidator receives aTokens.As a result, when
_liquidatorReceivesAToken
is true and _feeLiquidated
is non-zero, the interest rates for the collateral reserve are not updated to account for the liquidity loss due to the fee payment. This is a bug because the reduction in liquidity should trigger an interest rate update to maintain accurate reserve calculations./// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolLiquidationManager.sol --- uint256 constant LIQUIDATION_CLOSE_FACTOR_PERCENT = 50; /** * @dev users can invoke this function to liquidate an undercollateralized position. * @param _reserve the address of the collateral to liquidated * @param _reserve the address of the principal reserve * @param _user the address of the borrower * @param _purchaseAmount the amount of principal that the liquidator wants to repay * @param _receiveAToken true if the liquidators wants to receive the aTokens, false if * he wants to receive the underlying asset directly **/ function liquidationCall( address _collateral, address _reserve, address _user, uint256 _purchaseAmount, bool _receiveAToken ) external payable returns (uint256, string memory) { // Usage of a memory struct of vars to avoid "Stack too deep" errors due to local variables LiquidationCallLocalVars memory vars; /// Health Factor Check: (, , , , , , , vars.healthFactorBelowThreshold) = dataProvider.calculateUserGlobalData( _user ); if (!vars.healthFactorBelowThreshold) { return ( uint256(LiquidationErrors.HEALTH_FACTOR_ABOVE_THRESHOLD), "Health factor is not below the threshold" ); } /// Collateral Validation: vars.userCollateralBalance = core.getUserUnderlyingAssetBalance(_collateral, _user); //if _user hasn't deposited this specific collateral, nothing can be liquidated if (vars.userCollateralBalance == 0) { return ( uint256(LiquidationErrors.NO_COLLATERAL_AVAILABLE), "Invalid collateral to liquidate" ); } vars.isCollateralEnabled = core.isReserveUsageAsCollateralEnabled(_collateral) && core.isUserUseReserveAsCollateralEnabled(_collateral, _user); //if _collateral isn't enabled as collateral by _user, it cannot be liquidated if (!vars.isCollateralEnabled) { return ( uint256(LiquidationErrors.COLLATERAL_CANNOT_BE_LIQUIDATED), "The collateral chosen cannot be liquidated" ); } /// Borrow Validation: //if the user hasn't borrowed the specific currency defined by _reserve, it cannot be liquidated (, vars.userCompoundedBorrowBalance, vars.borrowBalanceIncrease) = core .getUserBorrowBalances(_reserve, _user); if (vars.userCompoundedBorrowBalance == 0) { return ( uint256(LiquidationErrors.CURRRENCY_NOT_BORROWED), "User did not borrow the specified currency" ); } /// Liquidation Amount Calculation: //all clear - calculate the max principal amount that can be liquidated vars.maxPrincipalAmountToLiquidate = vars .userCompoundedBorrowBalance .mul(LIQUIDATION_CLOSE_FACTOR_PERCENT) .div(100); vars.actualAmountToLiquidate = _purchaseAmount > vars.maxPrincipalAmountToLiquidate ? vars.maxPrincipalAmountToLiquidate : _purchaseAmount; /// Collateral Calculation: (uint256 maxCollateralToLiquidate, uint256 principalAmountNeeded) = calculateAvailableCollateralToLiquidate( _collateral, _reserve, vars.actualAmountToLiquidate, vars.userCollateralBalance ); /// Origination Fee Handling: vars.originationFee = core.getUserOriginationFee(_reserve, _user); //if there is a fee to liquidate, calculate the maximum amount of fee that can be liquidated if (vars.originationFee > 0) { ( vars.liquidatedCollateralForFee, vars.feeLiquidated ) = calculateAvailableCollateralToLiquidate( _collateral, _reserve, vars.originationFee, vars.userCollateralBalance.sub(maxCollateralToLiquidate) ); } //if principalAmountNeeded < vars.ActualAmountToLiquidate, there isn't enough //of _collateral to cover the actual amount that is being liquidated, hence we liquidate //a smaller amount if (principalAmountNeeded < vars.actualAmountToLiquidate) { vars.actualAmountToLiquidate = principalAmountNeeded; } /// Liquidity Check: //if liquidator reclaims the underlying asset, we make sure there is enough available collateral in the reserve if (!_receiveAToken) { uint256 currentAvailableCollateral = core.getReserveAvailableLiquidity(_collateral); if (currentAvailableCollateral < maxCollateralToLiquidate) { return ( uint256(LiquidationErrors.NOT_ENOUGH_LIQUIDITY), "There isn't enough liquidity available to liquidate" ); } } /// State Update: core.updateStateOnLiquidation( _reserve, _collateral, _user, vars.actualAmountToLiquidate, maxCollateralToLiquidate, vars.feeLiquidated, vars.liquidatedCollateralForFee, vars.borrowBalanceIncrease, _receiveAToken ); /// Asset Transfers: AToken collateralAtoken = AToken(core.getReserveATokenAddress(_collateral)); //if liquidator reclaims the aToken, he receives the equivalent atoken amount if (_receiveAToken) { collateralAtoken.transferOnLiquidation(_user, msg.sender, maxCollateralToLiquidate); } else { //otherwise receives the underlying asset //burn the equivalent amount of atoken collateralAtoken.burnOnLiquidation(_user, maxCollateralToLiquidate); core.transferToUser(_collateral, msg.sender, maxCollateralToLiquidate); } //transfers the principal currency to the pool core.transferToReserve.value(msg.value)(_reserve, msg.sender, vars.actualAmountToLiquidate); /// handle fee liquidation if (vars.feeLiquidated > 0) { //if there is enough collateral to liquidate the fee, first transfer burn an equivalent amount of //aTokens of the user collateralAtoken.burnOnLiquidation(_user, vars.liquidatedCollateralForFee); //then liquidate the fee by transferring it to the fee collection address core.liquidateFee( _collateral, vars.liquidatedCollateralForFee, addressesProvider.getTokenDistributor() ); emit OriginationFeeLiquidated( _collateral, _reserve, _user, vars.feeLiquidated, vars.liquidatedCollateralForFee, //solium-disable-next-line block.timestamp ); } emit LiquidationCall( _collateral, _reserve, _user, vars.actualAmountToLiquidate, maxCollateralToLiquidate, vars.borrowBalanceIncrease, msg.sender, _receiveAToken, //solium-disable-next-line block.timestamp ); return (uint256(LiquidationErrors.NO_ERROR), "No errors"); }
calculateAvailableCollateralToLiquidate
calculateAvailableCollateralToLiquidate
calculates how much collateral the liquidator can get based on principal liquidator repays and the liquidationBonus
./** * @dev calculates how much of a specific collateral can be liquidated, given * a certain amount of principal currency. This function needs to be called after * all the checks to validate the liquidation have been performed, otherwise it might fail. * @param _collateral the collateral to be liquidated * @param _principal the principal currency to be liquidated * @param _purchaseAmount the amount of principal being liquidated * @param _userCollateralBalance the collatera balance for the specific _collateral asset of the user being liquidated * @return the maximum amount that is possible to liquidated given all the liquidation constraints (user balance, close factor) and * the purchase amount **/ function calculateAvailableCollateralToLiquidate( address _collateral, address _principal, uint256 _purchaseAmount, uint256 _userCollateralBalance ) internal view returns (uint256 collateralAmount, uint256 principalAmountNeeded) { collateralAmount = 0; principalAmountNeeded = 0; IPriceOracleGetter oracle = IPriceOracleGetter(addressesProvider.getPriceOracle()); // Usage of a memory struct of vars to avoid "Stack too deep" errors due to local variables AvailableCollateralToLiquidateLocalVars memory vars; vars.collateralPrice = oracle.getAssetPrice(_collateral); vars.principalCurrencyPrice = oracle.getAssetPrice(_principal); vars.liquidationBonus = core.getReserveLiquidationBonus(_collateral); //this is the maximum possible amount of the selected collateral that can be liquidated, given the //max amount of principal currency that is available for liquidation. vars.maxAmountCollateralToLiquidate = vars .principalCurrencyPrice .mul(_purchaseAmount) .div(vars.collateralPrice) .mul(vars.liquidationBonus) .div(100); if (vars.maxAmountCollateralToLiquidate > _userCollateralBalance) { collateralAmount = _userCollateralBalance; principalAmountNeeded = vars .collateralPrice .mul(collateralAmount) .div(vars.principalCurrencyPrice) .mul(100) .div(vars.liquidationBonus); } else { collateralAmount = vars.maxAmountCollateralToLiquidate; principalAmountNeeded = _purchaseAmount; } return (collateralAmount, principalAmountNeeded); } /// --- v1/contract/aave-protocol/contracts/configuration/LendingPoolParametersProvider.sol --- uint256 private constant FLASHLOAN_FEE_TOTAL = 35; uint256 private constant FLASHLOAN_FEE_PROTOCOL = 3000; /** * @dev returns the fee applied to a flashloan and the portion to redirect to the protocol, in basis points. **/ function getFlashLoanFeesInBips() external pure returns (uint256, uint256) { return (FLASHLOAN_FEE_TOTAL, FLASHLOAN_FEE_PROTOCOL); }
flashLoan
The
flashLoan
function enables uncollateralized flash loans, allowing users to borrow assets temporarily within a single transaction, provided the borrowed amount plus a fee is returned by the end of the transaction.Steps:
- Access Control & Modifiers:
nonReentrant
: Prevents reentrancy attacks.onlyActiveReserve
: Ensures the reserve is active.onlyAmountGreaterThanZero
: Ensures the borrowed amount is non-zero.
- Liquidity Check:
- Verifies sufficient liquidity in the reserve before proceeding.
- Fee Calculation:
- Retrieves fee parameters from
parametersProvider
(total fee and protocol fee in basis points). - Calculates
amountFee
(total fee on the borrowed amount) andprotocolFee
(portion of the fee allocated to the protocol).
- Funds Transfer:
- Transfers the borrowed amount to the receiver contract via
core.transferToUser
.
- Execution of Flash Loan:
- Calls
executeOperation
on the receiver contract (must implementIFlashLoanReceiver
), passing the reserve address, amount, fee, and custom parameters.
- Balance Validation:
- After execution, checks that the core contract's balance increased by exactly
amountFee
, ensuring the receiver returned the principal plus fees.
- State Update:
- Invokes
core.updateStateOnFlashLoan
to update the reserve's state, distributing fees (protocol fee to the distributor, remainder to the LP).
- Event Emission
/// --- v1/contract/aave-protocol/contracts/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. NOTE 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 _receiver The address of the contract receiving the funds. The receiver should implement the IFlashLoanReceiver interface. * @param _reserve the address of the principal reserve * @param _amount the amount requested for this flashloan **/ function flashLoan(address _receiver, address _reserve, uint256 _amount, bytes memory _params) public /// Access Control & Modifiers: nonReentrant onlyActiveReserve(_reserve) onlyAmountGreaterThanZero(_amount) { /// Liquidity Check: //check that the reserve has enough available liquidity //we avoid using the getAvailableLiquidity() function in LendingPoolCore to save gas uint256 availableLiquidityBefore = _reserve == EthAddressLib.ethAddress() ? address(core).balance : IERC20(_reserve).balanceOf(address(core)); require( availableLiquidityBefore >= _amount, "There is not enough liquidity available to borrow" ); /// Fee Calculation: (uint256 totalFeeBips, uint256 protocolFeeBips) = parametersProvider .getFlashLoanFeesInBips(); //calculate amount fee uint256 amountFee = _amount.mul(totalFeeBips).div(10000); //protocol fee is the part of the amountFee reserved for the protocol - the rest goes to depositors uint256 protocolFee = amountFee.mul(protocolFeeBips).div(10000); require( amountFee > 0 && protocolFee > 0, "The requested amount is too small for a flashLoan." ); /// Funds Transfer: //get the FlashLoanReceiver instance IFlashLoanReceiver receiver = IFlashLoanReceiver(_receiver); address payable userPayable = address(uint160(_receiver)); //transfer funds to the receiver core.transferToUser(_reserve, userPayable, _amount); /// Execution of Flash Loan: receiver.executeOperation(_reserve, _amount, amountFee, _params); /// Balance Validation: //check that the actual balance of the core contract includes the returned amount uint256 availableLiquidityAfter = _reserve == EthAddressLib.ethAddress() ? address(core).balance : IERC20(_reserve).balanceOf(address(core)); require( availableLiquidityAfter == availableLiquidityBefore.add(amountFee), "The actual balance of the protocol is inconsistent" ); /// Reserve State Update: core.updateStateOnFlashLoan( _reserve, availableLiquidityBefore, amountFee.sub(protocolFee), protocolFee ); //solium-disable-next-line emit FlashLoan(_receiver, _reserve, _amount, amountFee, protocolFee, block.timestamp); }
AToken
balanceOf
This function calculates the balance of the user, which includes three parts:
- the principal balance
- interest generated by the principal balance
- interest generated by the redirected balance.
Process:
- Get user’s principal balance (which is aToken amount)
- Get directed balance received by this user from other users.
- Redirected balance is used to calculate intereset accrued accordingly
- If both principal balance and redirected balance is 0, then the balance of user is 0.
- Calculate total balance
- If user has redirected balance to other address, then user balance excludes interest generated by the principal balance
- Else, user gets interest generated by principal balance.
There are two cases:
/// --- v1/contract/aave-protocol/contracts/tokenization/AToken.sol --- // record amount of redirected balance of address. // The redirected balance is the balance // redirected by other accounts to the user, that is accrueing interest for him. mapping (address => uint256) private redirectedBalances; /// Record which address redirects its interest to which address mapping (address => address) private interestRedirectionAddresses; /** * @dev calculates the balance of the user, which is the * principal balance + interest generated by the principal balance + interest generated by the redirected balance * @param _user the user for which the balance is being calculated * @return the total balance of the user **/ function balanceOf(address _user) public view returns(uint256) { //current principal balance of the user uint256 currentPrincipalBalance = super.balanceOf(_user); //balance redirected by other users to _user for interest rate accrual uint256 redirectedBalance = redirectedBalances[_user]; if(currentPrincipalBalance == 0 && redirectedBalance == 0){ return 0; } //if the _user is not redirecting the interest to anybody, accrues //the interest for himself if(interestRedirectionAddresses[_user] == address(0)){ //accruing for himself means that both the principal balance and //the redirected balance partecipate in the interest return calculateCumulatedBalanceInternal( _user, currentPrincipalBalance.add(redirectedBalance) ) .sub(redirectedBalance); } else { //if the user redirected the interest, then only the redirected //balance generates interest. In that case, the interest generated //by the redirected balance is added to the current principal balance. return currentPrincipalBalance.add( calculateCumulatedBalanceInternal( _user, redirectedBalance ) .sub(redirectedBalance) ); } }
Redeem
AToken
redeem
function allows a user to redeem their aTokens for the underlying asset they deposited into the lending pool.Since aTokens accrues interest over time, the function must:
- Bring the user’s aToken balance up-to-date (with accrued interest).
- Adjust any redirected interest flows.
- Burn the redeemed aTokens.
- Transfer the equivalent underlying asset from the pool back to the user.
Steps:
- Input validation
requires the
_amount
to be greater than zero.- Calcualte accumulated interest of user
cumulateBalanceInternal
computes accrued interest since the last user checkpoint, mints those accrued aTokens into the user’s principal, and updates the user’s userIndex
to the current reserveIndex
. The function returns the user’s up-to-date balance (currentBalance
), the balanceIncrease
minted in this step, and the new index — ensuring the redemption uses the latest, interest-inclusive balance.- Compute redeem amount (full-balance sentinel)
The code supports a “redeem everything” sentinel (
uint(-1)
). If the caller passed that sentinel, amountToRedeem
is set to the freshly computed currentBalance
; otherwise it remains the supplied _amount
. This convenience avoids an extra balance fetch for clients wanting to withdraw everything.- Sanity check against the available balance
A safety check ensures
amountToRedeem <= currentBalance
. - Protocol risk/collateral check
Before proceeding, the contract calls
isTransferAllowed(msg.sender, amountToRedeem)
(which delegates to the lending pool data provider). That enforces protocol rules (e.g., you cannot redeem collateral if doing so would make your loans under-collateralized). This protects the lending pool and borrowers from undercollateralization caused by redemptions.- Update any interest redirection bookkeeping
If the redeemer had previously redirected their interest to another address, the function calls
updateRedirectedBalanceOfRedirectionAddressInternal(msg.sender, balanceIncrease, amountToRedeem)
. That call adds the accrued interest to the recipient’s redirected balance and subtracts the redeemed principal so recipients’ redirected balances remain consistent. This step is purely bookkeeping: it moves accounting units so future accruals on recipients are correct.- Burn the aTokens
It reduces the user’s stored principal (the on-chain ERC20 state) to reflect the withdrawal.
- Reset user index / redirection when balance becomes zero
If the user’s remaining balance after the burn is zero, the function calls
resetDataOnZeroBalanceInternal(msg.sender)
. This clears the user’s redirection address and, if no redirected balances remain, resets userIndexes[msg.sender]
to 0. This saves storage and prevents stale indexes from causing later accounting confusion.- Withdraw underlying from the pool
The actual transfer of underlying tokens out of the protocol is performed by calling
pool.redeemUnderlying
. The aToken contract delegates the real token movement to the LendingPool, because aTokens are just claim tokens — the pool holds the underlying liquidity and must execute the transfer./// --- v1/contract/aave-protocol/contracts/tokenization/AToken.sol --- /** * @dev redeems aToken for the underlying asset * @param _amount the amount being redeemed **/ function redeem(uint256 _amount) external { /// Input validation require(_amount > 0, "Amount to redeem needs to be > 0"); /// Calcualte accumulated interest of user (, uint256 currentBalance, uint256 balanceIncrease, uint256 index) = cumulateBalanceInternal(msg.sender); /// Compute redeem amount (full-balance sentinel) uint256 amountToRedeem = _amount; //if amount is equal to uint(-1), the user wants to redeem everything if(_amount == UINT_MAX_VALUE){ amountToRedeem = currentBalance; } /// Sanity check against the available balance require(amountToRedeem <= currentBalance, "User cannot redeem more than the available balance"); //check that the user is allowed to redeem the amount require(isTransferAllowed(msg.sender, amountToRedeem), "Transfer cannot be allowed."); //if the user is redirecting his interest towards someone else, //we update the redirected balance of the redirection address by adding the accrued interest, //and removing the amount to redeem updateRedirectedBalanceOfRedirectionAddressInternal(msg.sender, balanceIncrease, amountToRedeem); // burns tokens equivalent to the amount requested _burn(msg.sender, amountToRedeem); bool userIndexReset = false; //reset the user data if the remaining balance is 0 if(currentBalance.sub(amountToRedeem) == 0){ userIndexReset = resetDataOnZeroBalanceInternal(msg.sender); } // executes redeem of the underlying asset pool.redeemUnderlying( underlyingAssetAddress, msg.sender, amountToRedeem, currentBalance.sub(amountToRedeem) ); emit Redeem(msg.sender, amountToRedeem, balanceIncrease, userIndexReset ? 0 : index); } /** * @dev function to reset the interest stream redirection and the user index, if the * user has no balance left. * @param _user the address of the user * @return true if the user index has also been reset, false otherwise. useful to emit the proper user index value **/ function resetDataOnZeroBalanceInternal(address _user) internal returns(bool) { //if the user has 0 principal balance, the interest stream redirection gets reset interestRedirectionAddresses[_user] = address(0); //emits a InterestStreamRedirected event to notify that the redirection has been reset emit InterestStreamRedirected(_user, address(0),0,0,0); //if the redirected balance is also 0, we clear up the user index if(redirectedBalances[_user] == 0){ userIndexes[_user] = 0; return true; } else{ return false; } }
mintOnDeposit
mintOnDeposit
function mints interest-bearing tokens when a user deposits underlying assets into the lending pool. Only lending pool contract can call this, as lending pool contract executes logic necessary to ensure protocol data consistency.Steps:
- Cumulate the User’s Balance
Calls
cumulateBalanceInternal
to update the user’s principal balance to include any accrued interest since the last interaction.- Update Redirected Balances
_account
may have set a redirection of interest to another user.- The function updates the redirected balance of the target address:
- Adds both the accrued interest (
balanceIncrease
) and the new deposit amount (_amount
) to the redirected balance. - Removes
0
(nothing removed in this case). - If the user is redirecting their interest to someone else, the interest accrued and the new deposit should now start generating interest for the recipient.
updateRedirectedBalanceOfRedirectionAddressInternal
seems have issue:- If Alice has delegated balance to Bob, and Bob delegates to Charlie.
- The last time Charlie’s interest index is updated on T0
- Alice deposits on T1
updateRedirectedBalanceOfRedirectionAddressInternal
update Bob’s principal amount and in ture update Charlie’s.
- But the redirected balance update happens before interest update of Charlie. This means when Charlie deposits on T2, the increased balance on T1 generates interest from T0 to T2 which is wrong.
/// --- v1/contract/aave-protocol/contracts/tokenization/AToken.sol --- mapping(address => address) private interestRedirectionAddresses; mapping(address => uint256) private redirectedBalances; /** * @dev mints token in the event of users depositing the underlying asset into the lending pool * only lending pools can call this function * @param _account the address receiving the minted tokens * @param _amount the amount of tokens to mint */ function mintOnDeposit(address _account, uint256 _amount) external onlyLendingPool { //cumulates the balance of the user (, , uint256 balanceIncrease, uint256 index) = cumulateBalanceInternal(_account); //if the user is redirecting his interest towards someone else, //we update the redirected balance of the redirection address by adding the accrued interest //and the amount deposited updateRedirectedBalanceOfRedirectionAddressInternal(_account, balanceIncrease.add(_amount), 0); //mint an equivalent amount of tokens to cover the new deposit _mint(_account, _amount); emit MintOnDeposit(_account, _amount, balanceIncrease, index); } /** * @dev accumulates the accrued interest of the user to the principal balance * @param _user the address of the user for which the interest is being accumulated * @return the previous principal balance, the new principal balance, the balance increase * and the new user index **/ function cumulateBalanceInternal(address _user) internal returns(uint256, uint256, uint256, uint256) { uint256 previousPrincipalBalance = super.balanceOf(_user); //calculate the accrued interest since the last accumulation uint256 balanceIncrease = balanceOf(_user).sub(previousPrincipalBalance); //mints an amount of tokens equivalent to the amount accumulated _mint(_user, balanceIncrease); //updates the user index uint256 index = userIndexes[_user] = core.getReserveNormalizedIncome(underlyingAssetAddress); return ( previousPrincipalBalance, previousPrincipalBalance.add(balanceIncrease), balanceIncrease, index ); } /** * @dev updates the redirected balance of the user. If the user is not redirecting his * interest, nothing is executed. * @param _user the address of the user for which the interest is being accumulated * @param _balanceToAdd the amount to add to the redirected balance * @param _balanceToRemove the amount to remove from the redirected balance **/ function updateRedirectedBalanceOfRedirectionAddressInternal( address _user, uint256 _balanceToAdd, uint256 _balanceToRemove ) internal { address redirectionAddress = interestRedirectionAddresses[_user]; //if there isn't any redirection, nothing to be done if(redirectionAddress == address(0)){ return; } //compound balances of the redirected address (,,uint256 balanceIncrease, uint256 index) = cumulateBalanceInternal(redirectionAddress); //updating the redirected balance redirectedBalances[redirectionAddress] = redirectedBalances[redirectionAddress] .add(_balanceToAdd) .sub(_balanceToRemove); //if the interest of redirectionAddress is also being redirected, we need to update //the redirected balance of the redirection target by adding the balance increase address targetOfRedirectionAddress = interestRedirectionAddresses[redirectionAddress]; if(targetOfRedirectionAddress != address(0)){ redirectedBalances[targetOfRedirectionAddress] = redirectedBalances[targetOfRedirectionAddress].add(balanceIncrease); } emit RedirectedBalanceUpdated( redirectionAddress, balanceIncrease, index, _balanceToAdd, _balanceToRemove ); }
transferOnLiquidation
/// --- v1/contract/aave-protocol/contracts/tokenization/AToken.sol --- /** * @dev transfers tokens in the event of a borrow being liquidated, in case the liquidators reclaims the aToken * only lending pools can call this function * @param _from the address from which transfer the aTokens * @param _to the destination address * @param _value the amount to transfer **/ function transferOnLiquidation(address _from, address _to, uint256 _value) external onlyLendingPool { //being a normal transfer, the Transfer() and BalanceTransfer() are emitted //so no need to emit a specific event here executeTransferInternal(_from, _to, _value); }
executeTransferInternal
executeTransferInternal
executes logic of transfer. It handles:- balance cumulation of both
_from
and_to
address
- update redirected account’s related state
- transfer aToken from
_from
to_to
- reset
_from
account state if its balance is 0 after transfer.
/** * @dev executes the transfer of aTokens, invoked by both _transfer() and * transferOnLiquidation() * @param _from the address from which transfer the aTokens * @param _to the destination address * @param _value the amount to transfer **/ function executeTransferInternal( address _from, address _to, uint256 _value ) internal { require(_value > 0, "Transferred amount needs to be greater than zero"); //cumulate the balance of the sender (, uint256 fromBalance, uint256 fromBalanceIncrease, uint256 fromIndex ) = cumulateBalanceInternal(_from); //cumulate the balance of the receiver (, , uint256 toBalanceIncrease, uint256 toIndex ) = cumulateBalanceInternal(_to); //if the sender is redirecting his interest towards someone else, //adds to the redirected balance the accrued interest and removes the amount //being transferred updateRedirectedBalanceOfRedirectionAddressInternal(_from, fromBalanceIncrease, _value); //if the receiver is redirecting his interest towards someone else, //adds to the redirected balance the accrued interest and the amount //being transferred updateRedirectedBalanceOfRedirectionAddressInternal(_to, toBalanceIncrease.add(_value), 0); //performs the transfer super._transfer(_from, _to, _value); bool fromIndexReset = false; //reset the user data if the remaining balance is 0 if(fromBalance.sub(_value) == 0){ fromIndexReset = resetDataOnZeroBalanceInternal(_from); } emit BalanceTransfer( _from, _to, _value, fromBalanceIncrease, toBalanceIncrease, fromIndexReset ? 0 : fromIndex, toIndex ); }
burnOnLiquidation
burnOnLiquidation
burns token in the event of a borrow being liquidated.Steps:
- cumulate account balance
- update redirected account’s related state
- burn mToken of the account
- reset account state if its balance is 0 after transfer.
/// --- v1/contract/aave-protocol/contracts/tokenization/AToken.sol --- /** * @dev burns token in the event of a borrow being liquidated, in case the liquidators reclaims the underlying asset * Transfer of the liquidated asset is executed by the lending pool contract. * only lending pools can call this function * @param _account the address from which burn the aTokens * @param _value the amount to burn **/ function burnOnLiquidation(address _account, uint256 _value) external onlyLendingPool { //cumulates the balance of the user being liquidated (,uint256 accountBalance,uint256 balanceIncrease,uint256 index) = cumulateBalanceInternal(_account); //adds the accrued interest and substracts the burned amount to //the redirected balance updateRedirectedBalanceOfRedirectionAddressInternal(_account, balanceIncrease, _value); //burns the requested amount of tokens _burn(_account, _value); bool userIndexReset = false; //reset the user data if the remaining balance is 0 if(accountBalance.sub(_value) == 0){ userIndexReset = resetDataOnZeroBalanceInternal(_account); } emit BurnOnLiquidation(_account, _value, balanceIncrease, userIndexReset ? 0 : index); }
LendingPoolCore
userIndexes
userIndexes
records indexed rate of user which is already applied to user status.By combining current normalized income of asset and user's indexed rate, we can calcualte newly accumualted interest.
For example, each time user deposits, contract needs to udpate user’s balance with accrued interest based on current normalized income of asset, and records the applied rate which we call indexed rate. Next time user deposits more asset, we can correctly calcualte newly generated interest based on indexed rate and current rate.
/// --- v1/contract/aave-protocol/contracts/tokenization/AToken.sol --- mapping (address => uint256) private userIndexes;
calculateCumulatedBalanceInternal
calculateCumulatedBalanceInternal
calculates balance plus interest generated /// --- v1/contract/aave-protocol/contracts/tokenization/AToken.sol --- mapping (address => uint256) private userIndexes; /** * @dev calculate the interest accrued by _user on a specific balance * @param _user the address of the user for which the interest is being accumulated * @param _balance the balance on which the interest is calculated * @return the interest rate accrued **/ function calculateCumulatedBalanceInternal( address _user, uint256 _balance ) internal view returns (uint256) { return _balance .wadToRay() .rayMul(core.getReserveNormalizedIncome(underlyingAssetAddress)) .rayDiv(userIndexes[_user]) .rayToWad(); }
getReserveNormalizedIncome
getReserveNormalizedIncome
gets current normalized income of corresponding reserve
asset.It calculates by:
- get last timestamp when normalized income was updated (
reserve.lastUpdateTimestamp
)
- get last normalized income (
reserve.lastLiquidityCumulativeIndex
)
- get current liquidity rate (
reserve.currentLiquidityRate
)
- calcualte current normalized income linearly:
currentNormalizedIncome = lastLiquidityCumulativeIndex * (1 + _T/T_{year} * currentLiquidityRate)
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev gets the normalized income of the reserve. a value of 1e27 means there is no income. A value of 2e27 means there * there has been 100% income. * @param _reserve the reserve address * @return the reserve normalized income **/ function getReserveNormalizedIncome(address _reserve) external view returns (uint256) { CoreLibrary.ReserveData storage reserve = reserves[_reserve]; return reserve.getNormalizedIncome(); } /// --- v1/contract/aave-protocol/contracts/libraries/CoreLibrary.sol --- uint256 internal constant SECONDS_PER_YEAR = 365 days; /** * @dev returns the ongoing normalized income for the reserve. * a value of 1e27 means there is no income. As time passes, the income is accrued. * A value of 2*1e27 means that the income of the reserve is double the initial amount. * @param _reserve the reserve object * @return the normalized income. expressed in ray **/ function getNormalizedIncome(CoreLibrary.ReserveData storage _reserve) internal view returns (uint256) { uint256 cumulated = calculateLinearInterest( _reserve .currentLiquidityRate, _reserve .lastUpdateTimestamp ) .rayMul(_reserve.lastLiquidityCumulativeIndex); return cumulated; } /** * @dev function to calculate the interest using a linear interest rate formula * @param _rate the interest rate, in ray * @param _lastUpdateTimestamp the timestamp of the last update of the interest * @return the interest rate linearly accumulated during the timeDelta, in ray **/ function calculateLinearInterest(uint256 _rate, uint40 _lastUpdateTimestamp) internal view returns (uint256) { //solium-disable-next-line uint256 timeDifference = block.timestamp.sub(uint256(_lastUpdateTimestamp)); uint256 timeDelta = timeDifference.wadToRay().rayDiv(SECONDS_PER_YEAR.wadToRay()); return _rate.rayMul(timeDelta).add(WadRayMath.ray()); }
updateStateOnDeposit
updateStateOnDeposit
updates the reserve state in the core contract as a result of a deposit action.Process:
- Update reserve’s indexed rates
Update
lastLiquidityCumulativeIndex
and lastVariableBorrowCumulativeIndex
using function updateCumulativeIndexes
based on current rate setting (refer)- Update reserve’s interest rates based on updated reserve status and oracle
- stable rate
- variable rate
- liquidity rate
Including:
- If it’s the first time user deposits, it also sets that user’s deposit can be used as collateral in borrow.
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev updates the state of the core as a result of a deposit action * @param _reserve the address of the reserve in which the deposit is happening * @param _user the address of the the user depositing * @param _amount the amount being deposited * @param _isFirstDeposit true if the user is depositing for the first time **/ function updateStateOnDeposit( address _reserve, address _user, uint256 _amount, bool _isFirstDeposit ) external onlyLendingPool { /// --- Update reserve’s indexed rates --- reserves[_reserve].updateCumulativeIndexes(); /// --- Update reserve’s interest rates based on updated reserve status and oracle --- updateReserveInterestRatesAndTimestampInternal(_reserve, _amount, 0); if (_isFirstDeposit) { //if this is the first deposit of the user, we configure the deposit as enabled to be used as collateral setUserUseReserveAsCollateral(_reserve, _user, true); } }
updateStateOnRedeem
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev updates the state of the core as a result of a redeem action * @param _reserve the address of the reserve in which the redeem is happening * @param _user the address of the the user redeeming * @param _amountRedeemed the amount being redeemed * @param _userRedeemedEverything true if the user is redeeming everything **/ function updateStateOnRedeem( address _reserve, address _user, uint256 _amountRedeemed, bool _userRedeemedEverything ) external onlyLendingPool { //compound liquidity and variable borrow interests reserves[_reserve].updateCumulativeIndexes(); /// update liquidity and borrow rate based on updated reserve status updateReserveInterestRatesAndTimestampInternal(_reserve, 0, _amountRedeemed); //if user redeemed everything the useReserveAsCollateral flag is reset if (_userRedeemedEverything) { setUserUseReserveAsCollateral(_reserve, _user, false); } }
setUserUseReserveAsCollateral
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- mapping(address => mapping(address => CoreLibrary.UserReserveData)) internal usersReserveData; /** * @dev enables or disables a reserve as collateral * @param _reserve the address of the principal reserve where the user deposited * @param _user the address of the depositor * @param _useAsCollateral true if the depositor wants to use the reserve as collateral **/ function setUserUseReserveAsCollateral(address _reserve, address _user, bool _useAsCollateral) public onlyLendingPool { CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve]; user.useAsCollateral = _useAsCollateral; } /// --- v1/contract/aave-protocol/contracts/libraries/CoreLibrary.sol --- struct UserReserveData { //principal amount borrowed by the user. uint256 principalBorrowBalance; //cumulated variable borrow index for the user. Expressed in ray uint256 lastVariableBorrowCumulativeIndex; //origination fee cumulated by the user uint256 originationFee; // stable borrow rate at which the user has borrowed. Expressed in ray uint256 stableBorrowRate; uint40 lastUpdateTimestamp; //defines if a specific deposit should or not be used as a collateral in borrows bool useAsCollateral; }
getUserBasicReserveData
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev returns the basic data (balances, fee accrued, reserve enabled/disabled as collateral) * needed to calculate the global account data in the LendingPoolDataProvider * @param _reserve the address of the reserve * @param _user the address of the user * @return the user deposited balance, the principal borrow balance, the fee, and if the reserve is enabled as collateral or not **/ function getUserBasicReserveData(address _reserve, address _user) external view returns (uint256, uint256, uint256, bool) { CoreLibrary.ReserveData storage reserve = reserves[_reserve]; CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve]; uint256 underlyingBalance = getUserUnderlyingAssetBalance(_reserve, _user); if (user.principalBorrowBalance == 0) { return (underlyingBalance, 0, 0, user.useAsCollateral); } return ( underlyingBalance, user.getCompoundedBorrowBalance(reserve), user.originationFee, user.useAsCollateral ); } /** * @dev gets the underlying asset balance of a user based on the corresponding aToken balance. * @param _reserve the reserve address * @param _user the user address * @return the underlying deposit balance of the user **/ function getUserUnderlyingAssetBalance(address _reserve, address _user) public view returns (uint256) { AToken aToken = AToken(reserves[_reserve].aTokenAddress); return aToken.balanceOf(_user); } /// --- v1/contract/aave-protocol/contracts/libraries/CoreLibrary.sol --- struct UserReserveData { //principal amount borrowed by the user. uint256 principalBorrowBalance; //cumulated variable borrow index for the user. Expressed in ray uint256 lastVariableBorrowCumulativeIndex; //origination fee cumulated by the user uint256 originationFee; // stable borrow rate at which the user has borrowed. Expressed in ray uint256 stableBorrowRate; uint40 lastUpdateTimestamp; //defines if a specific deposit should or not be used as a collateral in borrows bool useAsCollateral; }
updateStateOnBorrow
Steps:
- get user’s borrow status
- update reserve’s status based on new borrow amount
- for stable borrow
- for variable borrow
Update average borrow rate, and total borrowed principal.
Update total borrowed principal.
- update user borrow status
- update reserve interest rate
- stable borrow rate
- variable borrow rate
- liquidity borrow rate
Note this update should happend after cumulative interest rate checkpoint update(
updateReserveStateOnBorrowInternal
)/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev updates the state of the core as a consequence of a borrow action. * @param _reserve the address of the reserve on which the user is borrowing * @param _user the address of the borrower * @param _amountBorrowed the new amount borrowed * @param _borrowFee the fee on the amount borrowed * @param _rateMode the borrow rate mode (stable, variable) * @return the new borrow rate for the user **/ function updateStateOnBorrow( address _reserve, address _user, uint256 _amountBorrowed, uint256 _borrowFee, CoreLibrary.InterestRateMode _rateMode ) external onlyLendingPool returns (uint256, uint256) { // getting the previous borrow data of the user (uint256 principalBorrowBalance, , uint256 balanceIncrease) = getUserBorrowBalances( _reserve, _user ); /// update reserve’s status based on new borrow amount updateReserveStateOnBorrowInternal( _reserve, _user, principalBorrowBalance, balanceIncrease, _amountBorrowed, _rateMode ); /// update user borrow status updateUserStateOnBorrowInternal( _reserve, _user, _amountBorrowed, balanceIncrease, _borrowFee, _rateMode ); /// update reserve interest rate updateReserveInterestRatesAndTimestampInternal(_reserve, 0, _amountBorrowed); return (getUserCurrentBorrowRate(_reserve, _user), balanceIncrease); }
getUserBorrowBalances
getUserBorrowBalances
calculates user’s previous principal amount, and principal plus interest accumulated, also the interest amount.Calculation of accumulated interest of variable interest borrow:
/** * @dev calculates and returns the borrow balances of the user * @param _reserve the address of the reserve * @param _user the address of the user * @return the principal borrow balance, the compounded balance and the balance increase since the last borrow/repay/swap/rebalance **/ function getUserBorrowBalances(address _reserve, address _user) public view returns (uint256, uint256, uint256) { CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve]; if (user.principalBorrowBalance == 0) { return (0, 0, 0); } uint256 principal = user.principalBorrowBalance; uint256 compoundedBalance = CoreLibrary.getCompoundedBorrowBalance( user, reserves[_reserve] ); return (principal, compoundedBalance, compoundedBalance.sub(principal)); } /** * @dev calculates the compounded borrow balance of a user * @param _self the userReserve object * @param _reserve the reserve object * @return the user compounded borrow balance **/ function getCompoundedBorrowBalance( CoreLibrary.UserReserveData storage _self, CoreLibrary.ReserveData storage _reserve ) internal view returns (uint256) { if (_self.principalBorrowBalance == 0) return 0; uint256 principalBorrowBalanceRay = _self.principalBorrowBalance.wadToRay(); uint256 compoundedBalance = 0; uint256 cumulatedInterest = 0; if (_self.stableBorrowRate > 0) { cumulatedInterest = calculateCompoundedInterest( _self.stableBorrowRate, _self.lastUpdateTimestamp ); } else { //variable interest cumulatedInterest = calculateCompoundedInterest( _reserve .currentVariableBorrowRate, _reserve .lastUpdateTimestamp ) .rayMul(_reserve.lastVariableBorrowCumulativeIndex) .rayDiv(_self.lastVariableBorrowCumulativeIndex); } compoundedBalance = principalBorrowBalanceRay.rayMul(cumulatedInterest).rayToWad(); if (compoundedBalance == _self.principalBorrowBalance) { //solium-disable-next-line if (_self.lastUpdateTimestamp != block.timestamp) { //no interest cumulation because of the rounding - we add 1 wei //as symbolic cumulated interest to avoid interest free loans. return _self.principalBorrowBalance.add(1 wei); } } return compoundedBalance; }
updateReserveStateOnBorrowInternal
updateReserveStateOnBorrowInternal
updates cumulative rate index of _reserve
. And it updates reserve total borrow information using function updateReserveTotalBorrowsByRateModeInternal
./// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev updates the state of a reserve as a consequence of a borrow action. * @param _reserve the address of the reserve on which the user is borrowing * @param _user the address of the borrower * @param _principalBorrowBalance the previous borrow balance of the borrower before the action * @param _balanceIncrease the accrued interest of the user on the previous borrowed amount * @param _amountBorrowed the new amount borrowed * @param _rateMode the borrow rate mode (stable, variable) **/ function updateReserveStateOnBorrowInternal( address _reserve, address _user, uint256 _principalBorrowBalance, uint256 _balanceIncrease, uint256 _amountBorrowed, CoreLibrary.InterestRateMode _rateMode ) internal { reserves[_reserve].updateCumulativeIndexes(); //increasing reserve total borrows to account for the new borrow balance of the user //NOTE: Depending on the previous borrow mode, the borrows might need to be switched from variable to stable or vice versa updateReserveTotalBorrowsByRateModeInternal( _reserve, _user, _principalBorrowBalance, _balanceIncrease, _amountBorrowed, _rateMode ); }
updateReserveTotalBorrowsByRateModeInternal
Steps:
- Determine rate mode of user’s borrow.
- Update reserve status
- first, remove the impact of the current loan in the reserve.
- for stable borrow, it needs to decrease the principal amoumt, and also updates average stable borrow rate. Because average stable borrow rate is necessary to calculate liquidity rate.
- for variable borrow, just decrease the principal amoumt.
- second, update reserve information based on new principal (previous principal + interest accumulated + new borrow)
Each user can only borrow under one rate mode at a time.
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev updates the state of the user as a consequence of a stable rate rebalance * @param _reserve the address of the principal reserve where the user borrowed * @param _user the address of the borrower * @param _balanceIncrease the accrued interest on the borrowed amount * @param _amountBorrowed the accrued interest on the borrowed amount **/ function updateReserveTotalBorrowsByRateModeInternal( address _reserve, address _user, uint256 _principalBalance, uint256 _balanceIncrease, uint256 _amountBorrowed, CoreLibrary.InterestRateMode _newBorrowRateMode ) internal { CoreLibrary.InterestRateMode previousRateMode = getUserCurrentBorrowRateMode( _reserve, _user ); CoreLibrary.ReserveData storage reserve = reserves[_reserve]; if (previousRateMode == CoreLibrary.InterestRateMode.STABLE) { CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve]; reserve.decreaseTotalBorrowsStableAndUpdateAverageRate( _principalBalance, user.stableBorrowRate ); } else if (previousRateMode == CoreLibrary.InterestRateMode.VARIABLE) { reserve.decreaseTotalBorrowsVariable(_principalBalance); } uint256 newPrincipalAmount = _principalBalance.add(_balanceIncrease).add(_amountBorrowed); if (_newBorrowRateMode == CoreLibrary.InterestRateMode.STABLE) { reserve.increaseTotalBorrowsStableAndUpdateAverageRate( newPrincipalAmount, reserve.currentStableBorrowRate ); } else if (_newBorrowRateMode == CoreLibrary.InterestRateMode.VARIABLE) { reserve.increaseTotalBorrowsVariable(newPrincipalAmount); } else { revert("Invalid new borrow rate mode"); } /** * @dev users with no loans in progress have NONE as borrow rate mode * @param _reserve the address of the reserve for which the information is needed * @param _user the address of the user for which the information is needed * @return the borrow rate mode for the user, **/ function getUserCurrentBorrowRateMode(address _reserve, address _user) public view returns (CoreLibrary.InterestRateMode) { CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve]; if (user.principalBorrowBalance == 0) { return CoreLibrary.InterestRateMode.NONE; } return user.stableBorrowRate > 0 ? CoreLibrary.InterestRateMode.STABLE : CoreLibrary.InterestRateMode.VARIABLE; } /// --- v1/contract/aave-protocol/contracts/libraries/CoreLibrary.sol --- /** * @dev decreases the total borrows at a stable rate on a specific reserve and updates the * average stable rate consequently * @param _reserve the reserve object * @param _amount the amount to substract to the total borrows stable * @param _rate the rate at which the amount has been repaid **/ function decreaseTotalBorrowsStableAndUpdateAverageRate( ReserveData storage _reserve, uint256 _amount, uint256 _rate ) internal { require(_reserve.totalBorrowsStable >= _amount, "Invalid amount to decrease"); uint256 previousTotalBorrowStable = _reserve.totalBorrowsStable; //updating reserve borrows stable _reserve.totalBorrowsStable = _reserve.totalBorrowsStable.sub(_amount); if (_reserve.totalBorrowsStable == 0) { _reserve.currentAverageStableBorrowRate = 0; //no income if there are no stable rate borrows return; } //update the average stable rate //weighted average of all the borrows uint256 weightedLastBorrow = _amount.wadToRay().rayMul(_rate); uint256 weightedPreviousTotalBorrows = previousTotalBorrowStable.wadToRay().rayMul( _reserve.currentAverageStableBorrowRate ); require( weightedPreviousTotalBorrows >= weightedLastBorrow, "The amounts to subtract don't match" ); _reserve.currentAverageStableBorrowRate = weightedPreviousTotalBorrows .sub(weightedLastBorrow) .rayDiv(_reserve.totalBorrowsStable.wadToRay()); } /** * @dev decreases the total borrows at a variable rate * @param _reserve the reserve object * @param _amount the amount to substract to the total borrows variable **/ function decreaseTotalBorrowsVariable(ReserveData storage _reserve, uint256 _amount) internal { require( _reserve.totalBorrowsVariable >= _amount, "The amount that is being subtracted from the variable total borrows is incorrect" ); _reserve.totalBorrowsVariable = _reserve.totalBorrowsVariable.sub(_amount); } /** * @dev increases the total borrows at a stable rate on a specific reserve and updates the * average stable rate consequently * @param _reserve the reserve object * @param _amount the amount to add to the total borrows stable * @param _rate the rate at which the amount has been borrowed **/ function increaseTotalBorrowsStableAndUpdateAverageRate( ReserveData storage _reserve, uint256 _amount, uint256 _rate ) internal { uint256 previousTotalBorrowStable = _reserve.totalBorrowsStable; //updating reserve borrows stable _reserve.totalBorrowsStable = _reserve.totalBorrowsStable.add(_amount); //update the average stable rate //weighted average of all the borrows uint256 weightedLastBorrow = _amount.wadToRay().rayMul(_rate); uint256 weightedPreviousTotalBorrows = previousTotalBorrowStable.wadToRay().rayMul( _reserve.currentAverageStableBorrowRate ); _reserve.currentAverageStableBorrowRate = weightedLastBorrow .add(weightedPreviousTotalBorrows) .rayDiv(_reserve.totalBorrowsStable.wadToRay()); } /** * @dev increases the total borrows at a variable rate * @param _reserve the reserve object * @param _amount the amount to add to the total borrows variable **/ function increaseTotalBorrowsVariable(ReserveData storage _reserve, uint256 _amount) internal { _reserve.totalBorrowsVariable = _reserve.totalBorrowsVariable.add(_amount); }
updateUserStateOnBorrowInternal
updateUserStateOnBorrowInternal
updates the state of a user as a consequence of a borrow action./// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev updates the state of a user as a consequence of a borrow action. * @param _reserve the address of the reserve on which the user is borrowing * @param _user the address of the borrower * @param _amountBorrowed the amount borrowed * @param _balanceIncrease the accrued interest of the user on the previous borrowed amount * @param _rateMode the borrow rate mode (stable, variable) * @return the final borrow rate for the user. Emitted by the borrow() event **/ function updateUserStateOnBorrowInternal( address _reserve, address _user, uint256 _amountBorrowed, uint256 _balanceIncrease, uint256 _fee, CoreLibrary.InterestRateMode _rateMode ) internal { CoreLibrary.ReserveData storage reserve = reserves[_reserve]; CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve]; if (_rateMode == CoreLibrary.InterestRateMode.STABLE) { //stable //reset the user variable index, and update the stable rate user.stableBorrowRate = reserve.currentStableBorrowRate; user.lastVariableBorrowCumulativeIndex = 0; } else if (_rateMode == CoreLibrary.InterestRateMode.VARIABLE) { //variable //reset the user stable rate, and store the new borrow index user.stableBorrowRate = 0; user.lastVariableBorrowCumulativeIndex = reserve.lastVariableBorrowCumulativeIndex; } else { revert("Invalid borrow rate mode"); } //increase the principal borrows and the origination fee user.principalBorrowBalance = user.principalBorrowBalance.add(_amountBorrowed).add( _balanceIncrease ); user.originationFee = user.originationFee.add(_fee); //solium-disable-next-line user.lastUpdateTimestamp = uint40(block.timestamp); }
isUserAllowedToBorrowAtStable
isUserAllowedToBorrowAtStable
checks whether user is allowed to borrow _reserve
in stable rate. Conditions:- Reserve must be enabled for stable rate borrowing
- Users cannot borrow from the reserve if their collateral amount is bigger than the amount the user wants to borrow, to prevent abuses
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev checks if a user is allowed to borrow at a stable rate * @param _reserve the reserve address * @param _user the user * @param _amount the amount the the user wants to borrow * @return true if the user is allowed to borrow at a stable rate, false otherwise **/ function isUserAllowedToBorrowAtStable(address _reserve, address _user, uint256 _amount) external view returns (bool) { CoreLibrary.ReserveData storage reserve = reserves[_reserve]; CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve]; if (!reserve.isStableBorrowRateEnabled) return false; return !user.useAsCollateral || !reserve.usageAsCollateralEnabled || _amount > getUserUnderlyingAssetBalance(_reserve, _user); } /** * @dev gets the underlying asset balance of a user based on the corresponding aToken balance. * @param _reserve the reserve address * @param _user the user address * @return the underlying deposit balance of the user **/ function getUserUnderlyingAssetBalance(address _reserve, address _user) public view returns (uint256) { AToken aToken = AToken(reserves[_reserve].aTokenAddress); return aToken.balanceOf(_user); }
updateStateOnRepay
updateStateOnRepay
handles the state updates when a user repays their borrowed funds. Let's analyze it in detail.Steps:
- Access Control
Protected by
onlyLendingPool
modifier, ensuring only the main LendingPool contract can call it- Update Reserve State
- Updates the reserve's cumulative indexes
- Update borrow amount
- Update User State
- Updates the user's principal borrow balance
- Reduces the origination fee by the amount repaid
- Resets rate mode if the entire loan is repaid
- Updates the last update timestamp
- Interest Rate Update
- Recalculates the reserve's interest rates based on the new liquidity conditions
- Updates the last update timestamp
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev updates the state of the core as a consequence of a repay action. * @param _reserve the address of the reserve on which the user is repaying * @param _user the address of the borrower * @param _paybackAmountMinusFees the amount being paid back minus fees * @param _originationFeeRepaid the fee on the amount that is being repaid * @param _balanceIncrease the accrued interest on the borrowed amount * @param _repaidWholeLoan true if the user is repaying the whole loan **/ function updateStateOnRepay( address _reserve, address _user, uint256 _paybackAmountMinusFees, uint256 _originationFeeRepaid, uint256 _balanceIncrease, bool _repaidWholeLoan /// Access Control ) external onlyLendingPool { /// Update Reserve State updateReserveStateOnRepayInternal( _reserve, _user, _paybackAmountMinusFees, _balanceIncrease ); /// Update User State updateUserStateOnRepayInternal( _reserve, _user, _paybackAmountMinusFees, _originationFeeRepaid, _balanceIncrease, _repaidWholeLoan ); /// Interest Rate Update updateReserveInterestRatesAndTimestampInternal(_reserve, _paybackAmountMinusFees, 0); } /** * @dev updates the state of the reserve as a consequence of a repay action. * @param _reserve the address of the reserve on which the user is repaying * @param _user the address of the borrower * @param _paybackAmountMinusFees the amount being paid back minus fees * @param _balanceIncrease the accrued interest on the borrowed amount **/ function updateReserveStateOnRepayInternal( address _reserve, address _user, uint256 _paybackAmountMinusFees, uint256 _balanceIncrease ) internal { CoreLibrary.ReserveData storage reserve = reserves[_reserve]; CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve]; CoreLibrary.InterestRateMode borrowRateMode = getUserCurrentBorrowRateMode(_reserve, _user); //update the indexes reserves[_reserve].updateCumulativeIndexes(); //compound the cumulated interest to the borrow balance and then subtracting the payback amount if (borrowRateMode == CoreLibrary.InterestRateMode.STABLE) { reserve.increaseTotalBorrowsStableAndUpdateAverageRate( _balanceIncrease, user.stableBorrowRate ); reserve.decreaseTotalBorrowsStableAndUpdateAverageRate( _paybackAmountMinusFees, user.stableBorrowRate ); } else { reserve.increaseTotalBorrowsVariable(_balanceIncrease); reserve.decreaseTotalBorrowsVariable(_paybackAmountMinusFees); } } /** * @dev updates the state of the user as a consequence of a repay action. * @param _reserve the address of the reserve on which the user is repaying * @param _user the address of the borrower * @param _paybackAmountMinusFees the amount being paid back minus fees * @param _originationFeeRepaid the fee on the amount that is being repaid * @param _balanceIncrease the accrued interest on the borrowed amount * @param _repaidWholeLoan true if the user is repaying the whole loan **/ function updateUserStateOnRepayInternal( address _reserve, address _user, uint256 _paybackAmountMinusFees, uint256 _originationFeeRepaid, uint256 _balanceIncrease, bool _repaidWholeLoan ) internal { CoreLibrary.ReserveData storage reserve = reserves[_reserve]; CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve]; //update the user principal borrow balance, adding the cumulated interest and then subtracting the payback amount user.principalBorrowBalance = user.principalBorrowBalance.add(_balanceIncrease).sub( _paybackAmountMinusFees ); user.lastVariableBorrowCumulativeIndex = reserve.lastVariableBorrowCumulativeIndex; //if the balance decrease is equal to the previous principal (user is repaying the whole loan) //and the rate mode is stable, we reset the interest rate mode of the user if (_repaidWholeLoan) { user.stableBorrowRate = 0; user.lastVariableBorrowCumulativeIndex = 0; } user.originationFee = user.originationFee.sub(_originationFeeRepaid); //solium-disable-next-line user.lastUpdateTimestamp = uint40(block.timestamp); }
transferToReserve
transferToReserve
also handles refund/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPool.sol --- /** * @dev transfers an amount from a user to the destination reserve * @param _reserve the address of the reserve where the amount is being transferred * @param _user the address of the user from where the transfer is happening * @param _amount the amount being transferred **/ function transferToReserve(address _reserve, address payable _user, uint256 _amount) external payable onlyLendingPool { if (_reserve != EthAddressLib.ethAddress()) { require(msg.value == 0, "User is sending ETH along with the ERC20 transfer."); ERC20(_reserve).safeTransferFrom(_user, address(this), _amount); } else { require(msg.value >= _amount, "The amount and the value sent to deposit do not match"); if (msg.value > _amount) { //send back excess ETH uint256 excessAmount = msg.value.sub(_amount); //solium-disable-next-line (bool result, ) = _user.call.value(excessAmount).gas(50000)(""); require(result, "Transfer of ETH failed"); } } }
transferToFeeCollectionAddress
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev transfers the protocol fees to the fees collection address * @param _token the address of the token being transferred * @param _user the address of the user from where the transfer is happening * @param _amount the amount being transferred * @param _destination the fee receiver address **/ function transferToFeeCollectionAddress( address _token, address _user, uint256 _amount, address _destination ) external payable onlyLendingPool { address payable feeAddress = address(uint160(_destination)); //cast the address to payable if (_token != EthAddressLib.ethAddress()) { require( msg.value == 0, "User is sending ETH along with the ERC20 transfer. Check the value attribute of the transaction" ); ERC20(_token).safeTransferFrom(_user, feeAddress, _amount); } else { require(msg.value >= _amount, "The amount and the value sent to deposit do not match"); //solium-disable-next-line (bool result, ) = feeAddress.call.value(_amount).gas(50000)(""); require(result, "Transfer of ETH failed"); } }
updateStateOnLiquidation
updateStateOnLiquidation
is a critical function that handles state updates during the liquidation process.Steps:
- Update Principal Reserve State:
- Update cumulative indexes for the principal reserve
- Increase total borrows by accrued interest (
_balanceIncrease
) - Decrease total borrows by the liquidated amount (
_amountToLiquidate
) - Handles stable and variable rate borrows separately
Calls
updatePrincipalReserveStateOnLiquidationInternal
to:- Update Collateral Reserve State:
- Update cumulative indexes for the collateral reserve
Calls
updateCollateralReserveStateOnLiquidationInternal
to:- Update User State:
- Adjust the user's principal borrow balance (add interest, subtract liquidated amount)
- Update variable borrow index if applicable
- Reduce origination fee by the liquidated fee amount
- Update last update timestamp(necessary for stable interest rate borrow’s fee calculation)
Calls
updateUserStateOnLiquidationInternal
to:- Update Interest Rates:
- Recalculates interest rates based on new liquidity conditions
- The principal reserve has increased liquidity due to repayment (
_amountToLiquidate
)
Calls
updateReserveInterestRatesAndTimestampInternal
for the principal reserve:- Handle Collateral Withdrawal:
- Updates interest rates for the collateral reserve:
- The collateral reserve loses liquidity (
_collateralToLiquidate + _liquidatedCollateralForFee
)
If the liquidator receives underlying assets (not aTokens):
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev updates the state of the core as a consequence of a liquidation action. * @param _principalReserve the address of the principal reserve that is being repaid * @param _collateralReserve the address of the collateral reserve that is being liquidated * @param _user the address of the borrower * @param _amountToLiquidate the amount being repaid by the liquidator * @param _collateralToLiquidate the amount of collateral being liquidated * @param _feeLiquidated the amount of origination fee being liquidated * @param _liquidatedCollateralForFee the amount of collateral equivalent to the origination fee + bonus * @param _balanceIncrease the accrued interest on the borrowed amount * @param _liquidatorReceivesAToken true if the liquidator will receive aTokens, false otherwise **/ function updateStateOnLiquidation( address _principalReserve, address _collateralReserve, address _user, uint256 _amountToLiquidate, uint256 _collateralToLiquidate, uint256 _feeLiquidated, uint256 _liquidatedCollateralForFee, uint256 _balanceIncrease, bool _liquidatorReceivesAToken ) external onlyLendingPool { /// Update Principal Reserve State: updatePrincipalReserveStateOnLiquidationInternal( _principalReserve, _user, _amountToLiquidate, _balanceIncrease ); /// Update Collateral Reserve State: updateCollateralReserveStateOnLiquidationInternal( _collateralReserve ); /// Update User State: updateUserStateOnLiquidationInternal( _principalReserve, _user, _amountToLiquidate, _feeLiquidated, _balanceIncrease ); /// Update Interest Rates: updateReserveInterestRatesAndTimestampInternal(_principalReserve, _amountToLiquidate, 0); /// Handle Collateral Withdrawal: if (!_liquidatorReceivesAToken) { updateReserveInterestRatesAndTimestampInternal( _collateralReserve, 0, _collateralToLiquidate.add(_liquidatedCollateralForFee) ); } }
updatePrincipalReserveStateOnLiquidationInternal
updatePrincipalReserveStateOnLiquidationInternal
updates reserve’s borrow states includes:- cumulative interest rate checkpoint
- total borrow amount
- average borrow rate for stable borrow
- variable borrow rate
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev updates the state of the principal reserve as a consequence of a liquidation action. * @param _principalReserve the address of the principal reserve that is being repaid * @param _user the address of the borrower * @param _amountToLiquidate the amount being repaid by the liquidator * @param _balanceIncrease the accrued interest on the borrowed amount **/ function updatePrincipalReserveStateOnLiquidationInternal( address _principalReserve, address _user, uint256 _amountToLiquidate, uint256 _balanceIncrease ) internal { CoreLibrary.ReserveData storage reserve = reserves[_principalReserve]; CoreLibrary.UserReserveData storage user = usersReserveData[_user][_principalReserve]; //update principal reserve data reserve.updateCumulativeIndexes(); CoreLibrary.InterestRateMode borrowRateMode = getUserCurrentBorrowRateMode( _principalReserve, _user ); if (borrowRateMode == CoreLibrary.InterestRateMode.STABLE) { //increase the total borrows by the compounded interest reserve.increaseTotalBorrowsStableAndUpdateAverageRate( _balanceIncrease, user.stableBorrowRate ); //decrease by the actual amount to liquidate reserve.decreaseTotalBorrowsStableAndUpdateAverageRate( _amountToLiquidate, user.stableBorrowRate ); } else { //increase the total borrows by the compounded interest reserve.increaseTotalBorrowsVariable(_balanceIncrease); //decrease by the actual amount to liquidate reserve.decreaseTotalBorrowsVariable(_amountToLiquidate); } }
updateCollateralReserveStateOnLiquidationInternal
updateCollateralReserveStateOnLiquidationInternal
update collateral’s cumulative interest rate checkpoint./// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev updates the state of the collateral reserve as a consequence of a liquidation action. * @param _collateralReserve the address of the collateral reserve that is being liquidated **/ function updateCollateralReserveStateOnLiquidationInternal( address _collateralReserve ) internal { //update collateral reserve reserves[_collateralReserve].updateCumulativeIndexes(); }
updateUserStateOnLiquidationInternal
updateUserStateOnLiquidationInternal
update user’s borrow principal state including:- principal borrow amount
- last cumulative borrow interest rate
- origination fee
- last update time
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev updates the state of the user being liquidated as a consequence of a liquidation action. * @param _reserve the address of the principal reserve that is being repaid * @param _user the address of the borrower * @param _amountToLiquidate the amount being repaid by the liquidator * @param _feeLiquidated the amount of origination fee being liquidated * @param _balanceIncrease the accrued interest on the borrowed amount **/ function updateUserStateOnLiquidationInternal( address _reserve, address _user, uint256 _amountToLiquidate, uint256 _feeLiquidated, uint256 _balanceIncrease ) internal { CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve]; CoreLibrary.ReserveData storage reserve = reserves[_reserve]; //first increase by the compounded interest, then decrease by the liquidated amount user.principalBorrowBalance = user.principalBorrowBalance.add(_balanceIncrease).sub( _amountToLiquidate ); if ( getUserCurrentBorrowRateMode(_reserve, _user) == CoreLibrary.InterestRateMode.VARIABLE ) { user.lastVariableBorrowCumulativeIndex = reserve.lastVariableBorrowCumulativeIndex; } if(_feeLiquidated > 0){ user.originationFee = user.originationFee.sub(_feeLiquidated); } //solium-disable-next-line user.lastUpdateTimestamp = uint40(block.timestamp); }
liquidateFee
liquidateFee
transfers the fees to the fees collection address in the case of liquidationthe difference between
liquidateFee
and transferToFeeCollectionAddress
is that liquidateFee
requires msg.value
is zero as it transfers collateral to fee receiver by itself rather than user repays the fee./// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev transfers the fees to the fees collection address in the case of liquidation * @param _token the address of the token being transferred * @param _amount the amount being transferred * @param _destination the fee receiver address **/ function liquidateFee( address _token, uint256 _amount, address _destination ) external payable onlyLendingPool { address payable feeAddress = address(uint160(_destination)); //cast the address to payable require( msg.value == 0, "Fee liquidation does not require any transfer of value" ); if (_token != EthAddressLib.ethAddress()) { ERC20(_token).safeTransfer(feeAddress, _amount); } else { //solium-disable-next-line (bool result, ) = feeAddress.call.value(_amount).gas(50000)(""); require(result, "Transfer of ETH failed"); } }
updateStateOnFlashLoan
updateStateOnFlashLoan
updates state basedd on flash loan.Steps:
- first update reserve cumulative interest rate checkpoint which happens before flashloan income
- update cumulative interest rate checkpoint based on flash loan income
- update interest rates based on the latest reserve state
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev updates the state of the core as a result of a flashloan action * @param _reserve the address of the reserve in which the flashloan is happening * @param _income the income of the protocol as a result of the action **/ function updateStateOnFlashLoan( address _reserve, uint256 _availableLiquidityBefore, uint256 _income, uint256 _protocolFee ) external onlyLendingPool { transferFlashLoanProtocolFeeInternal(_reserve, _protocolFee); //compounding the cumulated interest reserves[_reserve].updateCumulativeIndexes(); uint256 totalLiquidityBefore = _availableLiquidityBefore.add( getReserveTotalBorrows(_reserve) ); //compounding the received fee into the reserve reserves[_reserve].cumulateToLiquidityIndex(totalLiquidityBefore, _income); //refresh interest rates updateReserveInterestRatesAndTimestampInternal(_reserve, _income, 0); } /** * @dev transfers to the protocol fees of a flashloan to the fees collection address * @param _token the address of the token being transferred * @param _amount the amount being transferred **/ function transferFlashLoanProtocolFeeInternal(address _token, uint256 _amount) internal { address payable receiver = address(uint160(addressesProvider.getTokenDistributor())); if (_token != EthAddressLib.ethAddress()) { ERC20(_token).safeTransfer(receiver, _amount); } else { //solium-disable-next-line (bool result, ) = receiver.call.value(_amount)(""); require(result, "Transfer to token distributor failed"); } } /// --- v1/contract/aave-protocol/contracts/libraries/CoreLibrary.sol --- /** * @dev accumulates a predefined amount of asset to the reserve as a fixed, one time income. Used for example to accumulate * the flashloan fee to the reserve, and spread it through the depositors. * @param _self the reserve object * @param _totalLiquidity the total liquidity available in the reserve * @param _amount the amount to accomulate **/ function cumulateToLiquidityIndex( ReserveData storage _self, uint256 _totalLiquidity, uint256 _amount ) internal { uint256 amountToLiquidityRatio = _amount.wadToRay().rayDiv(_totalLiquidity.wadToRay()); uint256 cumulatedLiquidity = amountToLiquidityRatio.add(WadRayMath.ray()); _self.lastLiquidityCumulativeIndex = cumulatedLiquidity.rayMul( _self.lastLiquidityCumulativeIndex ); }
CoreLibrary
updateStateOnDeposit
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev updates the state of the core as a result of a deposit action * @param _reserve the address of the reserve in which the deposit is happening * @param _user the address of the the user depositing * @param _amount the amount being deposited * @param _isFirstDeposit true if the user is depositing for the first time **/ function updateStateOnDeposit( address _reserve, address _user, uint256 _amount, bool _isFirstDeposit ) external onlyLendingPool { reserves[_reserve].updateCumulativeIndexes(); updateReserveInterestRatesAndTimestampInternal(_reserve, _amount, 0); if (_isFirstDeposit) { //if this is the first deposit of the user, we configure the deposit as enabled to be used as collateral setUserUseReserveAsCollateral(_reserve, _user, true); } } /** * @dev enables or disables a reserve as collateral * @param _reserve the address of the principal reserve where the user deposited * @param _user the address of the depositor * @param _useAsCollateral true if the depositor wants to use the reserve as collateral **/ function setUserUseReserveAsCollateral(address _reserve, address _user, bool _useAsCollateral) public onlyLendingPool { CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve]; user.useAsCollateral = _useAsCollateral; }
updateReserveInterestRatesAndTimestampInternal
Updates the reserve current stable borrow rate Rf, the current variable borrow rate Rv and the current liquidity rate Rl based on reserve interest strategy.
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev Updates the reserve current stable borrow rate Rf, the current variable borrow rate Rv and the current liquidity rate Rl. * Also updates the lastUpdateTimestamp value. Please refer to the whitepaper for further information. * @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 updateReserveInterestRatesAndTimestampInternal( address _reserve, uint256 _liquidityAdded, uint256 _liquidityTaken ) internal { CoreLibrary.ReserveData storage reserve = reserves[_reserve]; (uint256 newLiquidityRate, uint256 newStableRate, uint256 newVariableRate) = IReserveInterestRateStrategy( reserve .interestRateStrategyAddress ) .calculateInterestRates( _reserve, getReserveAvailableLiquidity(_reserve).add(_liquidityAdded).sub(_liquidityTaken), reserve.totalBorrowsStable, reserve.totalBorrowsVariable, reserve.currentAverageStableBorrowRate ); reserve.currentLiquidityRate = newLiquidityRate; reserve.currentStableBorrowRate = newStableRate; reserve.currentVariableBorrowRate = newVariableRate; //solium-disable-next-line reserve.lastUpdateTimestamp = uint40(block.timestamp); emit ReserveUpdated( _reserve, newLiquidityRate, newStableRate, newVariableRate, reserve.lastLiquidityCumulativeIndex, reserve.lastVariableBorrowCumulativeIndex ); } /** * @dev gets the available liquidity in the reserve. The available liquidity is the balance of the core contract * @param _reserve the reserve address * @return the available liquidity **/ function getReserveAvailableLiquidity(address _reserve) public view returns (uint256) { uint256 balance = 0; if (_reserve == EthAddressLib.ethAddress()) { balance = address(this).balance; } else { balance = IERC20(_reserve).balanceOf(address(this)); } return balance; }
updateCumulativeIndexes
Update
lastLiquidityCumulativeIndex
and lastVariableBorrowCumulativeIndex
based on current liquidity rate(currentLiquidityRate
) and variable borrow rate(currentVariableBorrowRate
)/// --- v1/contract/aave-protocol/contracts/libraries/CoreLibrary.sol --- /** * @dev Updates the liquidity cumulative index Ci and variable borrow cumulative index Bvc. Refer to the whitepaper for * a formal specification. * @param _self the reserve object **/ function updateCumulativeIndexes(ReserveData storage _self) internal { uint256 totalBorrows = getTotalBorrows(_self); if (totalBorrows > 0) { //only cumulating if there is any income being produced uint256 cumulatedLiquidityInterest = calculateLinearInterest( _self.currentLiquidityRate, _self.lastUpdateTimestamp ); _self.lastLiquidityCumulativeIndex = cumulatedLiquidityInterest.rayMul( _self.lastLiquidityCumulativeIndex ); uint256 cumulatedVariableBorrowInterest = calculateCompoundedInterest( _self.currentVariableBorrowRate, _self.lastUpdateTimestamp ); _self.lastVariableBorrowCumulativeIndex = cumulatedVariableBorrowInterest.rayMul( _self.lastVariableBorrowCumulativeIndex ); } } /** * @dev returns the total borrows on the reserve * @param _reserve the reserve object * @return the total borrows (stable + variable) **/ function getTotalBorrows(CoreLibrary.ReserveData storage _reserve) internal view returns (uint256) { return _reserve.totalBorrowsStable.add(_reserve.totalBorrowsVariable); }
getCompoundedBorrowBalance
getCompoundedBorrowBalance
calculates user’s borrow considering interest generated./// --- v1/contract/aave-protocol/contracts/libraries/CoreLibrary.sol --- /** * @dev calculates the compounded borrow balance of a user * @param _self the userReserve object * @param _reserve the reserve object * @return the user compounded borrow balance **/ function getCompoundedBorrowBalance( CoreLibrary.UserReserveData storage _self, CoreLibrary.ReserveData storage _reserve ) internal view returns (uint256) { if (_self.principalBorrowBalance == 0) return 0; uint256 principalBorrowBalanceRay = _self.principalBorrowBalance.wadToRay(); uint256 compoundedBalance = 0; uint256 cumulatedInterest = 0; if (_self.stableBorrowRate > 0) { cumulatedInterest = calculateCompoundedInterest( _self.stableBorrowRate, _self.lastUpdateTimestamp ); } else { //variable interest cumulatedInterest = calculateCompoundedInterest( _reserve .currentVariableBorrowRate, _reserve .lastUpdateTimestamp ) .rayMul(_reserve.lastVariableBorrowCumulativeIndex) .rayDiv(_self.lastVariableBorrowCumulativeIndex); } compoundedBalance = principalBorrowBalanceRay.rayMul(cumulatedInterest).rayToWad(); if (compoundedBalance == _self.principalBorrowBalance) { //solium-disable-next-line if (_self.lastUpdateTimestamp != block.timestamp) { //no interest cumulation because of the rounding - we add 1 wei //as symbolic cumulated interest to avoid interest free loans. return _self.principalBorrowBalance.add(1 wei); } } return compoundedBalance; }
WadRayMath
WadRayMath.sol
provides mul and div function for wads (decimal numbers with 18 digits precision) and rays (decimals with 27 digits)Note all operation is rounded:
- if decimal part is smaller than 0.5, discard it.
- else increase 1.
/// --- v1/contract/aave-protocol/contracts/libraries/WadRayMath.sol --- uint256 internal constant WAD = 1e18; uint256 internal constant halfWAD = WAD / 2; uint256 internal constant RAY = 1e27; uint256 internal constant halfRAY = RAY / 2; uint256 internal constant WAD_RAY_RATIO = 1e9; function wadToRay(uint256 a) internal pure returns (uint256) { return a.mul(WAD_RAY_RATIO); } // round mul: // if decimal part is smaller than 0.5, discard it. // Else increase 1. function rayMul(uint256 a, uint256 b) internal pure returns (uint256) { return halfRAY.add(a.mul(b)).div(RAY); } // round div: // if decimal part is smaller than 0.5, discard it. // Else increase 1. function rayDiv(uint256 a, uint256 b) internal pure returns (uint256) { uint256 halfB = b / 2; return halfB.add(a.mul(RAY)).div(b); } // round operation: // if decimal part is smaller than 0.5, discard it. // Else increase 1. function rayToWad(uint256 a) internal pure returns (uint256) { uint256 halfRatio = WAD_RAY_RATIO / 2; return halfRatio.add(a).div(WAD_RAY_RATIO); }
DefaultReserveInterestRateStrategy
calculateInterestRates
function determines liquidity rates, stable borrow rates, and variable borrow rates for a given reserve. - Stable borrow rates and variable borrow rates are calculated in a tiered manner.
- Liquidity rates is calculated as weighted borrow rates of stable and variable borrows.
Params:
_reserve
: Address of the reserve (e.g., DAI pool).
_availableLiquidity
: How much of the asset is currently available to borrow.
_totalBorrowsStable
: Total amount borrowed at stable rates.
_totalBorrowsVariable
: Total amount borrowed at variable rates.
_averageStableBorrowRate
: Weighted average interest rate for stable borrows.
Returns:
currentLiquidityRate
: The rate earned by depositors.
currentStableBorrowRate
: The current stable borrow rate.
currentVariableBorrowRate
: The current variable borrow rate.
Note for function
getOverallBorrowRateInternal
, _currentAverageStableBorrowRate
paramters uses _averageStableBorrowRate
rather than newest stable rate currentStableBorrowRate
. Because for borrows based on stable rate, their interest rates don’t change, so the updated liquidity rate(weighted) should based on past average stable borrow rate and updated variable borrow rate.After update, the interests generated by borrows are always same with the interest generated by LP.
/// --- v1/contract/aave-protocol/contracts/lendingpool/DefaultReserveInterestRateStrategy.sol --- /** * @dev this constant represents the utilization rate at which the pool aims to obtain most competitive borrow rates * expressed in ray **/ uint256 public constant OPTIMAL_UTILIZATION_RATE = 0.8 * 1e27; /** * @dev this constant represents the excess utilization rate above the optimal. It's always equal to * 1-optimal utilization rate. Added as a constant here for gas optimizations * expressed in ray **/ uint256 public constant EXCESS_UTILIZATION_RATE = 0.2 * 1e27; //base variable borrow rate when Utilization rate = 0. Expressed in ray uint256 public baseVariableBorrowRate; //slope of the variable interest curve when utilization rate > 0 and <= OPTIMAL_UTILIZATION_RATE. Expressed in ray uint256 public variableRateSlope1; //slope of the variable interest curve when utilization rate > OPTIMAL_UTILIZATION_RATE. Expressed in ray uint256 public variableRateSlope2; //slope of the stable interest curve when utilization rate > 0 and <= OPTIMAL_UTILIZATION_RATE. Expressed in ray uint256 public stableRateSlope1; //slope of the stable interest curve when utilization rate > OPTIMAL_UTILIZATION_RATE. Expressed in ray uint256 public stableRateSlope2; /** * @dev calculates the interest rates depending on the available liquidity and the total borrowed. * @param _reserve the address of the reserve * @param _availableLiquidity the liquidity available in the reserve * @param _totalBorrowsStable the total borrowed from the reserve a stable rate * @param _totalBorrowsVariable the total borrowed from the reserve at a variable rate * @param _averageStableBorrowRate the weighted average of all the stable rate borrows * @return the liquidity rate, stable borrow rate and variable borrow rate calculated from the input parameters **/ function calculateInterestRates( address _reserve, uint256 _availableLiquidity, uint256 _totalBorrowsStable, uint256 _totalBorrowsVariable, uint256 _averageStableBorrowRate ) external view returns ( uint256 currentLiquidityRate, uint256 currentStableBorrowRate, uint256 currentVariableBorrowRate ) { uint256 totalBorrows = _totalBorrowsStable.add(_totalBorrowsVariable); uint256 utilizationRate = (totalBorrows == 0 && _availableLiquidity == 0) ? 0 : totalBorrows.rayDiv(_availableLiquidity.add(totalBorrows)); currentStableBorrowRate = ILendingRateOracle(addressesProvider.getLendingRateOracle()) .getMarketBorrowRate(_reserve); if (utilizationRate > OPTIMAL_UTILIZATION_RATE) { uint256 excessUtilizationRateRatio = utilizationRate .sub(OPTIMAL_UTILIZATION_RATE) .rayDiv(EXCESS_UTILIZATION_RATE); currentStableBorrowRate = currentStableBorrowRate.add(stableRateSlope1).add( stableRateSlope2.rayMul(excessUtilizationRateRatio) ); currentVariableBorrowRate = baseVariableBorrowRate.add(variableRateSlope1).add( variableRateSlope2.rayMul(excessUtilizationRateRatio) ); } else { currentStableBorrowRate = currentStableBorrowRate.add( stableRateSlope1.rayMul( utilizationRate.rayDiv( OPTIMAL_UTILIZATION_RATE ) ) ); currentVariableBorrowRate = baseVariableBorrowRate.add( utilizationRate.rayDiv(OPTIMAL_UTILIZATION_RATE).rayMul(variableRateSlope1) ); } currentLiquidityRate = getOverallBorrowRateInternal( _totalBorrowsStable, _totalBorrowsVariable, currentVariableBorrowRate, _averageStableBorrowRate ) .rayMul(utilizationRate); } /** * @dev calculates the overall borrow rate as the weighted average between the total variable borrows and total stable borrows. * @param _totalBorrowsStable the total borrowed from the reserve a stable rate * @param _totalBorrowsVariable the total borrowed from the reserve at a variable rate * @param _currentVariableBorrowRate the current variable borrow rate * @param _currentAverageStableBorrowRate the weighted average of all the stable rate borrows * @return the weighted averaged borrow rate **/ function getOverallBorrowRateInternal( uint256 _totalBorrowsStable, uint256 _totalBorrowsVariable, uint256 _currentVariableBorrowRate, uint256 _currentAverageStableBorrowRate ) internal pure returns (uint256) { uint256 totalBorrows = _totalBorrowsStable.add(_totalBorrowsVariable); if (totalBorrows == 0) return 0; uint256 weightedVariableRate = _totalBorrowsVariable.wadToRay().rayMul( _currentVariableBorrowRate ); uint256 weightedStableRate = _totalBorrowsStable.wadToRay().rayMul( _currentAverageStableBorrowRate ); uint256 overallBorrowRate = weightedVariableRate.add(weightedStableRate).rayDiv( totalBorrows.wadToRay() ); return overallBorrowRate; }
LendingPoolDataProvider
balanceDecreaseAllowed
balanceDecreaseAllowed
checks if decreasing a user's collateral balance by a specific amount would cause their health factor to fall below the liquidation threshold (1e18).Steps:
- Initial Checks:
- First checks if the reserve is enabled as collateral (
reserveUsageAsCollateralEnabled
) - Checks if the user is currently using this reserve as collateral (
core.isUserUseReserveAsCollateralEnabled
) - If either condition fails, returns
true
(decrease is allowed). Since the asset cant be used as collateral by this user, there is no borrow based on this asset, thus no need to block withdrawal.
- USer Global Data Calculation:
collateralBalanceETH
: User's total collateral value in ETHborrowBalanceETH
: User's total borrow value in ETHtotalFeesETH
: User's total fees in ETHcurrentLiquidationThreshold
: Weighted average liquidation threshold
Calls
calculateUserGlobalData(_user)
to get:- Early Return Condition:
If user has no borrows (
borrowBalanceETH == 0
), returns true
immediately. As user has no borrow, no need to block withdrawal.- Amount Conversion:
Converts the decrease amount to ETH value using price oracle.
- Collateral Decrease Calculation:
Calculates new collateral balance after decrease.
- Liquidation Threshold Recalculation:
Calculates new weighted average liquidation threshold.
- Health Factor Check:
Calculates new health factor using helper function
Returns
true
if new health factor > liquidation threshold (1e18), false
otherwiseLiquidation Threshold Recalculation:
Formula Breakdown:
vars.liquidationThresholdAfterDecrease = vars.collateralBalanceETH.mul(vars.currentLiquidationThreshold) .sub(vars.amountToDecreaseETH.mul(vars.reserveLiquidationThreshold)) .div(vars.collateralBalancefterDecrease);
vars.collateralBalanceETH
: The total value of the user's collateral (in ETH) before the decrease.
vars.currentLiquidationThreshold
: The current weighted average liquidation threshold (in percentage points, e.g., 50% is represented as 50) before the decrease.
vars.amountToDecreaseETH
: The value of the collateral being decreased (in ETH), converted from the reserve asset using the price oracle.
vars.reserveLiquidationThreshold
: The liquidation threshold specific to the reserve from which collateral is being decreased.
vars.collateralBalancefterDecrease
: The total collateral value (in ETH) after the decrease, calculated asvars.collateralBalanceETH - vars.amountToDecreaseETH
.
How It Works
- Weighted Sum Before Decrease:
vars.collateralBalanceETH.mul(vars.currentLiquidationThreshold)
computes the total "weighted liquidation value" of all collateral. This is the sum of each collateral's value multiplied by its liquidation threshold.
- Subtract the Removed Collateral's Contribution:
vars.amountToDecreaseETH.mul(vars.reserveLiquidationThreshold)
calculates the weighted value of the decreased collateral. For example, if decreasing 50 ETH with a threshold of 50%, this equals 50 * 50 = 2,500.
- New Weighted Sum:
- Subtracting the removed weighted value from the total weighted value gives the new weighted sum.
- New Average Threshold:
- Dividing the new weighted sum by the new total collateral value (
vars.collateralBalancefterDecrease
) gives the new weighted average liquidation threshold.
Why This Calculation?
- Weighted Average: The liquidation threshold must be weighted by the value of each collateral type because different assets have different risk profiles (e.g., ETH might have a higher threshold than DAI).
- Accuracy: This ensures that after decreasing collateral, the new threshold accurately reflects the remaining collateral's risk, which is critical for calculating the health factor correctly.
- Prevention of Liquidation: The function uses this new threshold to check if the health factor remains above the liquidation threshold (1e18). If not, the decrease is not allowed.
Example Scenario
Assume:
- Total collateral: 200 ETH
- Current average liquidation threshold: 65%
- Decreasing: 50 ETH from a reserve with threshold 50%
- New collateral: 150 ETH
Calculation:
- Weighted sum before: 200 * 65 = 13,000
- Weighted value removed: 50 * 50 = 2,500
- New weighted sum: 13,000 - 2,500 = 10,500
- New average threshold: 10,500 / 150 = 70%
Thus, the new liquidation threshold is 70%. This change then affects the health factor calculation to determine if the decrease is safe.
This calculation is essential for maintaining the protocol's stability and ensuring users do not inadvertently put themselves at risk of liquidation.
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolDataProvider.sol --- struct balanceDecreaseAllowedLocalVars { uint256 decimals; uint256 collateralBalanceETH; uint256 borrowBalanceETH; uint256 totalFeesETH; uint256 currentLiquidationThreshold; uint256 reserveLiquidationThreshold; uint256 amountToDecreaseETH; uint256 collateralBalancefterDecrease; uint256 liquidationThresholdAfterDecrease; uint256 healthFactorAfterDecrease; bool reserveUsageAsCollateralEnabled; } /** * @dev specifies the health factor threshold at which the user position is liquidated. * 1e18 by default, if the health factor drops below 1e18, the loan can be liquidated. **/ uint256 public constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1e18; /** * @dev check if a specific balance decrease is allowed (i.e. doesn't bring the user borrow position health factor under 1e18) * @param _reserve the address of the reserve * @param _user the address of the user * @param _amount the amount to decrease * @return true if the decrease of the balance is allowed **/ function balanceDecreaseAllowed(address _reserve, address _user, uint256 _amount) external view returns (bool) { // Usage of a memory struct of vars to avoid "Stack too deep" errors due to local variables balanceDecreaseAllowedLocalVars memory vars; /// Initial Checks ( vars.decimals, , vars.reserveLiquidationThreshold, vars.reserveUsageAsCollateralEnabled ) = core.getReserveConfiguration(_reserve); if ( !vars.reserveUsageAsCollateralEnabled || !core.isUserUseReserveAsCollateralEnabled(_reserve, _user) ) { return true; //if reserve is not used as collateral, no reasons to block the transfer } /// User Global Data Calculation: ( , vars.collateralBalanceETH, vars.borrowBalanceETH, vars.totalFeesETH, , vars.currentLiquidationThreshold, , ) = calculateUserGlobalData(_user); /// Early Return Condition: if (vars.borrowBalanceETH == 0) { return true; //no borrows - no reasons to block the transfer } IPriceOracleGetter oracle = IPriceOracleGetter(addressesProvider.getPriceOracle()); /// Amount Conversion: vars.amountToDecreaseETH = oracle.getAssetPrice(_reserve).mul(_amount).div( 10 ** vars.decimals ); /// Collateral Decrease Calculation: vars.collateralBalancefterDecrease = vars.collateralBalanceETH.sub( vars.amountToDecreaseETH ); //if there is a borrow, there can't be 0 collateral if (vars.collateralBalancefterDecrease == 0) { return false; } /// Liquidation Threshold Recalculation: vars.liquidationThresholdAfterDecrease = vars .collateralBalanceETH .mul(vars.currentLiquidationThreshold) .sub(vars.amountToDecreaseETH.mul(vars.reserveLiquidationThreshold)) .div(vars.collateralBalancefterDecrease); /// Health Factor Check: uint256 healthFactorAfterDecrease = calculateHealthFactorFromBalancesInternal( vars.collateralBalancefterDecrease, vars.borrowBalanceETH, vars.totalFeesETH, vars.liquidationThresholdAfterDecrease ); return healthFactorAfterDecrease > HEALTH_FACTOR_LIQUIDATION_THRESHOLD; } /** * @dev calculates the health factor from the corresponding balances * @param collateralBalanceETH the total collateral balance in ETH * @param borrowBalanceETH the total borrow balance in ETH * @param totalFeesETH the total fees in ETH * @param liquidationThreshold the avg liquidation threshold **/ function calculateHealthFactorFromBalancesInternal( uint256 collateralBalanceETH, uint256 borrowBalanceETH, uint256 totalFeesETH, uint256 liquidationThreshold ) internal pure returns (uint256) { if (borrowBalanceETH == 0) return uint256(-1); return (collateralBalanceETH.mul(liquidationThreshold).div(100)).wadDiv( borrowBalanceETH.add(totalFeesETH) ); } /// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- /** * @dev this function aggregates the configuration parameters of the reserve. * It's used in the LendingPoolDataProvider specifically to save gas, and avoid * multiple external contract calls to fetch the same data. * @param _reserve the reserve address * @return the reserve decimals * @return the base ltv as collateral * @return the liquidation threshold * @return if the reserve is used as collateral or not **/ function getReserveConfiguration(address _reserve) external view returns (uint256, uint256, uint256, bool) { uint256 decimals; uint256 baseLTVasCollateral; uint256 liquidationThreshold; bool usageAsCollateralEnabled; CoreLibrary.ReserveData storage reserve = reserves[_reserve]; decimals = reserve.decimals; baseLTVasCollateral = reserve.baseLTVasCollateral; liquidationThreshold = reserve.liquidationThreshold; usageAsCollateralEnabled = reserve.usageAsCollateralEnabled; return (decimals, baseLTVasCollateral, liquidationThreshold, usageAsCollateralEnabled); }
calculateUserGlobalData
calculateUserGlobalData
function in the LendingPoolDataProvider contract is a comprehensive function that calculates a user's overall position across all reserves in the protocol. It aggregates data from all of a user's positions to determine their overall financial health in the system.Steps:
- Initial Setup
- Gets the price oracle from the addresses provider
- Retrieves the list of all reserves in the protocol
- Initializes a memory struct to hold computation variables
- Reserve Iteration
- The function iterates through all reserves in the protocol.
- For each reserve, it gets the user's data:
- compoundedLiquidityBalance (balance including liquidity interest)
- compoundedBorrowBalance (borrow balance including interest)
- originationFee (fees cumulated)
- userUsesReserveAsCollateral (whether this reserve is collateral)
- For each reserve, it retrieves configuration data.
- The function converts all values to ETH using the price oracle.
- Total liquidity calculation
- All user deposits(liquidity) across all reserves are summed.
- Total collateral calculation
- Enabled as collateral
- Actually being used as collateral by the user
- Total Borrows Calculation
- All user origination fees across all reserves are summed.
Only reserves that are:
Are included in the collateral calculation:
All user borrows across all reserves are summed including interest.
- Weighted Average Calculations
- Loan-to-Value (LTV) Ratio
- Liquidation Threshold
After processing all reserves, the function calculates weighted averages:
- Health Factor Calculation
The health factor is calculated using a helper function
calculateHealthFactorFromBalancesInternal
- Liquidation Check
Finally, the function checks if the health factor is below the liquidation threshold.
/// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolDataProvider.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 * @return the total liquidity, total collateral, total borrow balances of the user in ETH. * also the average Ltv, liquidation threshold, and the health factor **/ function calculateUserGlobalData(address _user) public view returns ( uint256 totalLiquidityBalanceETH, uint256 totalCollateralBalanceETH, uint256 totalBorrowBalanceETH, uint256 totalFeesETH, uint256 currentLtv, uint256 currentLiquidationThreshold, uint256 healthFactor, bool healthFactorBelowThreshold ) { IPriceOracleGetter oracle = IPriceOracleGetter(addressesProvider.getPriceOracle()); // Usage of a memory struct of vars to avoid "Stack too deep" errors due to local variables UserGlobalDataLocalVars memory vars; address[] memory reserves = core.getReserves(); for (uint256 i = 0; i < reserves.length; i++) { vars.currentReserve = reserves[i]; ( vars.compoundedLiquidityBalance, vars.compoundedBorrowBalance, vars.originationFee, vars.userUsesReserveAsCollateral ) = core.getUserBasicReserveData(vars.currentReserve, _user); if (vars.compoundedLiquidityBalance == 0 && vars.compoundedBorrowBalance == 0) { continue; } //fetch reserve data ( vars.reserveDecimals, vars.baseLtv, vars.liquidationThreshold, vars.usageAsCollateralEnabled ) = core.getReserveConfiguration(vars.currentReserve); vars.tokenUnit = 10 ** vars.reserveDecimals; vars.reserveUnitPrice = oracle.getAssetPrice(vars.currentReserve); //liquidity and collateral balance if (vars.compoundedLiquidityBalance > 0) { uint256 liquidityBalanceETH = vars .reserveUnitPrice .mul(vars.compoundedLiquidityBalance) .div(vars.tokenUnit); totalLiquidityBalanceETH = totalLiquidityBalanceETH.add(liquidityBalanceETH); if (vars.usageAsCollateralEnabled && vars.userUsesReserveAsCollateral) { totalCollateralBalanceETH = totalCollateralBalanceETH.add(liquidityBalanceETH); currentLtv = currentLtv.add(liquidityBalanceETH.mul(vars.baseLtv)); currentLiquidationThreshold = currentLiquidationThreshold.add( liquidityBalanceETH.mul(vars.liquidationThreshold) ); } } if (vars.compoundedBorrowBalance > 0) { totalBorrowBalanceETH = totalBorrowBalanceETH.add( vars.reserveUnitPrice.mul(vars.compoundedBorrowBalance).div(vars.tokenUnit) ); totalFeesETH = totalFeesETH.add( vars.originationFee.mul(vars.reserveUnitPrice).div(vars.tokenUnit) ); } } currentLtv = totalCollateralBalanceETH > 0 ? currentLtv.div(totalCollateralBalanceETH) : 0; currentLiquidationThreshold = totalCollateralBalanceETH > 0 ? currentLiquidationThreshold.div(totalCollateralBalanceETH) : 0; healthFactor = calculateHealthFactorFromBalancesInternal( totalCollateralBalanceETH, totalBorrowBalanceETH, totalFeesETH, currentLiquidationThreshold ); healthFactorBelowThreshold = healthFactor < HEALTH_FACTOR_LIQUIDATION_THRESHOLD; } /// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolCore.sol --- address[] public reservesList; /** * @return the array of reserves configured on the core **/ function getReserves() external view returns (address[] memory) { return reservesList; } /** * @dev returns the basic data (balances, fee accrued, reserve enabled/disabled as collateral) * needed to calculate the global account data in the LendingPoolDataProvider * @param _reserve the address of the reserve * @param _user the address of the user * @return the user deposited balance, the principal borrow balance, the fee, and if the reserve is enabled as collateral or not **/ function getUserBasicReserveData(address _reserve, address _user) external view returns (uint256, uint256, uint256, bool) { CoreLibrary.ReserveData storage reserve = reserves[_reserve]; CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve]; uint256 underlyingBalance = getUserUnderlyingAssetBalance(_reserve, _user); if (user.principalBorrowBalance == 0) { return (underlyingBalance, 0, 0, user.useAsCollateral); } return ( underlyingBalance, user.getCompoundedBorrowBalance(reserve), user.originationFee, user.useAsCollateral ); } /** * @dev gets the underlying asset balance of a user based on the corresponding aToken balance. * @param _reserve the reserve address * @param _user the user address * @return the underlying deposit balance of the user **/ function getUserUnderlyingAssetBalance(address _reserve, address _user) public view returns (uint256) { AToken aToken = AToken(reserves[_reserve].aTokenAddress); return aToken.balanceOf(_user); } /** * @dev calculates the compounded borrow balance of a user * @param _self the userReserve object * @param _reserve the reserve object * @return the user compounded borrow balance **/ function getCompoundedBorrowBalance( CoreLibrary.UserReserveData storage _self, CoreLibrary.ReserveData storage _reserve ) internal view returns (uint256) { if (_self.principalBorrowBalance == 0) return 0; uint256 principalBorrowBalanceRay = _self.principalBorrowBalance.wadToRay(); uint256 compoundedBalance = 0; uint256 cumulatedInterest = 0; if (_self.stableBorrowRate > 0) { cumulatedInterest = calculateCompoundedInterest( _self.stableBorrowRate, _self.lastUpdateTimestamp ); } else { //variable interest cumulatedInterest = calculateCompoundedInterest( _reserve .currentVariableBorrowRate, _reserve .lastUpdateTimestamp ) .rayMul(_reserve.lastVariableBorrowCumulativeIndex) .rayDiv(_self.lastVariableBorrowCumulativeIndex); } compoundedBalance = principalBorrowBalanceRay.rayMul(cumulatedInterest).rayToWad(); if (compoundedBalance == _self.principalBorrowBalance) { //solium-disable-next-line if (_self.lastUpdateTimestamp != block.timestamp) { //no interest cumulation because of the rounding - we add 1 wei //as symbolic cumulated interest to avoid interest free loans. return _self.principalBorrowBalance.add(1 wei); } } return compoundedBalance; }
calculateCollateralNeededInETH
calculateCollateralNeededInETH
calculates thec total amount of collateral needed in ETH to cover a new borrow./// --- v1/contract/aave-protocol/contracts/lendingpool/LendingPoolDataProvider.sol --- /** * @notice calculates the amount of collateral needed in ETH to cover a new borrow. * @param _reserve the reserve from which the user wants to borrow * @param _amount the amount the user wants to borrow * @param _fee the fee for the amount that the user needs to cover * @param _userCurrentBorrowBalanceTH the current borrow balance of the user (before the borrow) * @param _userCurrentLtv the average ltv of the user given his current collateral * @return the total amount of collateral in ETH to cover the current borrow balance + the new amount + fee **/ function calculateCollateralNeededInETH( address _reserve, uint256 _amount, uint256 _fee, uint256 _userCurrentBorrowBalanceTH, uint256 _userCurrentFeesETH, uint256 _userCurrentLtv ) external view returns (uint256) { uint256 reserveDecimals = core.getReserveDecimals(_reserve); IPriceOracleGetter oracle = IPriceOracleGetter(addressesProvider.getPriceOracle()); uint256 requestedBorrowAmountETH = oracle .getAssetPrice(_reserve) .mul(_amount.add(_fee)) .div(10 ** reserveDecimals); //price is in ether //add the current already borrowed amount to the amount requested to calculate the total collateral needed. uint256 collateralNeededInETH = _userCurrentBorrowBalanceTH .add(_userCurrentFeesETH) .add(requestedBorrowAmountETH) .mul(100) .div(_userCurrentLtv); //LTV is calculated in percentage return collateralNeededInETH; }
FeeProvider
calculateLoanOriginationFee
/// --- v1/contract/aave-protocol/contracts/fees/FeeProvider.sol --- // percentage of the fee to be calculated on the loan amount uint256 public originationFeePercentage; /** * @dev initializes the FeeProvider after it's added to the proxy * @param _addressesProvider the address of the LendingPoolAddressesProvider */ function initialize(address _addressesProvider) public initializer { /// @notice origination fee is set as default as 25 basis points of the loan amount (0.0025%) originationFeePercentage = 0.0025 * 1e18; } /** * @dev calculates the origination fee for every loan executed on the platform. * @param _user can be used in the future to apply discount to the origination fee based on the * _user account (eg. stake AAVE tokens in the lending pool, or deposit > 1M USD etc.) * @param _amount the amount of the loan **/ function calculateLoanOriginationFee(address _user, uint256 _amount) external view returns (uint256) { return _amount.wadMul(originationFeePercentage); }
Concept
baseLTV (Loan-to-Value)
- Purpose: Determines the maximum borrowing power of a specific collateral asset. It answers the question: "How much can I borrow against this asset?"
- Definition: The maximum ratio (in percentage) of a loan you can take out against a specific type of collateral. For example, a
baseLtv
of 75% for ETH means you can borrow up to 0.75 ETH worth of other assets for every 1 ETH you deposit as collateral.
- Function: It is used to calculate the user's Available Borrows.
Available Borrows ETH += (Collateral Value in ETH) * (baseLtv / 100)
/// --- v1/contract/aave-protocol/contracts/libraries/CoreLibrary.sol --- struct ReserveData { /// ... //the ltv of the reserve. Expressed in percentage (0-100) uint256 baseLTVasCollate /// ... }
liquidationThreshold
- Purpose: Defines the point of failure for a loan backed by that specific collateral. It answers the question: "At what point will my position be liquidated?"
- Definition: The threshold (in percentage) at which a position is considered undercollateralized and can be liquidated. If the borrowed amount rises above this percentage of the collateral value, the health factor drops below 1, and the position becomes eligible for liquidation.
- Function: It is used to calculate the user's Health Factor, which determines if their position is safe.
/// --- v1/contract/aave-protocol/contracts/libraries/CoreLibrary.sol --- struct ReserveData { /// ... //the liquidation threshold of the reserve. Expressed in percentage (0-100) uint256 liquidationThreshold; /// ... }
originationFee
The origination fee is a one-time, upfront fee charged by the Aave protocol on every new loan (borrow). It's essentially a "borrowing fee" or "loan initiation fee" that is immediately added to the borrower's principal debt.
Key Characteristics:
- Added to Debt: It is not paid separately from the user's wallet. Instead, the fee is minted as additional debt when the loan is taken out.
- If a user borrows 100 DAI and the origination fee is 0.5%, they will immediately owe 100.5 DAI.
- This 0.5 DAI fee is a debt that also accrues interest over time.
- Protocol Revenue: This fee is a primary source of revenue for the Aave protocol and its token holders. The collected fees are sent to a
TokenDistributor
contract, which is responsible for distributing them to stakers of the AAVE token (or LEND token in v1).
- Dynamic Calculation: The fee is not always a fixed percentage. It is calculated by the
FeeProvider
contract, which can adjust the fee based on factors like the asset being borrowed or the overall market conditions.
- Separate from Interest: It's crucial to distinguish the origination fee from the ongoing interest rate (
variableBorrowRate
orstableBorrowRate
). The fee is a one-time cost, while interest is a recurring cost on the entire debt balance (including the fee).
Rate Mode
There are stable and variable rate modes in AAVE V1, and user can only use one mode.
User can switch rate mode.