---
# System prepended metadata

title: Understanding Aave V2 Code (4/n) - Withdraw Mechanism

---

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