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