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:
- Receive the borrow request parameters.
- Retrieve necessary context from storage (
_reserves,_usersConfig,_usersEModeCategory,_reservesCount) and thePoolAddressesProvider(oracle,priceOracleSentinel). - Package all information into the
DataTypes.ExecuteBorrowParamsstruct. - Delegate the entire operation to the
executeBorrowfunction within theBorrowLogiclibrary.
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:
-
Cache Reserve Data & Update State:
- Reads the
ReserveDatafor the borrowed asset from storage. - Creates a
ReserveCachein memory to optimize subsequent reads. - Calls
reserve.updateState(). This crucial step calculates interest accrued since the reserve's last update (usingReserveLogic.solandMathUtils.sol) and updates theliquidityIndexandvariableBorrowIndex. It ensures all calculations are based on the most up-to-date state before the borrow action modifies it.
- Reads the
-
Check Isolation Mode Status:
- Calls
userConfig.getIsolationModeState()(defined inUserConfiguration.sol). This checks if the user's collateral consists of a single asset flagged for isolation mode. If so, it returnstruealong with the collateral asset address and its specific debt ceiling. This information is needed for validation.
- Calls
-
Perform Core Validations (
ValidationLogic.validateBorrow):- This is the primary gatekeeper.
ValidationLogic.solperforms 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 (
priceOracleSentinelis 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.solimplicitly) - 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 theIPriceOracleGetter, 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)
- Reserve Status: Is the asset active, not frozen, not paused? (
- If any of these checks fail, the transaction reverts with a descriptive error from
Errors.sol.
- This is the primary gatekeeper.
-
Mint Debt Tokens:
- If validation passes, the actual debt is created by calling
mint()on the asset'sVariableDebtTokencontract (IVariableDebtToken.sol, implemented inVariableDebtToken.sol). - Internally, the debt token uses
ScaledBalanceTokenBase.sol's_mintScaled. It calculates theamountScaled = amount.rayDiv(currentVariableBorrowIndex)and stores this scaled amount. The user's actual debt balance (VariableDebtToken.balanceOf) is dynamically calculated later asscaledBalance.rayMul(currentVariableBorrowIndex). This scaled balance approach avoids iterating through users to update balances when interest accrues; only the index needs updating. - The
mintfunction returnsisFirstBorrowing, indicating if the user's debt balance for this asset was zero before this mint.
- If validation passes, the actual debt is created by calling
-
Update User Configuration:
- If
isFirstBorrowingis true, the code callsuserConfig.setBorrowing(reserve.id, true)(usingUserConfiguration.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 subsequentcalculateUserAccountDatacalls.
- If
-
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 theisolationModeTotalDebtfield within theReserveDataof the collateral asset. This ensures the debt ceiling for that specific collateral is enforced. AnIsolationModeTotalDebtUpdatedevent is emitted.
- If the user is borrowing in isolation mode (
-
Update Reserve Interest Rates & Virtual Balance:
- Calls
reserve.updateInterestRatesAndVirtualBalance(). This function (inReserveLogic.sol) recalculates thecurrentLiquidityRateandcurrentVariableBorrowRatebased on the new total supply and total debt (utilization) after the borrow. It calls the configuredIReserveInterestRateStrategycontract to get these new rates. It also updates thevirtualUnderlyingBalanceif applicable. AReserveDataUpdatedevent is emitted internally byReserveLogic.
- Calls
-
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 theATokencontract itself. Therefore,BorrowLogiccallsIAToken(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 ismsg.senderin this flow). (AToken.solusesGPv2SafeERC20.solfor the transfer).
- For a standard borrow (
-
Emit Borrow Event:
- Finally, the
Borrowevent (defined inIPool.sol) is emitted, signaling the successful completion of the operation and providing key details for off-chain indexers.
- Finally, the
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:
ReserveLogicto update interest accrual before and calculate new rates after.ValidationLogic(which usesGenericLogic, configuration libraries, and oracles) to perform comprehensive safety checks.VariableDebtTokento mint the debt position using scaled balance math.UserConfigurationlibrary to update the user's state map.ATokento 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.

