Aave

Theme

Aave V3 Borrowing Assets

Borrowing is a fundamental action in Aave V3, allowing users to leverage their supplied collateral to take out loans in other assets. This process involves intricate checks, state updates, and interactions across multiple contracts and libraries to ensure protocol solvency and accurately track user debt. This article traces the execution path of a typical borrow call.

The Entry Point: Pool.borrow()

A user (or a contract acting on their behalf) initiates a borrow request by calling the borrow function on the main Pool.sol contract.

// File: /src/contracts/protocol/pool/Pool.sol

/// @inheritdoc IPool
function borrow(
  address asset,          // The asset to borrow (e.g., USDC address)
  uint256 amount,         // The amount of the asset to borrow
  uint256 interestRateMode, // Must be 2 (Variable Rate in V3)
  uint16 referralCode,    // Optional code for integrators
  address onBehalfOf      // The address that will receive the debt tokens
) public virtual override {
  // Delegates the entire logic to the BorrowLogic library
  BorrowLogic.executeBorrow(
    _reserves, // Reference to reserve state storage
    _reservesList, // Reference to the list of reserves
    _eModeCategories, // Reference to eMode configurations
    _usersConfig[onBehalfOf], // The specific user's configuration map (tracks supply/borrow status)
    DataTypes.ExecuteBorrowParams({ // Pack parameters into a struct
      asset: asset,
      user: msg.sender, // The address initiating the transaction
      onBehalfOf: onBehalfOf, // The address receiving the debt
      amount: amount,
      interestRateMode: DataTypes.InterestRateMode(interestRateMode), // Cast to enum
      referralCode: referralCode,
      releaseUnderlying: true, // Standard borrow sends funds to the user
      reservesCount: _reservesCount, // Total number of reserves (for iteration)
      oracle: ADDRESSES_PROVIDER.getPriceOracle(), // Fetch price oracle address
      userEModeCategory: _usersEModeCategory[onBehalfOf], // Fetch user's eMode status
      priceOracleSentinel: ADDRESSES_PROVIDER.getPriceOracleSentinel() // Fetch L2 sentinel address (if any)
    })
  );
}

The Pool.borrow function itself does very little computation. Its primary role is to:

  1. Receive the borrow request parameters.
  2. Retrieve necessary context from storage (_reserves, _usersConfig, _usersEModeCategory, _reservesCount) and the PoolAddressesProvider (oracle, priceOracleSentinel).
  3. Package all information into the DataTypes.ExecuteBorrowParams struct.
  4. Delegate the entire operation to the executeBorrow function within the BorrowLogic library.

This delegation pattern is key to Aave V3's design, keeping the core Pool contract focused on orchestration and making the logic modular and easier to manage/upgrade.

Inside the Engine: BorrowLogic.executeBorrow()

The BorrowLogic.sol library handles the core mechanics of the borrowing process.

// File: /src/contracts/protocol/libraries/logic/BorrowLogic.sol

