# Understanding Aave V2 Code (4/n) - Withdraw Mechanism
## TLDR
- **Withdraw Flow**: Burn aToken -> Get underlying asset back
- **Critical Constraint**: Health factor > 1.0
- **Two Scenarios**: Full withdraw (no borrows) vs Partial withdraw (has borrows)
## What's Withdraw in Aave V2
Withdraw is the reverse operation of deposit:
- **Deposit**: Send USDC → Receive aUSDC (earning interest)
- **Withdraw**: Send aUSDC → Receive USDC (stop earning)
- But if you have borrowed assets, you **cannot freely withdraw** your collateral. Aave enforces a health factor check to ensure the protocol stays solvent.
## High-Level Flow
User withdraws USDC → Burns aUSDC → Receives underlying USDC
```
┌──────────────┐
│ User │
│ (has 10,000 │
│ aUSDC) │
└──────┬───────┘
│ 1. withdraw(USDC, 5000, user)
▼
┌─────────────────────────────┐
│ LendingPool.sol │
│ │
│ 2. Get reserve data │
│ 3. Get user aToken balance │
│ 4. Validate withdraw: │
│ ✓ Reserve active? │
│ ✓ Amount > 0? │
│ ✓ Sufficient balance? │
│ ✓ Has borrows? │
│ → YES: Check HF > 1 │
│ → NO: Skip HF check │
│ 5. Update state & rates │
│ 6. Update user config │
│ 7. Burn aTokens │
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ AToken.sol │
│ │
│ 8. Burn scaled amount │
│ 9. Transfer underlying │
│ USDC to user │
└─────────────────────────────┘
```
## Two Critical Scenarios
### Scenario 1: Withdraw WITHOUT Borrows (Simple)
```
User has:
- 10,000 aUSDC (collateral)
- 0 debt
Withdraw 5,000 USDC:
✅ No health factor check needed
✅ Instant withdrawal
✅ Remaining 5,000 aUSDC still earns interest
```
### Scenario 2: Withdraw WITH Borrows (Health Factor Check Required)
```
User has:
- 10 ETH collateral ($20,000 value)
- 5,000 USDC borrowed
- Health Factor: 3.2
Tries to withdraw 5 ETH:
1. Calculate new collateral: 10 - 5 = 5 ETH ($10,000)
2. Calculate new HF: ($10,000 × 0.80) / $5,000 = 1.6 ✅
3. HF > 1.0 → Withdrawal allowed
Tries to withdraw 8 ETH:
1. Calculate new collateral: 2 ETH ($4,000)
2. Calculate new HF: ($4,000 × 0.80) / $5,000 = 0.64 ❌
3. HF < 1.0 → Withdrawal BLOCKED
(Would make position liquidatable)
```
### Key Differences from Deposit & Borrow
| Operation | Direction | Token Flow | HF Check | Can Fail? |
|-----------|-----------|------------|----------|-----------|
| **Deposit** | Assets IN | Underlying → aToken (mint) | No | Rarely (only if reserve frozen) |
| **Borrow** | Assets OUT | Debt token (mint) → Underlying | Yes | Yes (insufficient collateral) |
| **Withdraw** | Assets OUT | aToken (burn) → Underlying | Yes (if has borrows) | Yes (would break HF) |
## Step by Step Code Walkthrough
### Step 1: Entry point - `withdraw()`
```solidity
// LendingPool.sol
function withdraw(
address asset, // asset to withdraw, eg. USDC address
uint256 amount, // withdrawal amount
address to // Recipient address
) external override whenNotPaused returns (uint256) {
// get reserve data for the asset
DataTypes.ReserveData storage reserve = _reserves[asset];
// get asset's aToken balance
address aToken = reserve.aTokenAddress;
uint256 userBalance = IAToken(aToken).balanceOf(msg.sender);
// handle withdraw max case
uint256 amountToWithdraw = amount;
if (amount == type(uint256).max) {
amountToWithdraw = userBalance;
}
// validate withdrawal amount
ValidationLogic.validateWithdraw(
asset,
amountToWithdraw,
userBalance,
_reserves,
_usersConfig[msg.sender],
_reservesList,
_reservesCount,
_addressesProvider.getPriceOracle()
);
// udpate reserve state (liquidity & variable borrow indexes)
reserve.updateState();
// update interest rates (variable, stable borrow rate, liquidity rate)
reserve.updateInterestRates(asset, aToken, 0, amountToWithdraw);
// if full withdrawal, update userconfig (set asset collateral flag as false)
if (amountToWithdraw == userBalance) {
_usersConfig[msg.sender].setUsingAsCollateral(reserve.id, false);
emit ReserveUsedAsCollateralDisabled(asset, msg.sender);
}
// burn aToken
IAToken(aToken).burn(msg.sender, to, amountToWithdraw, reserve.liquidityIndex);
// emit event
emit Withdraw(asset, msg.sender, to, amountToWithdraw);
// return amount to withdraw
return amountToWithdraw;
}
```
**Key operations**
- Get reserve data
- Validate the withdraw operation (key part)
- Update state & interest rates
- Burn aToken in scaled amount, transfer underlying asset in actual amount
### Step 2: Validation - The Health Factor Check
```solidity
ValidationLogic.validateWithdraw(
asset, // asset to withdraw
amountToWithdraw, // amount to withdraw
userBalance, // user's asset's aToken balance
_reserves, // reserve state
_usersConfig[msg.sender], // user config
_reservesList, // reserve list
_reservesCount, // number of reserves
_addressesProvider.getPriceOracle() // price oracle
);
// ValidationLogic.sol
function validateWithdraw(
address reserveAddress,
uint256 amount,
uint256 userBalance,
mapping(address => DataTypes.ReserveData) storage reservesData,
DataTypes.UserConfigurationMap storage userConfig,
mapping(uint256 => address) storage reserves,
uint256 reservesCount,
address oracle
) external view {
// check amount check: should be positive, <= user collateral balance
require(amount != 0, Errors.VL_INVALID_AMOUNT);
require(amount <= userBalance, Errors.VL_NOT_ENOUGH_AVAILABLE_USER_BALANCE);
// chekc reserve flag: reserve must be active
(bool isActive, , , ) = reservesData[reserveAddress].configuration.getFlags();
require(isActive, Errors.VL_NO_ACTIVE_RESERVE);
// check if the asset balance decrease is allowed
// require true to be processed
require(
GenericLogic.balanceDecreaseAllowed(
reserveAddress,
msg.sender,
amount,
reservesData,
userConfig,
reserves,
reservesCount,
oracle
),
Errors.VL_TRANSFER_NOT_ALLOWED
);
}
```
The key function is `balanceDecreaseAllowed()`
```solidity
function balanceDecreaseAllowed(
address asset,
address user,
uint256 amount,
mapping(address => DataTypes.ReserveData) storage reservesData,
DataTypes.UserConfigurationMap calldata userConfig,
mapping(uint256 => address) storage reserves,
uint256 reservesCount,
address oracle
) external view returns (bool) {
// no borrowing or not used as collateral: no risk, allow withdrawal
if (!userConfig.isBorrowingAny() || !userConfig.isUsingAsCollateral(reservesData[asset].id)) {
return true;
}
balanceDecreaseAllowedLocalVars memory vars;
// get reserve's liquidation threshold, decimals
(, vars.liquidationThreshold, , vars.decimals, ) = reservesData[asset]
.configuration
.getParams();
// liq. threshold = 0, the asset can't be used as collateral, withdraw won't affect health factor
if (vars.liquidationThreshold == 0) {
return true;
}
// get collateral info to calculate health factor
(
vars.totalCollateralInETH,
vars.totalDebtInETH,
,
vars.avgLiquidationThreshold,
) = calculateUserAccountData(user, reservesData, userConfig, reserves, reservesCount, oracle);
// if no debt yet, no liquidation risk for withdrawal
if (vars.totalDebtInETH == 0) {
return true;
}
// convert withdrawal amount to ETH
vars.amountToDecreaseInETH = IPriceOracleGetter(oracle).getAssetPrice(asset).mul(amount).div(
10**vars.decimals
);
// calculate collateral balance after withdrawal
vars.collateralBalanceAfterDecrease = vars.totalCollateralInETH.sub(vars.amountToDecreaseInETH);
//if there is a borrow, there can't be 0 collateral
if (vars.collateralBalanceAfterDecrease == 0) {
return false;
}
// calculate weighted avg liq. threshold after withdrawal
// formula = (total collateral in ETH * avg liq. threshold - withdrawal amount * liq. threshold) / total collateral after withdrawal
vars.liquidationThresholdAfterDecrease = vars
.totalCollateralInETH
.mul(vars.avgLiquidationThreshold)
.sub(vars.amountToDecreaseInETH.mul(vars.liquidationThreshold))
.div(vars.collateralBalanceAfterDecrease);
// calculate health factor after withdrawal
// formula = collateral balance after withdraw * avg liq. threshold after withdrawal / total debt in eth
uint256 healthFactorAfterDecrease =
calculateHealthFactorFromBalances(
vars.collateralBalanceAfterDecrease,
vars.totalDebtInETH,
vars.liquidationThresholdAfterDecrease
);
// require HF >= 1.0
return healthFactorAfterDecrease >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD;
}
```
**Key purpose**
- Basic validity checks on wtihdrawal amount
- Amount must be > 0
- Amount must not exceed user's aToken balance
- Reserve must be active
- Health factor calculation
- Ensure withdrawal won't drop HF below 1
- Only apply if user has active borrows
- Protocol safety guard
- Act as a gate-keeper before funds leave the protocol
- Block withdrawal that would create undercollateralized position
### Step 3: Update Reserve State and Interest Rates
```solidity
// udpate reserve state (liquidity & variable borrow indexes)
reserve.updateState();
// update interest rates (variable, stable borrow rate, liquidity rate)
reserve.updateInterestRates(asset, aToken, 0, amountToWithdraw);
```
**`updateState()` - Update cumulative indexes**
- What it updates
- `liquidityIndex`: for aToken balance growth, using linear interest
- `variableBorrowIndex`: for variable debt growth, using compound interest
- `lastUpdateTimeStamp`: to current block timestamp
- Purpose: Compound all accrued interest since last operation
- Why before burn: Need the current index to calculate how many scaled aTokens to burn
**`updateInterestRates()` - Recalculate APYs**
- What it updates
- `currentLiquidityRate` (what depositors earn)
- `currentVariableBorrowRate` (what variable borrowers pay)
- `currentStableBorrowRate` (what stable borrowers pay)
- Purpose: Adjust rates based on new supply/ demand dynamics
- How withdrawal affects rates
- Available liquidity decreases
- Utilization rate increases
- Interest rates goes up
### Step 4: Update UserConfig
```solidity
// if full withdrawal, update userconfig (set asset collateral flag as false)
if (amountToWithdraw == userBalance) {
// disable the asset as collateral
_usersConfig[msg.sender].setUsingAsCollateral(reserve.id, false);
// emit event
emit ReserveUsedAsCollateralDisabled(asset, msg.sender);
}
```
**Key purpose**
- Only when full withdrawal, the asset collateral flag will be cleared
### Step 5: Burn aTokens & Transfer Underlying
```solidity
IAToken(aToken).burn(msg.sender, to, amountToWithdraw, reserve.liquidityIndex);
// AToken.sol
function burn(
address user, // owner of the aToken
address receiverOfUnderlying, // receiver of the underlying
uint256 amount, // amount being burned
uint256 index // new liquidity index of the reserve
) external override onlyLendingPool {
// calculate scaled amount = amount / liquidty index
uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.CT_INVALID_BURN_AMOUNT);
// burn scaled amount of aToken from user
_burn(user, amountScaled);
// transfer the underlying asset to receiver
IERC20(_underlyingAsset).safeTransfer(receiverOfUnderlying, amount);
// emit event
emit Transfer(user, address(0), amount);
emit Burn(user, receiverOfUnderlying, amount, index);
}
```
**Key operation**
- Convert withdrawal amount to scaled amount (using new liquidity index updated by `updateState`)
- Burn the scaled amount of aToken from the user
- Transfer the withdrawal amount of underlying to the receiver
## Key Insights
### Withdrawal Constraint: Health Factor Protection
- **Deposit**: Always succeeds (unless reserve frozen/paused)
- **Withdraw**: Can fail if you have borrows and withdrawal would cause HF < 1.0
- **Why this matters**: This asymmetry is why liquidations exist - users can't withdraw collateral below safe levels
- **Design principle**: Protocol protects itself by enforcing HF > 1 constraint at withdrawal time
### Execution Order Matters
- The withdrawal flow follows the seqence: `validate -> update state -> update interest rates -> burn aTokens & transfer underlying`
- Validate first
- Check health factor using current state
- If it fails, revert early without wasting gas
- Prevent withdraw into an unsafe position
- Update reserve state
- Update liquidity index, which will be used to calculate the scaled amount of aTokens
- Update interest rates
- Use the updated state from `updateState`
- Withdrawal reduces liquidity -> increase utilization rate -> increase borrow rates
- Burn aTokens & Transfer underlying asset
- Use the updated liquidity index to calculate scaled amount
- Burn scaled amount of aTokens
- Transfer actual amount of underlying asset
### Collateral vs Borrowed Assets: Critical Distinction
**Risk parameters (LTV, Liquidation Threshold, Health Factor) apply ONLY to COLLATERAL assets, NOT borrowed assets**
```
Health Factor = (COLLATERAL Value × COLLATERAL's Threshold) / DEBT Value
↑_______ Uses collateral params _______↑ ↑_ Just a number _↑
```
The borrowed asset's LTV/Threshold is **irrelevant** to your position's safety. Only your collateral's parameters matter.
### Asset Asymmetry: Deposit/Borrow vs Collateral
**An asset can have LTV = 0 (cannot be collateral) but still be deposited and borrowed:**
```
TokenX Configuration:
- LTV: 0%
- Liquidation Threshold: 0%
- Borrowing Enabled: TRUE ✅
Flow:
┌─────────────────────────────────────┐
│ Alice: Deposits 10 ETH (good │
│ collateral, LTV 80%) │
│ → Borrows TokenX using ETH │
│ → Pays interest on TokenX │
└──────────────┬──────────────────────┘
│ Interest flows to...
▼
┌─────────────────────────────────────┐
│ Bob: Deposits 1000 TokenX (bad │
│ collateral, LTV 0%) │
│ → CANNOT borrow anything │
│ → BUT earns interest! ✅ │
└─────────────────────────────────────┘
```
**Why this works:**
- **Borrowing TokenX with ETH collateral**: Low protocol risk (ETH backs the loan)
- **Using TokenX as collateral**: High protocol risk (TokenX volatility could create bad debt)
- **Result**: Depositors earn interest from borrows, but risky assets can't be used as collateral
### Collateral Asset Selection: Aave is Selective
**Not all assets can be collateral, even major ones:**
From Aave V2 mainnet configuration:
```
✅ Can be collateral (LTV > 0):
- WETH, USDC, DAI (80%, 80%, 75% LTV) - Tier 1
- LINK, BAT, MKR (70%, 70%, 60% LTV) - Tier 2
- AAVE, SNX (50%, 15% LTV) - Tier 3 (risky)
❌ Cannot be collateral (LTV = 0):
- USDT, BUSD, SUSD (0% LTV)
→ Can still deposit & earn interest ✅
→ Can still be borrowed ✅
→ Just can't use as collateral ❌
```
## 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 20th, 2025*
*Part of my #LearnInPublic Defi series*