Back to Blog 

DeFiSmart ContractsAaveAudit
Aave V3 — DeFi Protocol's Code Explained. Part 1 — Pool.sol
8 min
Continuing the saga of DeFi Protocol by code, here I have started the first part of the walk-through Aave V3 protocol's smart contracts.
It is super helpful to dive into the code of such popular protocols because first of all you'll understand it much better and second because all new protocols forked from Aave will be based in the same code and if you end up auditing one of them, you'll already be familiar and it will make you save time.
In this article we are going to cover:
- Introduction & Architecture
- Usages
- Pool.sol main functions:
supply()withdraw()borrow()repay()
What is Aave?
Aave is a decentralised non-custodial liquidity market protocol where users can participate as suppliers or borrowers. Suppliers provide liquidity to the market to earn a passive income, while borrowers are able to borrow unexpected expenses, leveraging their holdings.
Liquidity is at the heart of the Aave Protocol as it enables the protocol's operations and user experience.
The liquidity of the protocol is measured by the availability of assets for basic protocol operations such as borrowing assets backed by collateral and claiming supplied assets along with accrued yield. A lack of liquidity will block operations.
Architecture
The Aave Protocol V3 contracts are divided in two repositories:
aave-v3-core — Hosts core protocol V3 contracts that contains the logic for supply, borrow, liquidation, flashloan, a/s/v tokens, portal, pool configuration, oracles and interest rate strategies.
Core protocol contracts fall in 4 categories:
- Configuration
- Pool logic
- Tokenization
- Misc
aave-v3-periphery — In this repository you will find the contracts related to rewards, ui data provider, incentive data provider, wallet balance provider and WETH gateway.
There are 2 categories of contracts:
- Rewards
- Misc
What can we do with Aave protocol?
Supply & Earn. By supplying, you will earn passive income based on the market borrowing demand.
Borrow. Additionally, supplying assets allows you to borrow by using your supplied assets as a collateral.
Pool.sol Smart Contract
I'm going to focus this Part 1 of the Aave V3 series on this contract, so that we can see first how it's composed one of the higher level smart contract and how it links with other low level contracts.
The Pool.sol contract is the main user facing contract of the protocol. It exposes the liquidity management methods.
Pool.sol is owned by the PoolAddressesProvider of the specific market. All admin functions are callable by the PoolConfigurator contract, which is defined in PoolAddressesProvider.
I have created this diagram which shows how Pool's main functions interact with low level functions.

