Skip to content

Latest commit

 

History

History
433 lines (351 loc) · 11.8 KB

File metadata and controls

433 lines (351 loc) · 11.8 KB

Withdraw Flow

End-to-end execution flow for withdrawing assets from Aave V3.

Quick Reference

Aspect Details
Entry Point Pool.withdraw(asset, amount, to)
Key Transformations Scaled Balance → Amount
State Changes _scaledBalance[msg.sender] -= scaledAmount
Events Emitted Withdraw, ReserveUsedAsCollateralDisabled (conditional)

Flow Diagram

flowchart TD
    %% Styling definitions
    classDef validation fill:#ffcccc,stroke:#ff0000,stroke-width:2px
    classDef transformation fill:#ccffcc,stroke:#00aa00,stroke-width:2px
    classDef storage fill:#ccccff,stroke:#0000ff,stroke-width:2px
    classDef event fill:#ffffcc,stroke:#aaaa00,stroke-width:2px
    classDef error fill:#ff0000,stroke:#000000,color:#fff
    classDef critical stroke:#ff0000,stroke-width:3px

    %% Entry point
    Entry["Pool.withdraw
    asset, amount, to"] --> Execute["WithdrawLogic
    executeWithdraw"]
    
    subgraph StateUpdate ["1. State Updates"]
        direction TB
        UpdateState["ReserveLogic
        updateState
        Updates indexes,
        accrues interest"] --> Validate["ValidationLogic
        validateWithdraw"]
        class Validate validation
    end
    
    subgraph TokenOps ["2. Token Operations"]
        direction TB
        GetBalance["TRANSFORMATION
        userBalance =
        scaledBalance
        .rayMul(index)"] --> MaxCheck{"amount ==
        MAX_UINT?"}
        
        MaxCheck -->|Yes| UseMax["Set amountToWithdraw
        = userBalance"]
        MaxCheck -->|No| UseProvided["Use provided amount"]
        
        UseMax --> Validate
        UseProvided --> Validate
        
        Validate --> UpdateRates["ReserveLogic
        updateInterestRates"]
    end
    
    subgraph HealthCheck ["3. Health Factor / Safety Checks"]
        direction TB
        Burn["AToken
        burn"] --> Transform["TRANSFORMATION
        scaledAmount =
        amount.rayDiv(index)"]
        class Transform transformation
        
        Transform --> Store["STORAGE UPDATE
        _scaledBalance -=
        scaledAmount"]
        class Store storage
        
        Store --> CollatCheck{"Is Collateral
        && amount ==
        userBalance?"}
        
        CollatCheck -->|Yes| DisableCollat["UserConfig
        setUsingAsCollateral
        (reserve.id, false)"]
        class DisableCollat storage
        
        DisableCollat --> CollatEvent["EMIT
        ReserveUsedAsCollateralDisabled"]
        class CollatEvent event
        
        CollatCheck -->|No| SkipCollat[Continue]
        
        CollatEvent --> HFCheck{"Is Collateral
        && Has Debt?"}
        SkipCollat --> HFCheck
    end
    
    UpdateRates --> TokenOps
    UpdateRates --> Burn
    
    %% Critical path: Health factor validation
    HFCheck -->|Yes| HFValidation["ValidationLogic
    validateHFAndLtv
    CRITICAL: HF >= 1.0"]
    class HFValidation validation
    class HFValidation critical
    
    HFCheck -->|No| SkipHF[Continue]
    
    HFValidation --> FinalEvent["EMIT
    Withdraw"]
    class FinalEvent event
    
    SkipHF --> FinalEvent
    
    %% Error annotations
    %% CRITICAL: Health factor validation reverts if HF < 1.0
    %% CRITICAL: Collateral disabled if withdrawing full balance
    
    %% Link styles for critical paths
    linkStyle 20 stroke:#ff0000,stroke-width:3px
Loading

Step-by-Step Execution

1. Entry Point

File: contracts/protocol/pool/Pool.sol

function withdraw(
    address asset,
    uint256 amount,
    address to
) external virtual override returns (uint256) {
    return SupplyLogic.executeWithdraw(
        _reserves,
        _reservesList,
        _usersConfig[msg.sender],
        DataTypes.ExecuteWithdrawParams({
            asset: asset,
            amount: amount,
            to: to,
            reservesCount: _reservesCount,
            oracle: ADDRESSES_PROVIDER.getPriceOracle(),
            userEModeCategory: _usersEModeCategory[msg.sender]
        })
    );
}

2. Execute Withdraw

File: contracts/protocol/libraries/logic/SupplyLogic.sol

function executeWithdraw(
    mapping(address => DataTypes.ReserveData) storage reserves,
    mapping(uint256 => address) storage reservesList,
    DataTypes.UserConfigurationMap storage userConfig,
    DataTypes.ExecuteWithdrawParams memory params
) external returns (uint256) {
    DataTypes.ReserveData storage reserve = reserves[params.asset];
    DataTypes.ReserveCache memory reserveCache = reserve.cache();
    
    // Update state
    reserve.updateState(reserveCache);
    
    // Get user's balance
    uint256 userBalance = IAToken(reserveCache.aTokenAddress)
        .scaledBalanceOf(msg.sender)
        .rayMul(reserveCache.nextLiquidityIndex);  // [TRANSFORMATION]
    
    // Handle max withdrawal
    uint256 amountToWithdraw = params.amount;
    if (params.amount == type(uint256).max) {
        amountToWithdraw = userBalance;
    }
    
    // Validate withdrawal
    ValidationLogic.validateWithdraw(
        reserves,
        reservesList,
        reserveCache,
        amountToWithdraw,
        userConfig
    );
    
    // Update interest rates
    reserve.updateInterestRates(
        reserveCache,
        params.asset,
        0,              // liquidityAdded
        amountToWithdraw  // liquidityTaken
    );
    
    // Burn aTokens
    IAToken(reserveCache.aTokenAddress).burn(
        msg.sender,
        params.to,
        amountToWithdraw,
        reserveCache.nextLiquidityIndex
    );
    
    // Check if collateral should be disabled
    if (userConfig.isUsingAsCollateral(reserve.id)) {
        if (amountToWithdraw == userBalance) {
            userConfig.setUsingAsCollateral(reserve.id, false);
            emit ReserveUsedAsCollateralDisabled(params.asset, msg.sender);
        }
        
        // Validate health factor if user has debt
        if (userConfig.isBorrowingAny()) {
            ValidationLogic.validateHFAndLtv(
                reserves,
                reservesList,
                userConfig,
                params.asset,
                params.userEModeCategory,
                params.reservesCount,
                params.oracle
            );
        }
    }
    
    emit Withdraw(
        params.asset,
        msg.sender,
        params.to,
        amountToWithdraw
    );
    
    return amountToWithdraw;
}

3. AToken Burn

File: contracts/protocol/tokenization/AToken.sol

function burn(
    address from,
    address receiverOfUnderlying,
    uint256 amount,
    uint256 index
) external override onlyPool {
    _burnScaled(from, receiverOfUnderlying, amount, index);
}

function _burnScaled(
    address from,
    address receiverOfUnderlying,
    uint256 amount,
    uint256 index
) internal {
    uint256 scaledAmount = amount.rayDiv(index);  // [TRANSFORMATION]
    _scaledBalance[from] -= scaledAmount;
    
    IERC20(_underlyingAsset).safeTransfer(receiverOfUnderlying, amount);
}

[TRANSFORMATION]: See Collateral Token Transformations for details on amount.rayDiv(index)

4. Validation Checks

File: contracts/protocol/libraries/logic/ValidationLogic.sol

function validateWithdraw(
    mapping(address => DataTypes.ReserveData) storage reserves,
    mapping(uint256 => address) storage reservesList,
    DataTypes.ReserveCache memory reserveCache,
    uint256 amount,
    DataTypes.UserConfigurationMap storage userConfig
) internal view {
    require(amount != 0, Errors.INVALID_AMOUNT);
    
    // Check reserve is active
    require(
        reserveCache.reserveConfiguration.getActive(),
        Errors.RESERVE_INACTIVE
    );
    
    // Check user has sufficient balance
    uint256 userBalance = IAToken(reserveCache.aTokenAddress)
        .scaledBalanceOf(msg.sender)
        .rayMul(reserveCache.nextLiquidityIndex);
    require(userBalance >= amount, Errors.INVALID_AMOUNT);
}

5. Health Factor Check

File: contracts/protocol/libraries/logic/ValidationLogic.sol

function validateHFAndLtv(
    mapping(address => DataTypes.ReserveData) storage reserves,
    mapping(uint256 => address) storage reservesList,
    DataTypes.UserConfigurationMap storage userConfig,
    address asset,
    uint8 userEModeCategory,
    uint256 reservesCount,
    address oracle
) internal view {
    // Calculate user account data
    (
        uint256 totalCollateralInBaseCurrency,
        uint256 totalDebtInBaseCurrency,
        uint256 avgLtv,
        uint256 avgLiquidationThreshold,
        uint256 healthFactor,
        bool hasZeroLtvCollateral
    ) = GenericLogic.calculateUserAccountData(
        reserves,
        reservesList,
        _eModeCategories,
        DataTypes.CalculateUserAccountDataParams({
            userConfig: userConfig,
            reservesCount: reservesCount,
            user: msg.sender,
            oracle: oracle,
            userEModeCategory: userEModeCategory
        })
    );
    
    // Check health factor
    require(
        healthFactor >= HEALTH_FACTOR_LIQUIDATION_THRESHOLD,
        Errors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD
    );
    
    // Check LTV if withdrawing 0 LTV collateral
    if (hasZeroLtvCollateral && totalDebtInBaseCurrency > 0) {
        DataTypes.ReserveData storage collateralReserve = reserves[asset];
        DataTypes.ReserveCache memory collateralReserveCache = collateralReserve.cache();
        
        require(
            collateralReserveCache.reserveConfiguration.getLtv() != 0 ||  // Not withdrawing 0-LTV asset
            avgLtv > 0,  // Or user has other collateral with LTV > 0
            Errors.LTV_VALIDATION_FAILED
        );
    }
}

Amount Transformations

Storage → Output

_scaledBalance[msg.sender] (stored scaled)
    ↓
liquidityIndex = 1.0002 * 10^27  // Current index
    ↓
userBalance = _scaledBalance[msg.sender].rayMul(liquidityIndex)
            = 1000 * 10^18  // 1000 tokens + accrued interest
    ↓
Validation: amount <= userBalance
    ↓
scaledAmountToBurn = amount.rayDiv(liquidityIndex)
                   = (1000 * 10^18 * 10^27) / (1.0002 * 10^27)
    ↓
_scaledBalance[msg.sender] -= scaledAmountToBurn
    ↓
Transfer amount to user

Key Points:

  • User receives WAD-decimal amount (18 decimals)
  • Withdrawn amount includes accrued interest from scaled balance
  • Health factor must remain above liquidation threshold
  • If withdrawing all collateral and user has debt, HF check is strict

Event Details

Withdraw Event

event Withdraw(
    address indexed reserve,    // Asset address
    address indexed user,       // msg.sender
    address indexed to,         // Recipient of underlying
    uint256 amount              // Amount withdrawn
);

ReserveUsedAsCollateralDisabled Event

Emitted when withdrawing all of a collateral Asset.

event ReserveUsedAsCollateralDisabled(
    address indexed reserve,
    address indexed user
);

Error Conditions

Error Condition File
INVALID_AMOUNT amount == 0 or amount > balance ValidationLogic.sol
RESERVE_INACTIVE Reserve is not active ValidationLogic.sol
HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD HF < 1.0 after withdrawal ValidationLogic.sol
LTV_VALIDATION_FAILED Withdrawing 0-LTV collateral while having debt ValidationLogic.sol

Related Flows


Source File Locations

contracts/protocol/pool/Pool.sol
contracts/protocol/libraries/logic/SupplyLogic.sol
contracts/protocol/libraries/logic/ValidationLogic.sol
contracts/protocol/libraries/logic/GenericLogic.sol
contracts/protocol/tokenization/AToken.sol
contracts/protocol/libraries/logic/ReserveLogic.sol