function executeBorrow(
  mapping(address => DataTypes.ReserveData) storage reservesData,
  mapping(uint256 => address) storage reservesList,
  mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories,
  DataTypes.UserConfigurationMap storage userConfig,
  DataTypes.ExecuteBorrowParams memory params
) external {
  // 1. Cache Reserve Data & Update State
  DataTypes.ReserveData storage reserve = reservesData[params.asset];
  DataTypes.ReserveCache memory reserveCache = reserve.cache();
  reserve.updateState(reserveCache); // Accrue interest up to this block

  // 2. Check Isolation Mode Status
  (
    bool isolationModeActive,
    address isolationModeCollateralAddress,
    uint256 isolationModeDebtCeiling
  ) = userConfig.getIsolationModeState(reservesData, reservesList);

  // 3. Perform Core Validations
  ValidationLogic.validateBorrow(
    reservesData,
    reservesList,
    eModeCategories,
    DataTypes.ValidateBorrowParams({
      reserveCache: reserveCache,
      userConfig: userConfig,
      asset: params.asset,
      userAddress: params.onBehalfOf,
      amount: params.amount,
      interestRateMode: params.interestRateMode,
      reservesCount: params.reservesCount,
      oracle: params.oracle,
      userEModeCategory: params.userEModeCategory,
      priceOracleSentinel: params.priceOracleSentinel,
      isolationModeActive: isolationModeActive,
      isolationModeCollateralAddress: isolationModeCollateralAddress,
      isolationModeDebtCeiling: isolationModeDebtCeiling
    })
  );

  bool isFirstBorrowing = false;

  // 4. Mint Debt Tokens
  (isFirstBorrowing, reserveCache.nextScaledVariableDebt) = IVariableDebtToken(
    reserveCache.variableDebtTokenAddress
  ).mint(params.user, params.onBehalfOf, params.amount, reserveCache.nextVariableBorrowIndex);

  // 5. Update User Configuration (if first borrow of this asset)
  if (isFirstBorrowing) {
    userConfig.setBorrowing(reserve.id, true);
  }

  // 6. Update Isolation Mode Debt (if applicable)
  if (isolationModeActive) {
    uint256 nextIsolationModeTotalDebt = reservesData[isolationModeCollateralAddress]
      .isolationModeTotalDebt += (params.amount /
      10 **
        (reserveCache.reserveConfiguration.getDecimals() -
          ReserveConfiguration.DEBT_CEILING_DECIMALS)).toUint128();
    emit IsolationModeTotalDebtUpdated(
      isolationModeCollateralAddress,
      nextIsolationModeTotalDebt
    );
  }

  // 7. Update Reserve Interest Rates & Virtual Balance
  reserve.updateInterestRatesAndVirtualBalance(
    reserveCache,
    params.asset,
    0, // liquidityAdded
    params.releaseUnderlying ? params.amount : 0 // liquidityTaken
  );

  // 8. Transfer Underlying Asset to User (if requested)
  if (params.releaseUnderlying) {
    IAToken(reserveCache.aTokenAddress).transferUnderlyingTo(params.user, params.amount);
  }

  // 9. Emit Borrow Event
  emit Borrow(
    params.asset,
    params.user,
    params.onBehalfOf,
    params.amount,
    DataTypes.InterestRateMode.VARIABLE,
    reserve.currentVariableBorrowRate,
    params.referralCode
  );
}