Something interesting to mention here is that in the Pool contract itself it will mainly be calling to either internal functions, such as
executeSupply() inside the contract SupplyLogic.sol or libraries like DataTypes which holds and stores the function's main parameters in structs.function supply()
1function supply(2 address asset,3 uint256 amount,4 address onBehalfOf,5 uint16 referralCode6) external;
- asset: address of the asset being supplied to the pool.
- amount: amount of asset being supplied.
- onBehalfOf: address that will receive the corresponding aTokens. Only the
onBehalfOfaddress will be able to withdraw asset from the pool. - referralCode: unique code for 3rd party referral program integration. Use 0 for no referral.
Supplies an
amount of underlying asset into the reserve, receiving in return overlying aTokens. E.g. User supplies 100 USDC and gets in return 100 aUSDC.Inside the function we find only the call to the internal function
executeSupply() inside SupplyLogic.sol:1SupplyLogic.executeSupply(2 _reserves,3 _reservesList,4 _usersConfig[onBehalfOf],5 DataTypes.ExecuteSupplyParams({6 asset: asset,7 amount: amount,8 onBehalfOf: onBehalfOf,9 referralCode: referralCode10 })11);
Don't get overwhelmed by seeing this
ExecuteSupplyParams call inside the function's parameter. It's simply passing a list of parameters wrapped in a struct.Now, looking inside
executeSupply() we can split it in three parts:1. Updates and validations
1reserve.updateState(reserveCache);23ValidationLogic.validateSupply(reserveCache, reserve, params.amount);45reserve.updateInterestRates(reserveCache, params.asset, params.amount, 0);
First it will update the reserves with the provided reserveData from the specific asset passed as argument.
Right after, it will pass this data to validation where the main checks will be that this asset fulfils the following:
1require(isActive, Errors.RESERVE_INACTIVE);2require(!isPaused, Errors.RESERVE_PAUSED);3require(!isFrozen, Errors.RESERVE_FROZEN);
And last thing to update is the interest rates with
updateInterestRates() which receives as parameters, the reserves cached, the specific asset and the amount of the asset.2. The main action — supply and mint
The supply of the ERC20 is done by using a
safeTransferFrom function and the mint of the aTokens:1IERC20(params.asset).safeTransferFrom(2 msg.sender,3 reserveCache.aTokenAddress,4 params.amount5);67IAToken(reserveCache.aTokenAddress).mint(8 msg.sender,9 params.onBehalfOf,10 params.amount,11 reserveCache.nextLiquidityIndex12);
3. Setting the collateral value
This will only happen if it is the first time the sender makes a supply. That's why we have the condition to be set if
isFirstSupply, the validation before modifying anything and finally the setter:1if (isFirstSupply) {2 if (ValidationLogic.validateUseAsCollateral()) {3 userConfig.setUsingAsCollateral(reserve.id, true);4 }5}
function withdraw()
1function withdraw(2 address asset,3 uint256 amount,4 address to5) external returns (uint256);
Withdraws an
amount of underlying asset from the reserve, burning the equivalent aTokens owned. E.g. User has 100 aUSDC, calls withdraw() and receives 100 USDC, burning the 100 aUSDC.Likewise
supply, withdraw directly calls to the internal function executeWithdraw inside SupplyLogic.sol:1SupplyLogic.executeWithdraw(2 _reserves,3 _reservesList,4 _eModeCategories,5 _usersConfig[msg.sender],6 DataTypes.ExecuteWithdrawParams({7 asset: asset,8 amount: amount,9 to: to,10 reservesCount: _reservesCount,11 oracle: ADDRESSES_PROVIDER.getPriceOracle(),12 userEModeCategory: _usersEModeCategory[msg.sender]13 })14);
Inside
executeWithdraw there are two main parts:1. Update and validate
1ValidationLogic.validateWithdraw(reserveCache, amountToWithdraw, userBalance);23reserve.updateInterestRates(reserveCache, params.asset, 0, amountToWithdraw);
Get the DeFi Protocol Security Checklist
15 vulnerabilities every DeFi team should check before mainnet. Used by 40+ protocols.
No spam. Unsubscribe anytime.
As with
executeSupply it validates the provided parameters in order to update interest rates.Then it checks
isUsingAsCollateral() and if it is true and the amount desired to withdraw is as high as the userBalance then it will cancel the existing collateral:1if (isCollateral && amountToWithdraw == userBalance) {2 userConfig.setUsingAsCollateral(reserve.id, false);3}
2. Burn the aTokens
Burns aTokens from
user and sends the equivalent amount of underlying to the address specified in params.to:1IAToken(reserveCache.aTokenAddress).burn(2 msg.sender,3 params.to,4 amountToWithdraw,5 reserveCache.nextLiquidityIndex6);
function borrow()
1function borrow(2 address asset,3 uint256 amount,4 uint256 interestRateMode,5 uint16 referralCode,6 address onBehalfOf7) external;
Allows users to borrow a specific
amount of the reserve underlying asset, provided that the borrower already supplied enough collateral, or was given enough allowance by a credit delegator on the corresponding debt token (StableDebtToken or VariableDebtToken).E.g. User borrows 100 USDC passing as
onBehalfOf his own address, receiving the 100 USDC in his wallet and 100 stable/variable debt tokens, depending on the interestRateMode.Inside
borrow it directly calls the internal executeBorrow function inside BorrowLogic.sol:1BorrowLogic.executeBorrow(2 _reserves,3 _reservesList,4 _eModeCategories,5 _usersConfig[onBehalfOf],6 DataTypes.ExecuteBorrowParams({7 asset: asset,8 user: msg.sender,9 onBehalfOf: onBehalfOf,10 amount: amount,11 interestRateMode: DataTypes.InterestRateMode(interestRateMode),12 referralCode: referralCode,13 releaseUnderlying: true,14 maxStableRateBorrowSizePercent: _maxStableRateBorrowSizePercent,15 reservesCount: _reservesCount,16 oracle: ADDRESSES_PROVIDER.getPriceOracle(),17 userEModeCategory: _usersEModeCategory[onBehalfOf],18 priceOracleSentinel: ADDRESSES_PROVIDER.getPriceOracleSentinel()19 })20);
One thing to highlight is the
interestRateMode. It is an enum that determines which debt token will be minted:1enum InterestRateMode {2 NONE,3 STABLE,4 VARIABLE5}
If STABLE it will execute:
1IStableDebtToken(reserveCache.stableDebtTokenAddress).mint(2 params.user,3 params.onBehalfOf,4 params.amount,5 currentStableRate6);
If VARIABLE:
1IVariableDebtToken(reserveCache.variableDebtTokenAddress).mint(2 params.user,3 params.onBehalfOf,4 params.amount,5 reserveCache.nextVariableBorrowIndex6);
Once the tokens are minted, the actual transfer of the underlying asset to the user happens:
1IAToken(reserveCache.aTokenAddress).transferUnderlyingTo(2 params.user,3 params.amount4);
function repay()
1function repay(2 address asset,3 uint256 amount,4 uint256 interestRateMode,5 address onBehalfOf6) external returns (uint256);
Repays a borrowed
amount on a specific reserve, burning the equivalent debt tokens owned. E.g. User repays 100 USDC, burning 100 variable/stable debt tokens of the onBehalfOf address.As in the rest of functions,
repay calls its internal function executeRepay inside BorrowLogic.sol:1BorrowLogic.executeRepay(2 _reserves,3 _reservesList,4 _usersConfig[onBehalfOf],5 DataTypes.ExecuteRepayParams({6 asset: asset,7 amount: amount,8 interestRateMode: DataTypes.InterestRateMode(interestRateMode),9 onBehalfOf: onBehalfOf,10 useATokens: false11 })12);
Note
useATokens is set to false here — there is another method called repayWithATokens() which allows a user to repay with aTokens without leaving dust from interest.The user repays its debt by either burning its stable/variable debt token:
1IStableDebtToken(reserveCache.stableDebtTokenAddress).burn(2 params.onBehalfOf,3 paybackAmount4);56IVariableDebtToken(reserveCache.variableDebtTokenAddress).burn(7 params.onBehalfOf,8 paybackAmount,9 reserveCache.nextVariableBorrowIndex10);
Or by burning the aTokens received when providing liquidity through the
supply() function:1IAToken(reserveCache.aTokenAddress).burn(2 msg.sender,3 reserveCache.aTokenAddress,4 paybackAmount,5 reserveCache.nextLiquidityIndex6);
The key difference:
- Using aTokens to repay: the transaction is finished — there is no additional transfer, because as mentioned in
withdraw(), aTokens represent the user's supplied equivalent. - Otherwise: the
IERC20().safeTransferFrom()executes to transfer the specified amount frommsg.sender:
1IERC20(params.asset).safeTransferFrom(2 msg.sender,3 reserveCache.aTokenAddress,4 paybackAmount5);67IAToken(reserveCache.aTokenAddress).handleRepayment(8 msg.sender,9 params.onBehalfOf,10 paybackAmount11);
Get in touch
Understanding protocols like Aave at the code level is what sets thorough auditors apart. At Zealynx, we bring this same depth to every engagement — dissecting core logic, library interactions, and edge cases across DeFi's most forked codebases. Reach out to discuss how we can secure your protocol.
Get the DeFi Protocol Security Checklist
15 vulnerabilities every DeFi team should check before mainnet. Used by 40+ protocols.
No spam. Unsubscribe anytime.


