Aave-V3 — DeFi Protocol’s code explained. Part 1— Pool.sol
Be ready for your next smart contract audit by learning now about Aave
Continuing the saga here in DeFi protocol by code, 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()
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.
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 following 4 categories:
Configuration
Pool logic
Tokenization
Misc
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 category 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 us executeSupply()
inside the contract SupplyLogic.sol
or libraries like DataTypes
which holds and stores the function’s main parameters in structs.
function supply()
function supply(
address asset,
uint256 amount,
address onBehalfOf,
uint16 referralCode
) 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. Note: only the onBehalfOf address 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
So, inside the function we are going to find only the call to the internal function executeSupply()
inside SupplyLogic.sol
:
SupplyLogic.executeSupply(
_reserves,
_reservesList,
_usersConfig[onBehalfOf],
DataTypes.ExecuteSupplyParams({
asset: asset,
amount: amount,
onBehalfOf: onBehalfOf,
referralCode: referralCode
})
);
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:
The updates and validations:
reserve.updateState(reserveCache);
ValidationLogic.validateSupply(reserveCache, reserve, params.amount);
reserve.updateInterestRates(reserveCache, params.asset, params.amount, 0);
Where 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:
require(isActive, Errors.RESERVE_INACTIVE);
require(!isPaused, Errors.RESERVE_PAUSED);
require(!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.
The main action of the function where the supply of the ERC20 is done by using a
safeTransferFrom
function and themint
of the aTokens (we will cover what this is in coming articles).
IERC20(params.asset).safeTransferFrom(
msg.sender,
reserveCache.aTokenAddress,
params.amount
);
IAToken(reserveCache.aTokenAddress).mint(
msg.sender,
params.onBehalfOf,
params.amount,
reserveCache.nextLiquidityIndex
);
Setting the Collateral value:
This will only happen if it is the first time the sender makes a supply. And that’s why we have the condition to be set if isFirstSupply
, the validation before modify anything and finally the setter:
if (isFirstSupply) {
if (ValidationLogic.validateUseAsCollateral() {
userConfig.setUsingAsCollateral(reserve.id, true);
}
}
function withdraw()
function withdraw(
address asset,
uint256 amount,
address to
) 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
This is the description provided in the IPool
interface codebase and it is an excellent summary of what happens inside this function.
Likewise supply
, withdraw
is directly calling to the internal function executeWithdraw
inside SupplyLogic.sol
:
SupplyLogic.executeWithdraw(
_reserves,
_reservesList,
_eModeCategories,
_usersConfig[msg.sender],
DataTypes.ExecuteWithdrawParams({
asset: asset,
amount: amount,
to: to,
reservesCount: _reservesCount,
oracle: ADDRESSES_PROVIDER.getPriceOracle(),
userEModeCategory: _usersEModeCategory[msg.sender]
})
);
In this case, I would divide what’s happening inside executeWithdraw
in two parts:
Keep reading with a 7-day free trial
Subscribe to DeFi Protocol by code to keep reading this post and get 7 days of free access to the full post archives.