# 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*