# Understanding Aave V2 Code (3/n) - Borrow Mechanism & Health Factor ## TLDR - **Borrow Flow**: Collateral required -> health factor check -> debt token minted -> interest compounds - **Health Factor**: Ratio of collateral value to debt value, must stay > 1 - **Variable vs Stable**: Two debt token types with different rate behaviors - **Liquidation Trigger**: When health factor < 1, postion becomes liquidatable - **Core Innovation**: Continuous health monitoring via aTokens and debt tokens ## What's Borrowing in Aave V2 Simple explanation: - You deposit collateral (eg. ETH) - Borrow against it (eg. USDC) up to your borrowing power - Pay compound interest on debt - Must maintain health factor >1 ## High-Level Flow User borrows USDC against ETH collateral → Receives debt tokens → Debt grows over time ``` ┌──────────────┐ │ User │ │ (has 10 ETH │ │ deposited) │ └──────┬───────┘ │ 1. borrow(USDC, 5000, VARIABLE) ▼ ┌─────────────────────────────┐ │ LendingPool.sol │ │ │ │ 2. Get price oracle │ │ 3. Calculate ETH value │ │ 4. Validate borrow: │ │ ✓ Reserve active? │ │ ✓ Has collateral? │ │ ✓ Health factor > 1? │ │ ✓ Enough borrowing power?│ │ 5. Update state & rates │ │ 6. Mint debt tokens │ │ 7. Transfer USDC to user │ └─────────────────────────────┘ │ ▼ ┌─────────────────────────────┐ │ VariableDebtToken.sol │ or StableDebtToken.sol │ │ │ 8. Mint scaled debt │ │ 9. Mark user as borrowing │ │10. Debt grows via index │ │ (compound interest) │ └─────────────────────────────┘ │ ▼ ┌─────────────────────────────┐ │ Continuous Monitoring │ │ │ │ Health Factor = (Collateral │ │ × Liq.Threshold) / Debt │ │ │ │ HF > 1.0 ✅ Safe │ │ HF < 1.0 ⚠️ Liquidatable │ └─────────────────────────────┘ ``` **Key Differences from Deposit:** - Requires existing collateral (aTokens) - Price oracle used to value collateral - Health factor validated before and after - Debt tokens minted (instead of aTokens) - Borrowed assets transferred OUT (instead of IN) - Continuous health monitoring (liquidation risk) ## Data Structure ### 1. User Account Data - Aggregated user metrics ```solidity // GenericLogic.sol tracks account health struct CalculateUserAccountDataVars { // pre-reserve variables (used in each loop iteration) uint256 reserveUnitPrice; // asset price in ETH uint256 tokenUnit; // normalized unit uint256 compoundedLiquidityBalance; // user's deposit balance uint256 compoundedBorrowBalance; // user's debt balance uint256 decimals; // token decimals uint256 ltv; // loan-to-value ratio uint256 liquidationThreshold; // liquidation threshold uint256 i; // loop count // accumulator variables (build up across all iterations) uint256 healthFactor; // health factor uint256 totalCollateralInETH; // sum of all collateral value in ETH uint256 totalDebtInETH; // sum of all debt value in ETH uint256 avgLtv; // weighted average LTV uint256 avgLiquidationThreshold; // weighted average liquidation threshold uint256 reservesLength; // total number of reserves in the protocol // state flags bool healthFactorBelowThreshold; // flag indicating if health factor < 1 address currentReserveAddress; // reserve address being processed in current loop bool usageAsCollateralEnabled; // flag indicating if this reserve can be used as collateral bool userUsesReserveAsCollateral; // flag indicating if the user choses to use this reserve as collateral } ``` | Variable | Usage | |----------|----------| | reserveUnitPrice | Convert asset amounts to a common denomination (ETH) for comparison | | tokenUnit | Normalize different token decimals when calculating values | | compoundedLiquidityBalance | Deposit value (incl. accrued interest) = `scaledBalance * currentLiquidityIndex` | | compoundedBorrowBalance | Debt value (incl. accrued interest) = `scaledDebt * currentBorrowIndex` | | decimals | Used with `tokenUnit` for precision calculations | | ltv | Loan-to-value, max borrowing percentage for THIS reserve when used as collateral, stored in basis points | | liquidationThreshold | The percentage at which THIS reserve becomes liquidatable if used as collateral, always higher than ltv | | i | Loop counter, track position in the reserves list iteration | | healthFactor | The final calculated health factor = `(totalCollateralInETH × avgLiquidationThreshold) / totalDebtInETH`, if < 1 position can be liquidated, | | totalCollateralInETH | Sum of all user's collateral across all reserves, valued in ETH = `totalCollateralInETH += balance × price` | | totalDebtInETH | Sum of all user's borrowed amounts across all reserves, valued in ETH = `totalDebtInETH += debt × price`| | avgLtv | Weighted average LTV across all collateral assets, determine available borrowing capacity, `Σ(assetValueETH × assetLTV) / totalCollateralETH` | | avgLiquidationThreshold | Weighted average liquidation threshold across all collateral, determine when liquidation can occur, `Σ(assetValueETH × liquidationThreshold) / totalCollateralETH` | | reservesLength | Total number of reserves in the protocol | | healthFactorBelowThreshold | Set the flag when health factor < 1 | | currentReserveAddress | Address of the reserve being processed in current loop iteration | | usageAsCollateralEnabled | Whether THIS reserve can be used as collateral (protocol-level config) | | userUsesReserveAsCollateral | Whether the USER chose to use THIS reserve as collateral | ### 2. Reserve Data ```solidity // DataTypes.sol struct ReserveData { uint128 variableBorrowIndex; // variable debt multiplier uint128 currentVariableBorrowRate; // variable borrow APY (what borrowers pay) uint128 currentStableBorrowRate; // stable borrow APY (what borrowers pay) // others etc. } ``` ## Math & Key Concepts Aave's borrow mechanism uses several mathematical concepts to ensure safe lending: - Health Factor - Safety metric for liquidation risk - LTV vs Liquidation Threshold - Two different collateral ratios - Borrowing Power - Maximum amount user can borrow - Variable vs Stable Debt - Two interest rate modes - Scaled Debt Balance - Storage-efficient debt representation - Weighted Averages - Handling multi-asset portfolios - Oracle Integration - Real-time price feeds for risk assessment ### 1. Health Factor - The Safety Metric - Definition: Health factor determines if a position can be liquidated. It's the core risk management metric in Aave. - Formula = `(Total collateral * Liquidation threshold) / Total debt` - Code Implementation ```solidity // GenericLogic.sol function calculateHealthFactorFromBalances( uint256 totalCollateralInETH, uint256 totalDebtInETH, uint256 liquidationThreshold ) internal pure returns (uint256) { if (totalDebtInETH == 0) return uint256(-1); // no debt = infinite health return (totalCollateralInETH.percentMul(liquidationThreshold)).wadDiv(totalDebtInETH); } ``` - Interpretation - HF > 1.0: Position is safe (healthy) ✅ - HF = 1.0: Position is at liquidation threshold ⚠️ - HF < 1.0: Position can be liquidated ❌ - HF = uint256(-1): No debt (infinite health) 🎉 - Example - User deposits: - 10 ETH (price: $2,000/ETH) - Total collateral value: $20,000 - ETH liquidation threshold: 80% (8000 in basis points) - User borrows: - 5,000 USDC - Total debt value: $5,000 - Health Factor = ($20,000 × 0.80) / $5,000 = 3.2 ✅ - Safe! User can borrow more or ETH price can drop significantly before liquidation. - When does liquidation occur? Let's see what happens as ETH price drops: | ETH Price | Collateral Value | Health Factor | Status | |-----------|------------------|---------------|-----------------------| | $2,000 | $20,000 | 3.20 | ✅ Very Safe | | $1,500 | $15,000 | 2.40 | ✅ Safe | | $1,000 | $10,000 | 1.60 | ✅ Safe but declining | | $781.25 | $7,812.50 | 1.25 | ⚠️ Risky | | $625 | $6,250 | 1.00 | ⚠️ AT THRESHOLD | | $600 | $6,000 | 0.96 | ❌ LIQUIDATABLE | When HF fails < 1, liquidators can step in to repay debt and seize collateral. ### 2. Loan-to-Value (LTV) & Liquidation Threshold These are two different but related concepts that often confuse users: - **LTV (Loan-to-Value)** - Purpose: Max % user can borrow initially - Conservative limit: Prevents immediate liquidation after borrowing - Used for: Borrowing power calculation - Example: 79% LTV on ETH = can borrow up to 75% of ETH value - **Liquidation Threshold** - Purpose: % at which user get liquidated - Higher than LTV: Create a safety buffer - Used for: Health factor calculation - Example: 80% threshold = liquidated when debt reaches 80% of collateral value - The Safety Buffer ``` Collateral: $20,000 ETH ├──────────────────────────────────────┤ 0% 100% 75% LTV 80% Threshold ↓ ↓ $15,000 $16,000 Max Initial Liquidation Borrow Point ``` - Prevents instant liquidation: If LTV = Liquidation Threshold, borrowing max amount would immediately put you at liquidation risk - Price volatility buffer: Gives borrowers time to react to price movements - Interest accrual buffer: Debt grows over time due to interest, so you need headroom - Protocol safety: Protects against rapid price crashes - Example - User borrows max at 75% LTV - Collateral: $20,000 ETH - Max borrow: $15,000 - Health Factor: ($20,000 × 0.80) / $15,000 = 1.067 ✅ - Buffer: 1.067 - 1 = 0.067 (6.7% price drop before liquidation) ### 3. Borrowing Power - Maximum amount user can borrow - Definition: Borrowing power determines how much user can borrow across all assets. - Formula: ``` Borrowing power = Total collateral value * Average LTV Available to borrow = Borrowing power - Current debt ``` - Example - Single-asset - User deposits 10 ETH (value: $20,000, LTV: 75%) - Borrowing power = 20,000 * 0.75 = $15,000 - Current debt = $0 - Available to borrow = $15,000 - Multiple-asset (Weighted average) - User has: ``` - 10 ETH deposited (value: $20,000, LTV: 75%, Threshold: 80%) - 5,000 DAI deposited (value: $5,000, LTV: 80%, Threshold: 85%) ``` - Total collateral = $25,000 - Weighted average LTV = (20,000 * 0.75 + 5,000 * 0.8) / 25,000 = 0.76 - Borrowing power = 25,000 * 0.76 = $19,000 - Current debt = $5,000 USDC - Available to borrow = 19,000 - 5,000 = $14,000 ### 4. Variable vs Stable Debt - Two interest rate modes Aave offers two types of borrowing interest rates - **Variable Rate Debt** - Characteristics - Interest rate fluctuates based on supply/demand (utilization) - Use `variableBorrowIndex` for auto balance growth - Generally lower rates when utilization is low - Risk: Rates can spike during high utilization period - How it works ```solidity // VariableDebtToken.sol function balanceOf(address user) public view override returns (uint256) { uint256 scaledBalance = super.balanceOf(user); if (scaledBalance == 0) { return 0; } return scaledBalance.rayMul(POOL.getReserveNormalizedVariableDebt(_underlyingAsset)); } ``` - Store only scaled balance (principal) - Actual debt = `scaled balance * current index` - Index grows continuously based on variable rate - Gas efficient, no storage updates for interest accrual - **Stable Rate Debt** - Characteristics - Interest rate locked at time of borrow - No index used - each user has their own rate - Predictable payments, protection from rate spikes - Usually slightly higher than current variable rate - Can be rebalanced in extreme conditions - How it works ```solidity // StableDebtToken.sol function balanceOf(address account) public view virtual override returns (uint256) { uint256 accountBalance = super.balanceOf(account); uint256 stableRate = _usersStableRate[account]; if (accountBalance == 0) { return 0; } uint256 cumulatedInterest = MathUtils.calculateCompoundedInterest( stableRate, _timestamps[account] ); return accountBalance.rayMul(cumulatedInterest); } ``` - Store actual balance (principal + accrued interest) - Each user has their own fixed rate - Interest calculated from timestamp - More gas intensive but predictable payments - **When to Use Each** - Choose Variable If - ✅ Short-term borrowing (days to weeks) - ✅ Expect utilization to stay low - ✅ Want lowest possible rate - ✅ Can handle rate volatility - ✅ Actively manage position - Choose Stable If - ✅ Long-term borrowing (months to years) - ✅ Need predictable payments - ✅ Expect market volatility - ✅ Want rate protection - ✅ Set-and-forget approach - Comparison Table | Feature | Variable Rate | Stable Rate | |---------------|-----------------------------|-------------------------------------| | Rate Behavior | Fluctuates with utilization | Locked at borrow time | | Index Used | `variableBorrowIndex ` | User-specific rate | | Token | `VariableDebtToken ` | `StableDebtToken ` | | Initial Rate | Lower (typically) | Higher (typically) | | Risk | Rate volatility | Locked at suboptimal rate | | Rebalancing | N/A | Can be rebalanced if conditions met | | Best For | Short-term, rate-optimizers | Long-term, risk-averse | | Availability | All assets | Limited assets | ### 5. Scaled Debt Balance - Storage-efficient debt representation - Debt tokens use the same scaled balance architecture as aTokens to enable auto balance growth without per-user storage updates. - Core concepts - `Scaled balance = Actual balance / Current index` - `Actual balance = Scaled balance * Current index` - Why this matters - Only the scaled-balance is stored onchain - The actual balance is calculated on demand - As the index grows, actual balance grows automatically ### 6. Weighted Averages - Handling multi-asset portfolios - When users have multiple types of collateral, Aave calculates weighted averages for LTV and Liquidation Threshold. - Formula: `avgLTV = Σ(assetValueETH × assetLTV) / totalCollateralETH` - Why weighted average? - Different assets have different risk profiles - ETH: LTV 80%, Threshold 82.5% - WBTC: LTV 70%, Threshold 75% - USDC: LTV 80%, Threshold 85% - Simply averaging would be unfair - a user with 95% ETH and 5% WBTC shouldn't have their LTV heavily affected by the small WBTC position. - Example - User collateral: Total collateral $30,000 - 10 ETH (value: $20,000, LTV: 80%) - 5,000 USDC (value: $5,000, LTV: 75%) - 0.25 WBTC (value: $5,000, LTV: 70%) - Weighted LTV - (20,000 × 0.80 + 5,000 × 0.75 + 5,000 × 0.70) / 30,000 = 0.775 (77.5%) - Borrowing power = 30,000 * 0.775 = $23,250 ### 7. Oracle Integration - Real-time price feeds for risk assessment - The price oracle converts all assets to a common denomination (ETH) for risk calculation. - Oracle usage: ```solidity // GenericLogic.sol vars.reserveUnitPrice = IPriceOracleGetter(_oracle).getAssetPrice( vars.currentReserveAddress ); // Calculate collateral value in ETH uint256 collateralValueInETH = vars.compoundedLiquidityBalance .mul(vars.reserveUnitPrice) .div(vars.tokenUnit); ``` - Example - User wants to borrow 5,000 USDC - **Get USDC price from oracle** - USDC price = 0.0005 ETH (when 1 ETH = $2,000) - **Convert amount to ETH value** - amountInETH = (price * amount) / 10^decimals = (0.005e18 * 5000e6) / 1e6 = 2.5 ETH worth - **Check collateral/ health factor**: User has 10 ETH, valued $20,000, liq. threshold 80% - health factor = (20,000 * 0.8) / (0 + 5000) = 3.2 ✅ Safe to borrow - Why ETH denomination in Aave v2? - ETH is native to Ethereum - Simplifies calculation (all prices in one unit) - Reduce oracle dependency (don't need ETH/USDC price) - Oracle failure risk - Stable price feeds -> incorrect health factor - Price manipulation -> unfair liquidation - Network congestion -> delayed updates - Mitigation: Aave uses Chainlink oracles with multiple data sources and freshness checks ## Step by Step Code Walkthrough ### Step 1: Entry Point - `borrow()` ```solidity // LendingPool.sol function borrow( address asset, // the reserve asset to borrow uint256 amount, // amount to borrow uint256 interestRateMode, // 1 = stable, 2 = variable uint16 referralCode, // referral code for integrators, 0 if none address onBehalfOf // who receive the debt (usually msg.sender) ) external override whenNotPaused { // get reference of the asset's current reserve DataTypes.ReserveData storage reserve = _reserves[asset]; _executeBorrow( ExecuteBorrowParams( asset, msg.sender, // user = tx sender (receive borrowed fund) onBehalfOf, // debt recipient amount, interestRateMode, reserve.aTokenAddress, referralCode, true // releaseUnderlying = true (transfer assets to user) ) ); } ``` - **What happens here** - **Modifier check**: `whenNotPaused` ensures the protocol is operational - **Load reserve data**: Fetch storage pointer for the asset's reserve (incl. all config & state) - **Prepare params**: Package all parameters into `ExecuteBorrowParams` struct - **Key details**: `msg.sender` receives the borrowed funds, but `onBehalfOf` receives the debt tokens - Normal case: both are the same address - Credit delegation: `onBehalfOf` can be different in advanced use cases (require allowance) - **Delegate to internal function**: Call `_executeBorrow()` which contains the core logics ### Step 2: Main Borrow Logic - `_executeBorrow()` ```solidity // LendingPool.sol function _executeBorrow(ExecuteBorrowParams memory vars) internal { // Step 1: load storage references DataTypes.ReserveData storage reserve = _reserves[vars.asset]; DataTypes.UserConfigurationMap storage userConfig = _usersConfig[vars.onBehalfOf]; // Step 2: get price oracle address oracle = _addressesProvider.getPriceOracle(); // Step 3: convert borrow amount to ETH value uint256 amountInETH = IPriceOracleGetter(oracle).getAssetPrice(vars.asset).mul(vars.amount).div( 10**reserve.configuration.getDecimals() ); // Step 4: validate the borrow operation ValidationLogic.validateBorrow( vars.asset, reserve, vars.onBehalfOf, vars.amount, amountInETH, vars.interestRateMode, _maxStableRateBorrowSizePercent, _reserves, userConfig, _reservesList, _reservesCount, oracle ); // Step 5: update reserve state (indexes & rates) reserve.updateState(); // Step 6: mint debt tokens (stable or variable) uint256 currentStableRate = 0; bool isFirstBorrowing = false; if (DataTypes.InterestRateMode(vars.interestRateMode) == DataTypes.InterestRateMode.STABLE) { currentStableRate = reserve.currentStableBorrowRate; isFirstBorrowing = IStableDebtToken(reserve.stableDebtTokenAddress).mint( vars.user, vars.onBehalfOf, vars.amount, currentStableRate ); } else { isFirstBorrowing = IVariableDebtToken(reserve.variableDebtTokenAddress).mint( vars.user, vars.onBehalfOf, vars.amount, reserve.variableBorrowIndex ); } // Step 7: update user config if first borrow if (isFirstBorrowing) { userConfig.setBorrowing(reserve.id, true); } // Step 8: update interest rates after borrow reserve.updateInterestRates( vars.asset, vars.aTokenAddress, 0, vars.releaseUnderlying ? vars.amount : 0 ); // Step 9: transfer borrowed assets to user if (vars.releaseUnderlying) { IAToken(vars.aTokenAddress).transferUnderlyingTo(vars.user, vars.amount); } // Step 10: Emit borrow event emit Borrow( vars.asset, vars.user, vars.onBehalfOf, vars.amount, vars.interestRateMode, DataTypes.InterestRateMode(vars.interestRateMode) == DataTypes.InterestRateMode.STABLE ? currentStableRate : reserve.currentVariableBorrowRate, vars.referralCode ); } ``` Let's break down each step in details #### 2.0 ExecuteBorrowParams Struct ```solidity struct ExecuteBorrowParams { address asset; // address of the reserve asset to borrow address user; // the borrow transaction initiator address onBehalfOf; // address that will receive the debt uint256 amount; // amount to borrow uint256 interestRateMode; // 1 = Stable, 2 = Variable address aTokenAddress; // address of aToken for this reserve uint16 referralCode; // referral code for integrators (for tracking/ incentives) bool releaseUnderlying; // whether to transfer the borrowed asset to the user } ``` - `asset`: The underlying token address being borrowed (eg. USDC, DAI, WETH) - `user`: The msg.sender who initiates the borrow call - the transaction initiator - `onBehalfOf`: The address that will incur the debt. This allows delegation - someone can borrow on behalf of another address (requires allowance) - `amount`: The quantity of tokens to borrow, in the asset's native decimals - `interestRateMode`: - 1 = Stable rate (fixed for a period) - 2 = Variable rate (changes with market conditions) - `aTokenAddress`: Used for balance checks adn collateral validation - `referralCode`: To track which integrator/ frontend referred the user (used for incentive distribution) - `releaseUnderlying`: - `true`: Transfer borrowed tokens to user (normal borrow) - `false`: Don't transfer (used for certain internal operations like flash loans) #### 2.1 Load Storage References ```solidity DataTypes.ReserveData storage reserve = _reserves[vars.asset]; DataTypes.UserConfigurationMap storage userConfig = _usersConfig[vars.onBehalfOf]; ``` - **Purpose**: Get direct storage pointers to avoid multiple lookups - `reserve`: Contain all state & config for the asset, incl. interest rates, indexes, debt token addresses, aToken address, etc. - `userConfig`: Bitmap tracking which reserves user is using as collateral/ borrowing #### 2.2 Get Price Oracle ```solidity address oracle = _addressesProvider.getPriceOracle(); // configuration/LendingPoolAddressesProvider.sol function getPriceOracle() external view override returns (address) { return getAddress(PRICE_ORACLE); } ``` - **Purpose**: Fetch the oracle contract address for price feeds - **Why needed**: Must convert all assets to ETH for risk calculations - **Oracle source**: Primarily use Chainlink price feeds with fallback mechanism - Details in misc/AaveOracle.sol and interface/IChainlinkAggregator.sol #### 2.3 Convert Borrow Amount to ETH ```solidity uint256 amountInETH = // type casting the oracle address to the interface IPriceOracleGetter(oracle). // call the oracle, return price of 1 unit of the asset in ETH getAssetPrice(vars.asset). // multiple by the amount being borrowed mul(vars.amount). // divide by the token's decimal to normalize div(10**reserve.configuration.getDecimals()); ``` - **Purpose**: Calculate ETH-denominated value of borrowed amount - **Why needed**: - All health factor and collateral calculations use ETH as base unit - Simplifies multi-asset portfolio calculations #### 2.4 Validate Borrow Operation ```solidity ValidationLogic.validateBorrow( vars.asset, reserve, vars.onBehalfOf, vars.amount, amountInETH, vars.interestRateMode, _maxStableRateBorrowSizePercent, _reserves, userConfig, _reservesList, _reservesCount, oracle ); // ValidationLogic.sol struct ValidateBorrowLocalVars { uint256 currentLtv; // Loan-to-Value ratio uint256 currentLiquidationThreshold; // Liquidation threshold uint256 amountOfCollateralNeededETH; // Required collateral uint256 userCollateralBalanceETH; // User's total collateral uint256 userBorrowBalanceETH; // User's total borrows uint256 availableLiquidity; // Available liquidity in pool uint256 healthFactor; // User's health factor bool isActive; // Is reserve active? bool isFrozen; // Is reserve frozen? bool borrowingEnabled; // Is borrowing enabled? bool stableRateBorrowingEnabled; // Is stable rate enabled? } // ValidationLogic.sol function validateBorrow( address asset, // asset being borrowed DataTypes.ReserveData storage reserve, // reserve info for this asset address userAddress, // borrower's address uint256 amount, // amount to borrow (in token units) uint256 amountInETH, // amount in ETH uint256 interestRateMode, // 1 = Stable, 2 = Variable uint256 maxStableLoanPercent, // max % of liquidity for stable borrow mapping(address => DataTypes.ReserveData) storage reservesData, // all reserves info DataTypes.UserConfigurationMap storage userConfig, // user config mapping(uint256 => address) storage reserves, // reserve list uint256 reservesCount, // total nb of reserves address oracle // price oracle address ) external view { ValidateBorrowLocalVars memory vars; // get the reserve's config flags from the bitmap (vars.isActive, vars.isFrozen, vars.borrowingEnabled, vars.stableRateBorrowingEnabled) = reserve .configuration .getFlags(); // Step 1: reserve status checks // reserve must be active, not frozen, not 0, borrowing enabled require(vars.isActive, Errors.VL_NO_ACTIVE_RESERVE); require(!vars.isFrozen, Errors.VL_RESERVE_FROZEN); require(amount != 0, Errors.VL_INVALID_AMOUNT); require(vars.borrowingEnabled, Errors.VL_BORROWING_NOT_ENABLED); // Step 2: validate interest rate mode // must be 1 (STABLE) or 2 (VARIABLE) require( uint256(DataTypes.InterestRateMode.VARIABLE) == interestRateMode || uint256(DataTypes.InterestRateMode.STABLE) == interestRateMode, Errors.VL_INVALID_INTEREST_RATE_MODE_SELECTED ); // Step 3: user account health check ( vars.userCollateralBalanceETH, // total value of user's collateral in ETH vars.userBorrowBalanceETH, // total value of user's existing borrowing in ETH vars.currentLtv, // weighted average LTV (max borrow ratio) vars.currentLiquidationThreshold, // current liq. threshold vars.healthFactor // health factor (must > 1) ) = GenericLogic.calculateUserAccountData( userAddress, reservesData, // all reserves in protocol userConfig, // user's positions bitmap reserves, // reserve list reservesCount, // total reserves count oracle // price oracle ); // Step 4: check collateral & health factor // user collateral balance in ETH must > 0 require(vars.userCollateralBalanceETH > 0, Errors.VL_COLLATERAL_BALANCE_IS_0); // health factor must > 1 require( vars.healthFactor > GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD, Errors.VL_HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD ); // Step 5: check if user has enough collateral for new borrow // formula: collateral needed = (existing borrow + new borrow) / LTV // add the current already borrowed amount to the amount requested to calculate the total collateral needed. vars.amountOfCollateralNeededETH = vars.userBorrowBalanceETH.add(amountInETH).percentDiv( vars.currentLtv ); //LTV is calculated in percentage // amount of collateral needed should <= total value of user's collateral require( vars.amountOfCollateralNeededETH <= vars.userCollateralBalanceETH, Errors.VL_COLLATERAL_CANNOT_COVER_NEW_BORROW ); // Step 6: 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 portion of the total available liquidity **/ if (interestRateMode == uint256(DataTypes.InterestRateMode.STABLE)) { // check if the borrow mode is stable and if stable rate borrowing is enabled on this reserve require(vars.stableRateBorrowingEnabled, Errors.VL_STABLE_BORROWING_NOT_ENABLED); // prevent borrowing same asset the user is using as collateral require( !userConfig.isUsingAsCollateral(reserve.id) || // user is NOT using this asset as collateral reserve.configuration.getLtv() == 0 || // or the asset has LTV = 0 (can't be collateral) amount > IERC20(reserve.aTokenAddress).balanceOf(userAddress), // or borrow amount > user's aToken balance Errors.VL_COLLATERAL_SAME_AS_BORROWING_CURRENCY ); // get the current available liquidity vars.availableLiquidity = IERC20(asset).balanceOf(reserve.aTokenAddress); // max stable loan size = available liquidity * max stable loan percent uint256 maxLoanSizeStable = vars.availableLiquidity.percentMul(maxStableLoanPercent); // limit stable rate borrow to % of liquidity require(amount <= maxLoanSizeStable, Errors.VL_AMOUNT_BIGGER_THAN_MAX_LOAN_SIZE_STABLE); } } ``` - Summary of all checks | # | Check | Purpose | |-----|--------------------------------------|-----------------------------------| | 1 | Reserve is active | Ensure reserve is operational | | 2 | Reserve not frozen | Not in emergency mode | | 3 | Amount > 0 | Valid borrow amount | | 4 | Borrowing enabled | Reserve allows borrowing | | 5 | Valid interest rate mode | 1 (stable) or 2 (variable) | | 6 | User has collateral | Can't borrow without deposits | | 7 | Health factor > 1 | User not at liquidation risk | | 8 | Enough collateral for total borrows | LTV check | | 9 | (Stable only) Stable rate enabled | Reserve permits stable borrowing | | 10 | (Stable only) Not same asset | Prevent collateral = borrow abuse | | 11 | (Stable only) Within liquidity limit | Max 25% of pool at stable rate | - Dive deeper into `calculateUserAccountData()` - Key of Aave's risk engine - It answers the fundamental question: "Can the user afford this borrow?" - What it does - Cross-asset analysis: Only function that looks at user's ENTIRE portfolio across ALL reserves - Real-time solvency: Calculate current collateral value and debt using live oracle prices - Risk metrics: Computes the 5 key metrics that determine if borrow is safe: 1. Total collateral in ETH 2. Total existing debt in ETH 3. Weighted average LTV (max borrow capacity) 4. Weighted liquidation threshold (liquidation point) 5. Health factor (solvency measure, must > 1) - All subsequent validation checks depend on its output ```solidity function calculateUserAccountData( address user, mapping(address => DataTypes.ReserveData) storage reservesData, DataTypes.UserConfigurationMap memory userConfig, mapping(uint256 => address) storage reserves, uint256 reservesCount, address oracle ) internal view returns ( uint256, // totalCollateralinETH uint256, // totalDebtInETH uint256, // avgLTM uint256, // avgLiquidationThreshold uint256 // healthFactor ) { // step 1: create a local variable struct in memory to hold intermediate calculation values CalculateUserAccountDataVars memory vars; // step 2: early exist for empty accounts if (userConfig.isEmpty()) { return (0, 0, 0, 0, uint256(-1)); } // step 3: loop through all reserves for (vars.i = 0; vars.i < reservesCount; vars.i++) { // step 3.1: skip unsed reserves if (!userConfig.isUsingAsCollateralOrBorrowing(vars.i)) { continue; } // step 3.2: get info of THIS asset vars.currentReserveAddress = reserves[vars.i]; DataTypes.ReserveData storage currentReserve = reservesData[vars.currentReserveAddress]; (vars.ltv, vars.liquidationThreshold, , vars.decimals, ) = currentReserve .configuration .getParams(); vars.tokenUnit = 10**vars.decimals; // step 3.3: get price for THIS asset vars.reserveUnitPrice = IPriceOracleGetter(oracle).getAssetPrice(vars.currentReserveAddress); // step 3.4: calculate collateral if (vars.liquidationThreshold != 0 && userConfig.isUsingAsCollateral(vars.i)) { // get user's aToken balance vars.compoundedLiquidityBalance = IERC20(currentReserve.aTokenAddress).balanceOf(user); // convert to ETH value uint256 liquidityBalanceETH = vars.reserveUnitPrice.mul(vars.compoundedLiquidityBalance).div(vars.tokenUnit); // accumulate total collateral amount vars.totalCollateralInETH = vars.totalCollateralInETH.add(liquidityBalanceETH); // weight the LTV by collateral value vars.avgLtv = vars.avgLtv.add(liquidityBalanceETH.mul(vars.ltv)); // weight the liquidation threshold vars.avgLiquidationThreshold = vars.avgLiquidationThreshold.add( liquidityBalanceETH.mul(vars.liquidationThreshold) ); } // step 3.5: calculate borrowing if (userConfig.isBorrowing(vars.i)) { // get stasble debt balance vars.compoundedBorrowBalance = IERC20(currentReserve.stableDebtTokenAddress).balanceOf( user ); // add variable debt balance vars.compoundedBorrowBalance = vars.compoundedBorrowBalance.add( IERC20(currentReserve.variableDebtTokenAddress).balanceOf(user) ); // convert to ETH value and accumulate vars.totalDebtInETH = vars.totalDebtInETH.add( vars.reserveUnitPrice.mul(vars.compoundedBorrowBalance).div(vars.tokenUnit) ); } } // step 4: finalize weighted averages calculation // avgLtv = Σ(assetValueETH × assetLTV) / totalCollateralETH vars.avgLtv = vars.totalCollateralInETH > 0 ? vars.avgLtv.div(vars.totalCollateralInETH) : 0; // avgLiquidationThreshold = Σ(assetValueETH × liquidationThreshold) / totalCollateralETH vars.avgLiquidationThreshold = vars.totalCollateralInETH > 0 ? vars.avgLiquidationThreshold.div(vars.totalCollateralInETH) : 0; // step 5: calculate health factor // HF = (collateral * liq. threshold) / total debt vars.healthFactor = calculateHealthFactorFromBalances( vars.totalCollateralInETH, vars.totalDebtInETH, vars.avgLiquidationThreshold ); // return ad value return ( vars.totalCollateralInETH, vars.totalDebtInETH, vars.avgLtv, vars.avgLiquidationThreshold, vars.healthFactor ); } ``` - Why `amount > aToken balance` allows the stable rate borrowing? - The full check is an OR condition - the check allows stable rate borrow if ANY is true - It fails when ALL conditions are false - User IS using same asset as collateral, AND - Asset has LTV > 0, AND - Borrow amount <= user's aToken balance - The attack being prevented - User deposits 10,000 USDC -> Get 10,000 aUSDC ✅ - LTV 80% > 0 ✅ - User tries to borrow 8,000 USDC at stable rate, 8,000 < 10,000 aUSDC ❌ - Key insight: This indicates a pure same-asset circular borrowing - Legitimate operation - User deposits 10,000 USDC -> Get 10,000 aUSDC ✅ - LTV 80% > 0 ✅ - User tries to borrow 20,000 USDC at stable rate, 20,000 > 10,000 aUSDC ✅ - Key insight: If you're borrowing MORE of an asset than you deposited, you must have other collateral backing it. This indicates a legitimate diverse collateral strategy, not a same-asset rate arbitrage. - Why stable rate borrowing needs to be cap at certin % of liquidity - It's a risk management strategy from the protocol perspective - Stable rate borrowing contains interest rate risk for the protocol - Example - Time T0: - Pool has 1,000,000 USDC - Market rate: 3% APR - User borrows 800,000 USDC at stable 3.5% APR - Time T1: - Market rate spikes to 15% APR (DeFi summer, high demand) - User still pays only 3.5% APR (stable rate locked in) - Protocol must pay depositors ~15% to keep liquidity - Protocol pays OUT 15% but receives IN 3.5% -> lose money! 💸 - When cap at certain %, the risk is protected - Why not just offer 100% variable rate borrowing in Aave v2? - User demand: Some users need predictability (businesses, long-term planning) - Competitive advantage: Offers something other protocols don't - Premium pricing: Stable rates are typically 0.5-2% higher than variable - Risk is manageable: At 25% cap for example, the risk is acceptable #### 2.5 Update Reserve State ```solidity reserve.updateState(); ``` - **Purpose**: Updated liquidity & variable borrow index - **What it does**: - Calculate time elapsed since last update - Compound interest based on current rates - Update `liquidityIndex` for aToken balance growth - Update `variableBorrowIndex` for variable debt growth - Detailed function walkthrough has been covered in the previous [deposit blog](https://hackmd.io/@ChloeIsCoding/r1XOmagfWg) #### 2.6 Mint Debt Tokens Two paths depending on interest rate mode: - **Variable Rate Path** ```solidity isFirstBorrowing = IVariableDebtToken(reserve.variableDebtTokenAddress).mint( vars.user, vars.onBehalfOf, vars.amount, reserve.variableBorrowIndex ); // tokenization/VariableDebtToken.sol function mint( address user, address onBehalfOf, uint256 amount, uint256 index ) external override onlyLendingPool returns (bool) { // return bool indicating if it's 1st borrow // delegation check if (user != onBehalfOf) { _decreaseBorrowAllowance(onBehalfOf, user, amount); } // check previous balance, used to determine if it's 1st borrow uint256 previousBalance = super.balanceOf(onBehalfOf); // calculate the scaled balance (principal) uint256 amountScaled = amount.rayDiv(index); require(amountScaled != 0, Errors.CT_INVALID_MINT_AMOUNT); // mint scaled amount to onBehalfOf _mint(onBehalfOf, amountScaled); // emit events emit Transfer(address(0), onBehalfOf, amount); emit Mint(user, onBehalfOf, amount, index); // return 1st borrow flag return previousBalance == 0; } ``` - **Mint**: Variable debt tokens to `onBehalfOf` - **Scaled balance**: `scaledAmount = amount / variableBorrowIndex` - **Auto growth**: Balance increases as index grows - **Stable Rate Path** ```solidity if (DataTypes.InterestRateMode(vars.interestRateMode) == DataTypes.InterestRateMode.STABLE) { currentStableRate = reserve.currentStableBorrowRate; isFirstBorrowing = IStableDebtToken(reserve.stableDebtTokenAddress).mint( vars.user, vars.onBehalfOf, vars.amount, currentStableRate ); } // tokenization/StableDebtToken.sol function mint( address user, address onBehalfOf, uint256 amount, uint256 rate ) external override onlyLendingPool returns (bool) { // create local variables MintLocalVars memory vars; // check credit delegation if (user != onBehalfOf) { _decreaseBorrowAllowance(onBehalfOf, user, amount); } // calculate accrued interest // currentBalance: current debt incl. accrued interest // balanceIncrease: interest that has accumulated (, uint256 currentBalance, uint256 balanceIncrease) = _calculateBalanceIncrease(onBehalfOf); // update total debt supply vars.previousSupply = totalSupply(); // get current total debt across all users vars.currentAvgStableRate = _avgStableRate; // get protocol's avg stabe rate across all users vars.nextSupply = _totalSupply = vars.previousSupply.add(amount); // calculate new total supply AND update storage // convert to ray precision vars.amountInRay = amount.wadToRay(); // calculate user's new weighted average stable rate // new rate = (old rate * old balance + new rate * new amount) / (old balance + new amount) vars.newStableRate = _usersStableRate[onBehalfOf] .rayMul(currentBalance.wadToRay()) .add(vars.amountInRay.rayMul(rate)) .rayDiv(currentBalance.add(amount).wadToRay()); require(vars.newStableRate <= type(uint128).max, Errors.SDT_STABLE_DEBT_OVERFLOW); // update the user's stable rate _usersStableRate[onBehalfOf] = vars.newStableRate; // update timestamps //solium-disable-next-line _totalSupplyTimestamp = _timestamps[onBehalfOf] = uint40(block.timestamp); // Calculates the updated average stable rate for the protocol // new rate = (old rate * old balance + new rate * new amount) / new balance vars.currentAvgStableRate = _avgStableRate = vars .currentAvgStableRate .rayMul(vars.previousSupply.wadToRay()) .add(rate.rayMul(vars.amountInRay)) .rayDiv(vars.nextSupply.wadToRay()); // mint the debt tokens: new borrow + accrued interest _mint(onBehalfOf, amount.add(balanceIncrease), vars.previousSupply); // emit events emit Transfer(address(0), onBehalfOf, amount); emit Mint( user, onBehalfOf, amount, currentBalance, balanceIncrease, vars.newStableRate, vars.currentAvgStableRate, vars.nextSupply ); // return 1st borrow flag return currentBalance == 0; } ``` - **Mint**: Stable debt tokens to `onBehalfOf` - **Actual balance**: Mints amount + accruedInterest to capture unpaid interest into balance (unlike variable which uses scaled balance) - **Per-User Rate**: Each user has own fixed rate stored on-chain (can't use global index like variable) - **Weighted average rate**: When user borrows more, new rate = weighted average of old rate × old balance + new rate × new amount - **Protocol tracking**: Maintain global avg. stable rate across all users for depositor yield calculation - Key difference: Stable vs Variable Debt Token | Aspect | VariableDebtToken | StableDebtToken | |--------------|-----------------------------------|----------------------------------| | Rate | Uses index (changes) | Uses fixed rate | | Balance | Scaled by index | Grows by time × rate | | Storged balance | Scaled balance (principal) | Actual balance (principal + interest) | | User rate | Same for all users | Each user has own rate | | Complexity | Simple | Complex (weighted avg) | | Mint params | (user, onBehalfOf, amount, index) | (user, onBehalfOf, amount, rate) | | Rate updates | Automatic via index | Manual recalculation | | Timestamps | Not tracked | Tracked per user | #### 2.7 Update User Config (If 1st borrow) ```solidity if (isFirstBorrowing) { userConfig.setBorrowing(reserve.id, true); } // UserConfiguration.sol function setBorrowing( DataTypes.UserConfigurationMap storage self, uint256 reserveIndex, // which reserve bool borrowing // true = set bit, false = clear bit ) internal { // need to be < 128 as userconfig can only store max 128 reserves require(reserveIndex < 128, Errors.UL_INVALID_INDEX); // calculate bit position self.data = (self.data & ~(1 << (reserveIndex * 2))) | (uint256(borrowing ? 1 : 0) << (reserveIndex * 2)); } ``` - **Purpose**: Mark this reserve as "borrowed" in user's bitmap - **Bitmap structure**: bit 0 = collateral flag, bit 1 = borrowing flag #### 2.8 Update Interest Rates After Borrowing ```solidity reserve.updateInterestRates( vars.asset, // reserve address vars.aTokenAddress, // aToken address 0, // liquidity added vars.releaseUnderlying ? vars.amount : 0 // liquidity taken ); ``` - **Purpose**: Recalculate interest rates based on new utilization - **What changes** - Available liquidity decreases by borrowed amount - Utilization rate increases due to borrowing - Variable & stable borrow rate adjusts based on new utilization - Deposit rate adjusts (depositors earn more as utilization increases) #### 2.9 Transfer Borrowed Assets To User ```solidity if (vars.releaseUnderlying) { IAToken(vars.aTokenAddress).transferUnderlyingTo(vars.user, vars.amount); } // AToken.sol function transferUnderlyingTo(address target, uint256 amount) external override onlyLendingPool // only lendingPool can call returns (uint256) { IERC20(_underlyingAsset).safeTransfer(target, amount); return amount; // the amount transferred } ``` - **Purpose**: Send borrowed assets to the user address - **How it works**: - aToken contract holds underlying assets (eg. USDC, DAI, etc.) - aToken transfers underlying from its balance to user address - aToken total supply doesn't change (as it represents deposits, not liquidity) #### 2.10 Emit Borrow Event ```solidity emit Borrow( vars.asset, // borrowed asset (indexed) vars.user, // who initiated the transaction vars.onBehalfOf, // who received the debt (indexed) vars.amount, // borrowed amount vars.interestRateMode, // stable or variable DataTypes.InterestRateMode(vars.interestRateMode) == DataTypes.InterestRateMode.STABLE ? currentStableRate : reserve.currentVariableBorrowRate, // stable or variable current rate vars.referralCode // partner referral code (indexed) ); ``` - **Purpose**: Log borrow for offchain indexing and analytics - **Used by**: Aave UI, analytics platform, liquidation bots etc. ## Key Insights ### Risk Management Architecture - **Defense in depth**: Multi-layered validation protects protocol at every stage - Pre-transaction validation (checks in `validateBorrow()` - fail fast before state changes) - Post-transaction health checks (health factor must remain > 1) - Continuous monitoring via debt tokens (balances auto-update without gas costs) - LTV/Threshold buffer zones (safety gap prevents instant liquidation) - **Price oracle dependency**: All risk calculations flow through oracle prices - Every borrow converts amounts to ETH for unified risk assessment - Oracle failure = protocol risk (mitigated via Chainlink multi-source feeds) - Single point of failure for health factor calculations ### Gas-Efficient Design Patterns - **Scaled balance architecture**: Core innovation enabling passive interest accrual for variable rate borrow - Only principal stored on-chain as `scaledBalance = amount / index` - Actual balance calculated on-demand: `balance = scaledBalance * currentIndex` - Zero gas cost for interest compounding across millions of users - Same pattern used in both aTokens (deposits) and variable debt tokens (borrows) ### Multi-Asset Complexity - **Weighted average calculations**: Protocol handles diverse collateral portfolios - Each asset has different risk parameters (ETH: 80% LTV, WBTC: 70% LTV) - Weighted by collateral value, not simple arithmetic average - Formula: `avgLTV = Σ(assetValueETH × assetLTV) / totalCollateralETH` - Ensures fair treatment regardless of portfolio composition - **Cross-reserve calculations**: `calculateUserAccountData()` is the key risk engine - Only function that analyzes user's ENTIRE portfolio - Loops through all reserves to compute 5 critical metrics - Called on every borrow to ensure global solvency ### Two-Rate System Trade-offs - **Variable rate**: Simple, capital-efficient, protocol-favored - Shared index across all users (gas-efficient) - Risk born by borrowers (rate volatility) - Automatically adjusts to market conditions - **Stable rate**: Complex, capital-inefficient, now deprecated - Per-user rate storage (expensive) - Risk borne by protocol (rate arbitrage exposure) - Required liquidity caps (max 25%) to limit protocol losses - **Historical note**: Fully deprecated in Jan 2024 after Nov 2023 security issue - **Rate updates cascade**: Every borrow/repay recalculates rates - Changes utilization → triggers interest rate model - Affects ALL users in the pool (not just borrower) - Real-time supply/demand mechanism ### Implementation Insights - **Credit delegation pattern**: `user` ≠ `onBehalfOf` enables advanced strategies - User initiates transaction and receives borrowed funds - OnBehalfOf incurs the debt (requires allowance) - Unlocks institutional use cases (treasuries, margin trading) - **Reserve state updates**: Every operation follows strict sequence - Update state (compound indexes) BEFORE minting debt tokens - Mint debt tokens BEFORE updating rates - Update rates AFTER liquidity changes - Order matters for consistency and prevents manipulation ## Reference - Aave v2 whitepaper: https://github.com/aave/protocol-v2/blob/master/aave-v2-whitepaper.pdf - Aave v2 repo: https://github.com/aave/protocol-v2 ## Discussion - Twitter: [@chloe_zhuX](https://x.com/Chloe_zhuX) - Telegram: [@Chloe_zhu](https://t.me/chloe_zhu) - GitHub: [@Chloezhu010](https://github.com/Chloezhu010) --- *Last updated: Dec 17th, 2025* *Part of my #LearnInPublic Defi series*