Let's break down these steps:

  1. Cache Reserve Data & Update State:

    • Reads the ReserveData for the borrowed asset from storage.
    • Creates a ReserveCache in memory to optimize subsequent reads.
    • Calls reserve.updateState(). This crucial step calculates interest accrued since the reserve's last update (using ReserveLogic.sol and MathUtils.sol) and updates the liquidityIndex and variableBorrowIndex. It ensures all calculations are based on the most up-to-date state before the borrow action modifies it.
  2. Check Isolation Mode Status:

    • Calls userConfig.getIsolationModeState() (defined in UserConfiguration.sol). This checks if the user's collateral consists of a single asset flagged for isolation mode. If so, it returns true along with the collateral asset address and its specific debt ceiling. This information is needed for validation.
  3. Perform Core Validations (ValidationLogic.validateBorrow):

    • This is the primary gatekeeper. ValidationLogic.sol performs a series of checks using the provided parameters and state:
      • Reserve Status: Is the asset active, not frozen, not paused? (ReserveConfiguration.sol)
      • Borrowing Enabled: Is borrowing enabled for this asset? (ReserveConfiguration.sol)
      • Liquidity: Is there enough underlying liquidity available in the corresponding aToken contract? (AToken.sol, IERC20.sol) (Relevant for non-virtual assets).
      • L2 Sentinel: If on L2 (priceOracleSentinel is set), is borrowing allowed by the sentinel? (IPriceOracleSentinel.sol)
      • Interest Rate Mode: Is the requested mode VARIABLE (Mode 2)?
      • Borrow Cap: Would this borrow exceed the reserve's borrow cap? (ReserveConfiguration.sol)
      • Isolation Mode: If active, is this asset borrowable within isolation mode? Would this borrow exceed the collateral's specific debt ceiling? (ReserveConfiguration.sol, IsolationModeLogic.sol implicitly)
      • eMode: If the user is in an eMode category, is this asset borrowable within that category? (EModeConfiguration.sol)
      • Health Factor: Calculates the user's current health factor using GenericLogic.calculateUserAccountData. This involves fetching all user balances (supply/borrow), getting prices from the IPriceOracleGetter, applying LTVs/thresholds (considering eMode), and ensuring the HF is above the liquidation threshold (usually 1).
      • Collateral Coverage: Calculates if the user's existing collateral is sufficient to cover the new total debt (existing debt + this borrow amount) based on the applicable LTV. (GenericLogic.sol, PercentageMath.sol)
      • Siloed Borrowing: If the user is already borrowing a "siloed" asset, they can only borrow more of that same asset. If the asset being borrowed now is siloed, the user cannot be borrowing anything else. (UserConfiguration.sol, ReserveConfiguration.sol)
    • If any of these checks fail, the transaction reverts with a descriptive error from Errors.sol.
  4. Mint Debt Tokens:

    • If validation passes, the actual debt is created by calling mint() on the asset's VariableDebtToken contract (IVariableDebtToken.sol, implemented in VariableDebtToken.sol).
    • Internally, the debt token uses ScaledBalanceTokenBase.sol's _mintScaled. It calculates the amountScaled = amount.rayDiv(currentVariableBorrowIndex) and stores this scaled amount. The user's actual debt balance (VariableDebtToken.balanceOf) is dynamically calculated later as scaledBalance.rayMul(currentVariableBorrowIndex). This scaled balance approach avoids iterating through users to update balances when interest accrues; only the index needs updating.
    • The mint function returns isFirstBorrowing, indicating if the user's debt balance for this asset was zero before this mint.
  5. Update User Configuration:

    • If isFirstBorrowing is true, the code calls userConfig.setBorrowing(reserve.id, true) (using UserConfiguration.sol) to flip the borrowing bit for this asset in the user's configuration map. This is crucial for tracking which assets contribute to the user's total debt in subsequent calculateUserAccountData calls.
  6. Update Isolation Mode Debt (Conditional):

    • If the user is borrowing in isolation mode (isolationModeActive), the calculated debt amount (converted to the appropriate scale for the debt ceiling) is added to the isolationModeTotalDebt field within the ReserveData of the collateral asset. This ensures the debt ceiling for that specific collateral is enforced. An IsolationModeTotalDebtUpdated event is emitted.
  7. Update Reserve Interest Rates & Virtual Balance:

    • Calls reserve.updateInterestRatesAndVirtualBalance(). This function (in ReserveLogic.sol) recalculates the currentLiquidityRate and currentVariableBorrowRate based on the new total supply and total debt (utilization) after the borrow. It calls the configured IReserveInterestRateStrategy contract to get these new rates. It also updates the virtualUnderlyingBalance if applicable. A ReserveDataUpdated event is emitted internally by ReserveLogic.
  8. Transfer Underlying Asset:

    • For a standard borrow (releaseUnderlying == true), the funds need to be sent to the user. The liquidity backing aTokens is held by the AToken contract itself. Therefore, BorrowLogic calls IAToken(aTokenAddress).transferUnderlyingTo(params.user, params.amount). This instructs the aToken contract to transfer the borrowed underlying asset (e.g., USDC) to the borrower's address (params.user, which is msg.sender in this flow). (AToken.sol uses GPv2SafeERC20.sol for the transfer).
  9. Emit Borrow Event:

    • Finally, the Borrow event (defined in IPool.sol) is emitted, signaling the successful completion of the operation and providing key details for off-chain indexers.

Conclusion

Borrowing in Aave V3 is a carefully orchestrated process. The user interacts with the Pool, which immediately delegates to BorrowLogic. This library coordinates calls to:

  • ReserveLogic to update interest accrual before and calculate new rates after.
  • ValidationLogic (which uses GenericLogic, configuration libraries, and oracles) to perform comprehensive safety checks.
  • VariableDebtToken to mint the debt position using scaled balance math.
  • UserConfiguration library to update the user's state map.
  • AToken to release the underlying funds to the borrower.

This multi-step process, relying heavily on specialized libraries and state updates, ensures that borrows are validated against collateral, adhere to risk parameters (caps, LTVs, isolation/eMode rules), and that the protocol's accounting remains consistent.