5/5
## A Deep Dive into Aave V3 Interest Rate Updates: The Supply Scenario This lesson explores the intricate process of how interest rates are updated within the Aave V3 protocol. We'll focus on the specific scenario of a user supplying liquidity, as the fundamental mechanics for interest rate calculations are consistently applied across other core operations like withdrawing liquidity, borrowing, and repaying loans. Understanding this flow is key to grasping how Aave V3 dynamically adjusts to market conditions and ensures fair interest distribution. The journey of a supply operation and its associated interest rate update involves several smart contracts working in concert. Let's break down the sequence step-by-step. ### 1. User Interaction: Initiating Supply via `Pool.sol` The process begins when a user decides to supply assets to an Aave V3 market. They interact with the `Pool.sol` contract by calling the `supply` function. * **Function Call:** `supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)` * **Purpose:** This function serves as the main entry point for users to add liquidity. It takes the asset address, the amount to supply, the address for whom the liquidity is being supplied (often the caller themselves), and an optional referral code. * **Delegation:** The `supply` function itself is lean; it primarily validates inputs and then delegates the core operational logic to the `SupplyLogic.executeSupply` internal library function. ```solidity // Pool.sol function supply( address asset, uint256 amount, address onBehalfOf, // Corrected from 'традиці onBehalfOf' uint16 referralCode ) public virtual override { SupplyLogic.executeSupply( _reserves, _reservesList, _usersConfig[onBehalfOf], DataTypes.ExecuteSupplyParams({ asset: asset, amount: amount, onBehalfOf: onBehalfOf, referralCode: referralCode }) ); } ``` ### 2. Orchestrating the Supply: `SupplyLogic.sol` The `SupplyLogic.executeSupply` function is where the main sequence of events for a supply operation unfolds. This function is responsible for updating the reserve's state, calculating interest, and minting aTokens to the supplier. **Key Steps within `executeSupply`:** 1. **Retrieve Reserve Data:** It first fetches the `DataTypes.ReserveData` struct for the specific `asset` being supplied. This struct holds all critical information about the reserve, including its current interest rates, indexes, and configuration. 2. **Update State (`reserve.updateState`):** This is the first crucial step directly related to interest calculation. Before processing the new supply, the protocol must account for any interest accrued since the last reserve update. `reserve.updateState` (a function within `ReserveLogic.sol`) is called to bring the reserve's interest indexes up to the current block timestamp. 3. **Update Interest Rates and Virtual Balance (`reserve.updateInterestRatesAndVirtualBalance`):** After past interest is accounted for, this function (also in `ReserveLogic.sol`) is invoked. It recalculates the *current* liquidity and borrow interest rates based on the new state of the reserve (which will soon include the supplied amount) and also updates the reserve's virtual balance. The `liquidityAdded` parameter is set to `params.amount` (the amount being supplied), and `liquidityTaken` is `0`. 4. **Mint ATokens:** Finally, the corresponding aTokens, representing the user's share in the liquidity pool, are minted to them. The amount minted is determined by the supplied `amount` and the `nextLiquidityIndex` (which reflects the accumulated interest). ```solidity // SupplyLogic.sol function executeSupply( mapping(address => DataTypes.ReserveData) storage reservesData, // Assuming similar to _reserves mapping(uint256 => address) storage reservesList, // Assuming similar to _reservesList DataTypes.UserConfiguration storage userConfig, // Assuming similar to _usersConfig[onBehalfOf] DataTypes.ExecuteSupplyParams memory params ) external { // Simplified parameters for clarity based on Pool.sol call DataTypes.ReserveData storage reserve = reservesData[params.asset]; DataTypes.ReserveCache memory reserveCache = reserve.cache(); reserve.updateState(reserveCache); // Calls ReserveLogic.updateState to accrue past interest // Other logic like validations, user solvency checks might be here... // This function recalculates current borrow/supply rates AFTER accounting for the new supply's impact reserve.updateInterestRatesAndVirtualBalance(reserveCache, params.asset, params.amount, 0); // liquidityAdded = params.amount, liquidityTaken = 0 // Other logic like updating user configuration... IAToken(reserveCache.aTokenAddress).mint( msg.sender, // Or appropriate caller context params.onBehalfOf, params.amount, reserveCache.nextLiquidityIndex // The liquidity index after updateState ); // Other logic like emitting events... } ``` ### 3. Accruing Past Interest: `ReserveLogic.sol::updateState` The `updateState` function in `ReserveLogic.sol` plays a pivotal role in ensuring that interest is correctly accrued *before* any new actions (like the current supply) modify the reserve's parameters. * **Timestamp Check:** It first checks if the reserve was already updated within the current `block.timestamp`. If so, it returns early to prevent redundant calculations and potential reentrancy issues within the same transaction block. * **Update Indexes:** The core of its work is to call the internal `_updateIndexes` function. This function calculates the interest earned by liquidity providers and accrued by borrowers since the `reserveLastUpdateTimestamp`. * **Accrue to Treasury:** It also calls `_accrueToTreasury` to handle the protocol's share of the interest (though not detailed here). * **Timestamp Update:** Finally, it updates `reserve.lastUpdateTimestamp` to the current `block.timestamp`. ```solidity // ReserveLogic.sol function updateState( DataTypes.ReserveData storage reserve, DataTypes.ReserveCache memory reserveCache ) internal { if (reserveCache.reserveLastUpdateTimestamp == uint40(block.timestamp)) { return; // Skip if already updated in the same block } _updateIndexes(reserve, reserveCache); _accrueToTreasury(reserve, reserveCache); // (Not covered in detail but present) reserve.lastUpdateTimestamp = uint40(block.timestamp); reserveCache.reserveLastUpdateTimestamp = uint40(block.timestamp); } ``` ### 4. Calculating Cumulative Interest: `ReserveLogic.sol::_updateIndexes` The `_updateIndexes` internal function is where the actual calculation of cumulative interest happens. It updates two primary indexes: * **Liquidity Index (`reserve.liquidityIndex`):** This index tracks the cumulative interest earned by all liquidity providers for the asset. It grows over time as interest accrues. * **Calculation:** The interest accrued since the `reserveLastUpdateTimestamp` is calculated using `MathUtils.calculateLinearInterest`. This function takes the `reserveCache.currLiquidityRate` (the rate *before* the current transaction's impact) and the time delta. * `cumulatedLiquidityInterest = MathUtils.calculateLinearInterest(reserveCache.currLiquidityRate, reserveCache.reserveLastUpdateTimestamp)` * The `reserveCache.nextLiquidityIndex` is then computed by multiplying the `reserveCache.currLiquidityIndex` (the index *before* this update) by `(1 + cumulatedLiquidityInterest)`. This is done using `rayMul`, a fixed-point arithmetic function. * `reserveCache.nextLiquidityIndex = cumulatedLiquidityInterest.rayMul(reserveCache.currLiquidityIndex)` * Finally, the storage variable `reserve.liquidityIndex` is updated. * **Variable Borrow Index (`reserve.variableBorrowIndex`):** This index tracks the cumulative interest accrued by users who have borrowed the asset at a variable rate. * **Calculation:** Similar to the liquidity index, but it uses `MathUtils.calculateCompoundedInterest` because variable borrow interest in Aave V3 is compounded per second. * `cumulatedVariableBorrowInterest = MathUtils.calculateCompoundedInterest(reserveCache.currVariableBorrowRate, reserveCache.reserveLastUpdateTimestamp, 안정StableBorrowRateTimestamp)` (The third parameter `안정StableBorrowRateTimestamp` is specific as per the video summary). * The `reserveCache.nextVariableBorrowIndex` is updated by multiplying the `reserveCache.currVariableBorrowIndex` with the newly compounded interest. * `reserve.variableBorrowIndex` is then updated in storage. ```solidity // ReserveLogic.sol (Simplified for liquidity index as per summary) function _updateIndexes( DataTypes.ReserveData storage reserve, DataTypes.ReserveCache memory reserveCache ) internal { // --- Liquidity Index --- if (reserveCache.currLiquidityRate != 0) { uint256 cumulatedLiquidityInterest = MathUtils.calculateLinearInterest( reserveCache.currLiquidityRate, reserveCache.reserveLastUpdateTimestamp ); // nextLiquidityIndex = currLiquidityIndex * (1 + cumulatedInterestRateForPeriod) // rayMul(rate) implies rate is 1 + actual_rate, or it's (1+rate)*index // Assuming cumulatedLiquidityInterest here is (1 + interest_for_period_in_RAY) reserveCache.nextLiquidityIndex = cumulatedLiquidityInterest.rayMul( reserveCache.currLiquidityIndex ); reserve.liquidityIndex = reserveCache.nextLiquidityIndex.toUint128(); } else { // If liquidity rate is 0, index doesn't change reserveCache.nextLiquidityIndex = reserveCache.currLiquidityIndex; } // --- Variable Borrow Index --- // Similar logic applies for the variable borrow index, but it uses // MathUtils.calculateCompoundedInterest with reserveCache.currVariableBorrowRate. // The update would be: // uint256 cumulatedVariableBorrowInterest = MathUtils.calculateCompoundedInterest(...); // reserveCache.nextVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul(reserveCache.currVariableBorrowIndex); // reserve.variableBorrowIndex = reserveCache.nextVariableBorrowIndex.toUint128(); // (Detailed implementation for variable borrow index omitted for brevity but follows this pattern) } ``` ### 5. Calculating New Current Rates: `ReserveLogic.sol::updateInterestRatesAndVirtualBalance` Once past interest has been accrued and indexes updated by `updateState` (via `_updateIndexes`), the `updateInterestRatesAndVirtualBalance` function is called. Its purpose is to determine the *new* instantaneous interest rates that will apply from this point forward, considering the impact of the current operation (e.g., the new liquidity being supplied). * **Call Interest Rate Strategy:** This function calls an external Interest Rate Strategy contract (e.g., `DefaultReserveInterestRateStrategyV2.sol`). This strategy contract implements Aave's specific interest rate model. It's provided with parameters like the total debt, liquidity added/taken in the current transaction, and other reserve-specific data. * **Receive New Rates:** The strategy contract returns the `nextLiquidityRate`, `nextStableRate`, and `nextVariableRate`. * **Update Reserve Rates:** The `reserve.currentLiquidityRate` and `reserve.currentVariableBorrowRate` are updated with these new values. These rates will be used for future interest accrual calculations until the next state update. * **Update Virtual Balance:** If virtual accounting is active for the reserve, `reserve.virtualUnderlyingBalance` is updated. For a supply, `liquidityAdded` increases this balance. This virtual balance helps in interest rate calculations, particularly in reflecting the true "virtual" liquidity available for borrowing. ```solidity // ReserveLogic.sol function updateInterestRatesAndVirtualBalance( DataTypes.ReserveData storage reserve, DataTypes.ReserveCache memory reserveCache, address reserveAddress, // Asset address uint256 liquidityAdded, uint256 liquidityTaken ) internal { // Calculate totalVariableDebt (sum of variable borrows) // uint256 totalVariableDebt = ...; (Logic to get current total variable debt) // uint256 totalStableDebt = ...; (Logic to get current total stable debt) // uint256 totalDebt = totalVariableDebt + totalStableDebt; // Parameters for calculateInterestRates are extensive and include: // unbacked, deficit, liquidityAdded, liquidityTaken, totalVariableDebt, totalStableDebt, // averageStableBorrowRate, reserveFactor, virtualUnderlyingBalance, aTokenAddress, etc. // These are gathered from reserve data and current transaction. DataTypes.CalculateInterestRatesParams memory irParams = DataTypes.CalculateInterestRatesParams({ unbacked: reserve.unbacked + reserve.deficit, // Example, actual may vary liquidityAdded: liquidityAdded, liquidityTaken: liquidityTaken, totalVariableDebt: reserve.totalScaledVariableDebt.rayMul(reserveCache.nextVariableBorrowIndex), // Simplified example totalStableDebt: reserve.totalPrincipalStableDebt, // Simplified example averageStableBorrowRate: reserve.averageStableRate, // Simplified example reserveFactor: reserve.reserveFactor, virtualUnderlyingBalance: reserve.virtualUnderlyingBalance, aTokenAddress: reserveCache.aTokenAddress, reserveAddress: reserveAddress // ... other necessary parameters }); (uint256 nextLiquidityRate, uint256 nextStableRate, uint256 nextVariableRate) = IReserveInterestRateStrategy( reserve.interestRateStrategyAddress ).calculateInterestRates(irParams); reserve.currentLiquidityRate = nextLiquidityRate.toUint128(); reserve.currentVariableBorrowRate = nextVariableRate.toUint128(); // Also update currentStableBorrowRate if applicable, and other rate-related timestamps/info if (reserveCache.reserveConfiguration.getIsVirtualAccActive()) { if (liquidityAdded > 0) { reserve.virtualUnderlyingBalance += liquidityAdded.toUint128(); } if (liquidityTaken > 0) { // This should be safe due to prior checks ensuring sufficient liquidity reserve.virtualUnderlyingBalance -= liquidityTaken.toUint128(); } } } ``` ### 6. The Interest Rate Model: `DefaultReserveInterestRateStrategyV2.sol` The heart of Aave's interest rate determination lies within its interest rate strategy contracts, such as `DefaultReserveInterestRateStrategyV2.sol`. The `calculateInterestRates` function in this contract takes various reserve parameters and calculates the new borrow and supply rates. * **Calculate Utilization Rate:** A key input to the model is the **utilization rate (U)**, which measures how much of the available liquidity is being borrowed. * `vars.borrowUsageRatio = params.totalDebt.rayDiv(availableLiquidityPlusDebt)` * `availableLiquidityPlusDebt` is essentially the total supply (liquidity in the pool + total debt). * A `vars.supplyUsageRatio` is also calculated, which might adjust for `params.unbacked` amounts if applicable. * `vars.supplyUsageRatio = params.totalDebt.rayDiv(availableLiquidityPlusDebt + params.unbacked)` * **Calculate Variable Borrow Rate:** The `currentVariableBorrowRate` is determined based on the `borrowUsageRatio` relative to an `optimalUsageRatio`. Aave employs a piecewise linear model (often visualized with a Desmos graph like `aave-v3-interest-rate-model`): * If `borrowUsageRatio <= optimalUsageRatio`: The rate is calculated using a base rate plus a slope (`variableRateSlope1`). `vars.currentVariableBorrowRate = baseVariableBorrowRate + (borrowUsageRatio * variableRateSlope1 / optimalUsageRatio)` * If `borrowUsageRatio > optimalUsageRatio`: The rate increases more steeply, using `variableRateSlope1` up to the optimal point, and then an additional, steeper slope (`variableRateSlope2`) for utilization beyond optimal. `vars.currentVariableBorrowRate = baseVariableBorrowRate + variableRateSlope1 + (excessBorrowUsageRatio * variableRateSlope2 / RAY)` (where `excessBorrowUsageRatio` is the portion of utilization above optimal, scaled). * **Calculate Liquidity Rate:** The `currentLiquidityRate` (the rate paid to suppliers) is derived from the `currentVariableBorrowRate`. It's essentially the borrow rate multiplied by the utilization, with a portion (the `reserveFactor`) taken by the protocol. `vars.currentLiquidityRate = vars.currentVariableBorrowRate.rayMul(vars.supplyUsageRatio).percentMul(PERCENTAGE_FACTOR - params.reserveFactor)` * `PERCENTAGE_FACTOR` is 100% (e.g., 10000 for basis points). `params.reserveFactor` is the percentage of interest income allocated to the Aave collector. ```solidity // DefaultReserveInterestRateStrategyV2.sol (Conceptual Snippet) // struct RateData { uint optimalUsageRatio; uint baseVariableBorrowRate; uint variableRateSlope1; uint variableRateSlope2; ... } // struct Vars { uint borrowUsageRatio; uint supplyUsageRatio; uint currentVariableBorrowRate; uint currentLiquidityRate; ...} function calculateInterestRates(DataTypes.CalculateInterestRatesParams memory params) // ... returns (uint256, uint256, uint256) // liquidityRate, stableRate, variableRate { RateData memory rateData = _rates[params.reserveAddress]; // Or however specific rate parameters are fetched Vars memory vars; uint256 availableLiquidity = IERC20(params.reserveAddress).balanceOf(params.aTokenAddress) - params.unbacked; // Simplified uint256 availableLiquidityPlusDebt = availableLiquidity + params.totalVariableDebt + params.totalStableDebt; if (availableLiquidityPlusDebt == 0) { // Avoid division by zero // Return base rates or configured minimums return (0, rateData.baseStableBorrowRate, rateData.baseVariableBorrowRate); } vars.borrowUsageRatio = (params.totalVariableDebt + params.totalStableDebt).rayDiv(availableLiquidityPlusDebt); // supplyUsageRatio might be the same or slightly different depending on unbacked asset handling vars.supplyUsageRatio = (params.totalVariableDebt + params.totalStableDebt).rayDiv(availableLiquidityPlusDebt + params.unbacked); if (vars.borrowUsageRatio > rateData.optimalUsageRatio) { uint256 excessUsageRatio = (vars.borrowUsageRatio - rateData.optimalUsageRatio).rayMul(WadRayMath.RAY).rayDiv( WadRayMath.RAY - rateData.optimalUsageRatio // Scale excess to be between 0 and RAY ); vars.currentVariableBorrowRate = rateData.baseVariableBorrowRate + rateData.variableRateSlope1 + rateData.variableRateSlope2.rayMul(excessUsageRatio); } else { vars.currentVariableBorrowRate = rateData.baseVariableBorrowRate + rateData.variableRateSlope1.rayMul(vars.borrowUsageRatio).rayDiv(rateData.optimalUsageRatio); } // Liquidity rate is borrow rate * utilization * (1 - reserveFactor) vars.currentLiquidityRate = vars.currentVariableBorrowRate.rayMul(vars.supplyUsageRatio).percentMul( PercentageMath.PERCENTAGE_FACTOR - params.reserveFactor ); // Stable rate calculation also happens here, typically more complex and considering overall market conditions. uint256 currentStableBorrowRate = /* ... logic for stable rate ... */; return (vars.currentLiquidityRate, currentStableBorrowRate, vars.currentVariableBorrowRate); } ``` ### 7. Minting ATokens: `AToken.sol` and `ScaledBalanceTokenBase.sol` After all interest calculations and rate updates are complete, `SupplyLogic.sol` proceeds to mint aTokens for the user. Aave's aTokens utilize a scaled balance mechanism. * **Scaled Balance:** Instead of storing the raw token amount, aTokens store a "scaled" balance. The actual underlying asset amount an aToken represents is `scaledBalance * currentLiquidityIndex`. When a user supplies, the amount of scaled aTokens they receive is `suppliedAmount / liquidityIndexAtTimeOfSupply`. This design ensures that a user's aToken balance doesn't change frequently with each interest accrual block; instead, the value of each aToken (its corresponding underlying amount) increases. * **`AToken.sol::mint`:** The `mint` function on the AToken contract (which often inherits from or delegates to `ScaledBalanceTokenBase.sol`) handles this. * **`ScaledBalanceTokenBase.sol::_mintScaled`:** This internal function is key: 1. It calculates `amountScaled = amount.rayDiv(index)`, where `amount` is the underlying asset amount supplied by the user, and `index` is the `reserveCache.nextLiquidityIndex` (the liquidity index calculated in `_updateIndexes`, reflecting all interest up to the current block). 2. Before minting the new scaled tokens, it updates the user's state. The logic `balanceIncrease = scaledBalance.rayMul(index).rayDiv(_userState[onBehalfOf].additionalData) - scaledBalance;` calculates the interest accrued on the user's *existing* scaled balance from their last interaction (recorded at `_userState[onBehalfOf].additionalData`) up to the current `index`. This ensures their existing balance is implicitly "topped up" with accrued interest before new tokens are added. 3. It then stores the current `index` in `_userState[onBehalfOf].additionalData`. This `additionalData` (user's last updated index) is crucial. To get a user's true current underlying balance at any time, the protocol can calculate `scaledATokenBalance * currentGlobalLiquidityIndex / userLastUpdatedIndex`. 4. Finally, it calls the underlying ERC20 `_mint` function with the `amountScaled`. ```solidity // ScaledBalanceTokenBase.sol // `index` here is reserveCache.nextLiquidityIndex passed from SupplyLogic function _mintScaled( address caller, // Not explicitly used in this snippet but part of function signature address onBehalfOf, uint256 amount, // Underlying amount being supplied uint256 index // Current liquidity index (nextLiquidityIndex) ) internal returns (bool) { // Returns true if first supply for user uint256 amountScaled = amount.rayDiv(index); if (amountScaled == 0) { // Revert or handle zero scaled amount if amount is non-zero (precision loss) // require(amount == 0, "ZERO_SCALED_AMOUNT"); // Example check } // Update user state BEFORE minting new scaled tokens // This effectively accrues interest on the user's existing balance up to the current `index` // by updating their last known interaction index. uint256 previousUserIndex = _userState[onBehalfOf].additionalData; uint256 scaledBalance = super.balanceOf(onBehalfOf); // ERC20 balanceOf (scaled) bool isFirstSupply = (scaledBalance == 0 && previousUserIndex == 0); // The balanceIncrease calculation, while present, its direct usage for minting isn't shown // in the snippet. The critical part is updating _userState[onBehalfOf].additionalData. // If scaledBalance is not 0, it means the user already has some balance. // Their "real" balance before this mint would be scaledBalance * index / previousUserIndex. // The new scaled tokens are added on top of this. // uint256 balanceIncrease = 0; // if (scaledBalance != 0) { // balanceIncrease = scaledBalance.rayMul(index).rayDiv(previousUserIndex) - scaledBalance; // } _userState[onBehalfOf].additionalData = index.toUint128(); _mint(onBehalfOf, amountScaled.toUint128()); // Mint the new scaled amount // Emit events like MintUnbacked (if applicable), Mint, Transfer // ... return isFirstSupply; } ``` ### Summary of the Interest Update Logic The Aave V3 protocol employs a sophisticated yet logical two-phase process for updating interest rates and balances whenever a relevant action (like supplying liquidity) occurs: 1. **Accumulate Past Interest (`updateState` & `_updateIndexes`):** * The system first calculates all interest that has accrued since the last recorded update. * This is done using the interest rates that were *current* during that past period. * The `liquidityIndex` (for suppliers, linear interest) and `variableBorrowIndex` (for borrowers, compounded interest) are updated to reflect this accumulated interest. This effectively brings the "book value" of all positions up to date *before* the new transaction is factored in. 2. **Calculate New Current Rates & Apply Action (`updateInterestRatesAndVirtualBalance`, then the core action like minting aTokens):** * With the reserve state reflecting all past interest, the protocol then calculates the *new* current interest rates (`currentLiquidityRate`, `currentVariableBorrowRate`). * This calculation, performed by the interest rate strategy contract, considers the current state of the reserve *including the effects of the impending transaction* (e.g., the new liquidity being added). These new rates will govern interest accrual from this point forward until the next update. * The primary action (e.g., supplying assets and minting aTokens) is then executed. For supplies, aTokens are minted based on the supplied amount and the *newly updated* `liquidityIndex` (from step 1). This two-step approach ensures fairness and accuracy: * Users who were in the pool before the current transaction receive interest based on the rates and pool conditions that existed *before* the new transaction. * The new transaction then influences the rates that will apply to everyone *after* its inclusion. This detailed walkthrough of the supply scenario provides a solid foundation for understanding how Aave V3 manages interest rates dynamically, a core component of its decentralized lending and borrowing capabilities. The same fundamental principles of updating indexes first, then recalculating current rates, apply to withdrawals, borrows, and repayments.
This lesson explores the intricate process of how interest rates are updated within the Aave V3 protocol. We'll focus on the specific scenario of a user supplying liquidity, as the fundamental mechanics for interest rate calculations are consistently applied across other core operations like withdrawing liquidity, borrowing, and repaying loans. Understanding this flow is key to grasping how Aave V3 dynamically adjusts to market conditions and ensures fair interest distribution.
The journey of a supply operation and its associated interest rate update involves several smart contracts working in concert. Let's break down the sequence step-by-step.
Pool.sol
The process begins when a user decides to supply assets to an Aave V3 market. They interact with the Pool.sol
contract by calling the supply
function.
Function Call: supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)
Purpose: This function serves as the main entry point for users to add liquidity. It takes the asset address, the amount to supply, the address for whom the liquidity is being supplied (often the caller themselves), and an optional referral code.
Delegation: The supply
function itself is lean; it primarily validates inputs and then delegates the core operational logic to the SupplyLogic.executeSupply
internal library function.
SupplyLogic.sol
The SupplyLogic.executeSupply
function is where the main sequence of events for a supply operation unfolds. This function is responsible for updating the reserve's state, calculating interest, and minting aTokens to the supplier.
Key Steps within executeSupply
:
Retrieve Reserve Data: It first fetches the DataTypes.ReserveData
struct for the specific asset
being supplied. This struct holds all critical information about the reserve, including its current interest rates, indexes, and configuration.
Update State (reserve.updateState
): This is the first crucial step directly related to interest calculation. Before processing the new supply, the protocol must account for any interest accrued since the last reserve update. reserve.updateState
(a function within ReserveLogic.sol
) is called to bring the reserve's interest indexes up to the current block timestamp.
Update Interest Rates and Virtual Balance (reserve.updateInterestRatesAndVirtualBalance
): After past interest is accounted for, this function (also in ReserveLogic.sol
) is invoked. It recalculates the current liquidity and borrow interest rates based on the new state of the reserve (which will soon include the supplied amount) and also updates the reserve's virtual balance. The liquidityAdded
parameter is set to params.amount
(the amount being supplied), and liquidityTaken
is 0
.
Mint ATokens: Finally, the corresponding aTokens, representing the user's share in the liquidity pool, are minted to them. The amount minted is determined by the supplied amount
and the nextLiquidityIndex
(which reflects the accumulated interest).
ReserveLogic.sol::updateState
The updateState
function in ReserveLogic.sol
plays a pivotal role in ensuring that interest is correctly accrued before any new actions (like the current supply) modify the reserve's parameters.
Timestamp Check: It first checks if the reserve was already updated within the current block.timestamp
. If so, it returns early to prevent redundant calculations and potential reentrancy issues within the same transaction block.
Update Indexes: The core of its work is to call the internal _updateIndexes
function. This function calculates the interest earned by liquidity providers and accrued by borrowers since the reserveLastUpdateTimestamp
.
Accrue to Treasury: It also calls _accrueToTreasury
to handle the protocol's share of the interest (though not detailed here).
Timestamp Update: Finally, it updates reserve.lastUpdateTimestamp
to the current block.timestamp
.
ReserveLogic.sol::_updateIndexes
The _updateIndexes
internal function is where the actual calculation of cumulative interest happens. It updates two primary indexes:
Liquidity Index (reserve.liquidityIndex
): This index tracks the cumulative interest earned by all liquidity providers for the asset. It grows over time as interest accrues.
Calculation: The interest accrued since the reserveLastUpdateTimestamp
is calculated using MathUtils.calculateLinearInterest
. This function takes the reserveCache.currLiquidityRate
(the rate before the current transaction's impact) and the time delta.
cumulatedLiquidityInterest = MathUtils.calculateLinearInterest(reserveCache.currLiquidityRate, reserveCache.reserveLastUpdateTimestamp)
The reserveCache.nextLiquidityIndex
is then computed by multiplying the reserveCache.currLiquidityIndex
(the index before this update) by (1 + cumulatedLiquidityInterest)
. This is done using rayMul
, a fixed-point arithmetic function.
reserveCache.nextLiquidityIndex = cumulatedLiquidityInterest.rayMul(reserveCache.currLiquidityIndex)
Finally, the storage variable reserve.liquidityIndex
is updated.
Variable Borrow Index (reserve.variableBorrowIndex
): This index tracks the cumulative interest accrued by users who have borrowed the asset at a variable rate.
Calculation: Similar to the liquidity index, but it uses MathUtils.calculateCompoundedInterest
because variable borrow interest in Aave V3 is compounded per second.
cumulatedVariableBorrowInterest = MathUtils.calculateCompoundedInterest(reserveCache.currVariableBorrowRate, reserveCache.reserveLastUpdateTimestamp, 안정StableBorrowRateTimestamp)
(The third parameter 안정StableBorrowRateTimestamp
is specific as per the video summary).
The reserveCache.nextVariableBorrowIndex
is updated by multiplying the reserveCache.currVariableBorrowIndex
with the newly compounded interest.
reserve.variableBorrowIndex
is then updated in storage.
ReserveLogic.sol::updateInterestRatesAndVirtualBalance
Once past interest has been accrued and indexes updated by updateState
(via _updateIndexes
), the updateInterestRatesAndVirtualBalance
function is called. Its purpose is to determine the new instantaneous interest rates that will apply from this point forward, considering the impact of the current operation (e.g., the new liquidity being supplied).
Call Interest Rate Strategy: This function calls an external Interest Rate Strategy contract (e.g., DefaultReserveInterestRateStrategyV2.sol
). This strategy contract implements Aave's specific interest rate model. It's provided with parameters like the total debt, liquidity added/taken in the current transaction, and other reserve-specific data.
Receive New Rates: The strategy contract returns the nextLiquidityRate
, nextStableRate
, and nextVariableRate
.
Update Reserve Rates: The reserve.currentLiquidityRate
and reserve.currentVariableBorrowRate
are updated with these new values. These rates will be used for future interest accrual calculations until the next state update.
Update Virtual Balance: If virtual accounting is active for the reserve, reserve.virtualUnderlyingBalance
is updated. For a supply, liquidityAdded
increases this balance. This virtual balance helps in interest rate calculations, particularly in reflecting the true "virtual" liquidity available for borrowing.
DefaultReserveInterestRateStrategyV2.sol
The heart of Aave's interest rate determination lies within its interest rate strategy contracts, such as DefaultReserveInterestRateStrategyV2.sol
. The calculateInterestRates
function in this contract takes various reserve parameters and calculates the new borrow and supply rates.
Calculate Utilization Rate: A key input to the model is the utilization rate (U), which measures how much of the available liquidity is being borrowed.
vars.borrowUsageRatio = params.totalDebt.rayDiv(availableLiquidityPlusDebt)
availableLiquidityPlusDebt
is essentially the total supply (liquidity in the pool + total debt).
A vars.supplyUsageRatio
is also calculated, which might adjust for params.unbacked
amounts if applicable.
vars.supplyUsageRatio = params.totalDebt.rayDiv(availableLiquidityPlusDebt + params.unbacked)
Calculate Variable Borrow Rate: The currentVariableBorrowRate
is determined based on the borrowUsageRatio
relative to an optimalUsageRatio
. Aave employs a piecewise linear model (often visualized with a Desmos graph like aave-v3-interest-rate-model
):
If borrowUsageRatio <= optimalUsageRatio
: The rate is calculated using a base rate plus a slope (variableRateSlope1
).
vars.currentVariableBorrowRate = baseVariableBorrowRate + (borrowUsageRatio * variableRateSlope1 / optimalUsageRatio)
If borrowUsageRatio > optimalUsageRatio
: The rate increases more steeply, using variableRateSlope1
up to the optimal point, and then an additional, steeper slope (variableRateSlope2
) for utilization beyond optimal.
vars.currentVariableBorrowRate = baseVariableBorrowRate + variableRateSlope1 + (excessBorrowUsageRatio * variableRateSlope2 / RAY)
(where excessBorrowUsageRatio
is the portion of utilization above optimal, scaled).
Calculate Liquidity Rate: The currentLiquidityRate
(the rate paid to suppliers) is derived from the currentVariableBorrowRate
. It's essentially the borrow rate multiplied by the utilization, with a portion (the reserveFactor
) taken by the protocol.
vars.currentLiquidityRate = vars.currentVariableBorrowRate.rayMul(vars.supplyUsageRatio).percentMul(PERCENTAGE_FACTOR - params.reserveFactor)
PERCENTAGE_FACTOR
is 100% (e.g., 10000 for basis points). params.reserveFactor
is the percentage of interest income allocated to the Aave collector.
AToken.sol
and ScaledBalanceTokenBase.sol
After all interest calculations and rate updates are complete, SupplyLogic.sol
proceeds to mint aTokens for the user. Aave's aTokens utilize a scaled balance mechanism.
Scaled Balance: Instead of storing the raw token amount, aTokens store a "scaled" balance. The actual underlying asset amount an aToken represents is scaledBalance * currentLiquidityIndex
. When a user supplies, the amount of scaled aTokens they receive is suppliedAmount / liquidityIndexAtTimeOfSupply
. This design ensures that a user's aToken balance doesn't change frequently with each interest accrual block; instead, the value of each aToken (its corresponding underlying amount) increases.
AToken.sol::mint
: The mint
function on the AToken contract (which often inherits from or delegates to ScaledBalanceTokenBase.sol
) handles this.
ScaledBalanceTokenBase.sol::_mintScaled
: This internal function is key:
It calculates amountScaled = amount.rayDiv(index)
, where amount
is the underlying asset amount supplied by the user, and index
is the reserveCache.nextLiquidityIndex
(the liquidity index calculated in _updateIndexes
, reflecting all interest up to the current block).
Before minting the new scaled tokens, it updates the user's state. The logic balanceIncrease = scaledBalance.rayMul(index).rayDiv(_userState[onBehalfOf].additionalData) - scaledBalance;
calculates the interest accrued on the user's existing scaled balance from their last interaction (recorded at _userState[onBehalfOf].additionalData
) up to the current index
. This ensures their existing balance is implicitly "topped up" with accrued interest before new tokens are added.
It then stores the current index
in _userState[onBehalfOf].additionalData
. This additionalData
(user's last updated index) is crucial. To get a user's true current underlying balance at any time, the protocol can calculate scaledATokenBalance * currentGlobalLiquidityIndex / userLastUpdatedIndex
.
Finally, it calls the underlying ERC20 _mint
function with the amountScaled
.
The Aave V3 protocol employs a sophisticated yet logical two-phase process for updating interest rates and balances whenever a relevant action (like supplying liquidity) occurs:
Accumulate Past Interest (updateState
& _updateIndexes
):
The system first calculates all interest that has accrued since the last recorded update.
This is done using the interest rates that were current during that past period.
The liquidityIndex
(for suppliers, linear interest) and variableBorrowIndex
(for borrowers, compounded interest) are updated to reflect this accumulated interest. This effectively brings the "book value" of all positions up to date before the new transaction is factored in.
Calculate New Current Rates & Apply Action (updateInterestRatesAndVirtualBalance
, then the core action like minting aTokens):
With the reserve state reflecting all past interest, the protocol then calculates the new current interest rates (currentLiquidityRate
, currentVariableBorrowRate
).
This calculation, performed by the interest rate strategy contract, considers the current state of the reserve including the effects of the impending transaction (e.g., the new liquidity being added). These new rates will govern interest accrual from this point forward until the next update.
The primary action (e.g., supplying assets and minting aTokens) is then executed. For supplies, aTokens are minted based on the supplied amount and the newly updated liquidityIndex
(from step 1).
This two-step approach ensures fairness and accuracy:
Users who were in the pool before the current transaction receive interest based on the rates and pool conditions that existed before the new transaction.
The new transaction then influences the rates that will apply to everyone after its inclusion.
This detailed walkthrough of the supply scenario provides a solid foundation for understanding how Aave V3 manages interest rates dynamically, a core component of its decentralized lending and borrowing capabilities. The same fundamental principles of updating indexes first, then recalculating current rates, apply to withdrawals, borrows, and repayments.
A comprehensive dissection to A Deep Dive into Aave V3 Interest Rate Updates: The Supply Scenario - Uncover Aave V3's meticulous two-phase interest update process initiated by a supply operation, covering past interest accrual and index updates. Witness how new current rates are determined before scaled aTokens are minted to the liquidity provider.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
Aave V3 core functionalities and key features
APY & APR, market forces, utilization rates, interest rate models
Reserves, ATokens, debt tokens, liquidity and borrow index
Aave smart contracts architecture
Supply, borrow, repay, withdrawal, liquidation
Compound interest
Reserve factors, liquidation pricing, credit delegation, LTV
Aave V3 flash loans
How to buy long and sell short using Aave V3
How to build a functional app to buy long, sell short, and close a position.
Last updated on July 25, 2025
Duration: 8min
Duration: 49min
Duration: 43min
Duration: 12min
Course Overview
About the course
Aave V3 core functionalities and key features
APY & APR, market forces, utilization rates, interest rate models
Reserves, ATokens, debt tokens, liquidity and borrow index
Aave smart contracts architecture
Supply, borrow, repay, withdrawal, liquidation
Compound interest
Reserve factors, liquidation pricing, credit delegation, LTV
Aave V3 flash loans
How to buy long and sell short using Aave V3
How to build a functional app to buy long, sell short, and close a position.
Last updated on July 25, 2025