本文是Aave V3协议智能合约系列文章的第一部分,深入解析了Aave V3协议的核心机制。文章详细介绍了Aave协议的架构,并重点分析了Pool.sol合约中的supply()、withdraw()、borrow()、repay()这四个主要函数的工作原理和内部调用流程。
通过代码继续DeFi协议的传奇,在此我开始了Aave V3协议智能合约的演练第一部分。
深入研究这些流行协议的代码非常有帮助,首先你会更好地理解它,其次所有从Aave分叉的新协议都将基于相同的代码,如果你最终审计其中一个,你将已经熟悉,这将为你节省时间。
本文将涵盖:
介绍与架构
用途
Pool.sol主要函数:
supply()withdraw()borrow()repay()Aave是一个去中心化的非托管流动性市场协议,用户可以作为供应者或借款人参与。供应者向市场提供流动性以赚取被动收入,而借款人则能够借款应对意外开支,利用其持有的资产。
流动性是Aave协议的核心,因为它支持协议的运作和用户体验。
协议的流动性通过资产的可用性来衡量,这些资产用于基本的协议操作,例如借入以抵押品为后盾的资产,以及提取已供应资产和累计收益。缺乏流动性将阻碍操作。
Aave协议V3合约分为两个代码仓库:
aave-v3-core — 托管核心协议V3合约,其中包含供应、借款、清算、闪电贷、a/s/v代币、Portal、池配置、预言机和利率策略的逻辑。
核心协议合约分为4类:
aave-v3-periphery — 在这个代码仓库中,你会找到与奖励、UI数据提供者、激励数据提供者、钱包余额提供者和WETH网关相关的合约。
合约分为2类:
供应与赚取。通过供应,你将根据市场借款需求赚取被动收入。
借款。此外,供应资产允许你通过将你供应的资产用作抵押品来进行借款。
Aave V3 系列的第一部分,我将重点关注这个合约,以便我们首先了解一个更高级别的智能合约是如何组成的,以及它如何与低级合约关联。
Pool.sol合约是协议中面向用户的主要合约。它暴露了流动性管理方法。
Pool.sol 由特定市场的 PoolAddressesProvider 拥有。所有管理功能都可由 PoolConfigurator 合约调用,该合约在 PoolAddressesProvider 中定义。
我创建了这个图表,展示了Pool的主要功能如何与低级功能交互。

这里值得一提的是,在Pool合约本身中,它主要会调用内部函数,例如 SupplyLogic.sol 合约内的 executeSupply(),或者像 DataTypes 这样的库,后者将函数的主要参数存储在结构体中。
function supply(
address asset,
uint256 amount,
address onBehalfOf,
uint16 referralCode
) external;
onBehalfOf 地址才能从池中提取资产。将 amount 的基础资产供应到储备金中,并收到相应的aToken。例如,用户供应100 USDC,并收到100 aUSDC。
在该函数内部,我们只发现调用了 SupplyLogic.sol 中的内部函数 executeSupply():
SupplyLogic.executeSupply(
_reserves,
_reservesList,
_usersConfig[onBehalfOf],
DataTypes.ExecuteSupplyParams({
asset: asset,
amount: amount,
onBehalfOf: onBehalfOf,
referralCode: referralCode
})
);
不要因为在函数的参数中看到 ExecuteSupplyParams 调用而感到不知所措。它只是传递了一个包装在结构体中的参数列表。
现在,查看 executeSupply() 内部,我们可以将其分为三个部分:
reserve.updateState(reserveCache);
ValidationLogic.validateSupply(reserveCache, reserve, params.amount);
reserve.updateInterestRates(reserveCache, params.asset, params.amount, 0);
首先,它将使用从作为参数传入的特定资产提供的 reserveData 来更新储备金。
紧接着,它将这些数据传递给验证逻辑,主要检查将是该资产是否满足以下条件:
require(isActive, Errors.RESERVE_INACTIVE);
require(!isPaused, Errors.RESERVE_PAUSED);
require(!isFrozen, Errors.RESERVE_FROZEN);
最后更新的是使用 updateInterestRates() 更新利率,该函数接收缓存的储备金、特定资产和资产数量作为参数。
ERC20的供应是通过使用 safeTransferFrom 函数完成的,aToken的铸造如下:
IERC20(params.asset).safeTransferFrom(
msg.sender,
reserveCache.aTokenAddress,
params.amount
);
IAToken(reserveCache.aTokenAddress).mint(
msg.sender,
params.onBehalfOf,
params.amount,
reserveCache.nextLiquidityIndex
);
这仅在发送者首次供应时发生。因此,我们有 isFirstSupply 的条件,在修改任何内容之前进行验证,最后是设置器:
if (isFirstSupply) {
if (ValidationLogic.validateUseAsCollateral()) {
userConfig.setUsingAsCollateral(reserve.id, true);
}
}
function withdraw(
address asset,
uint256 amount,
address to
) external returns (uint256);
从储备金中提取 amount 的基础资产,并销毁相应的aToken。例如,用户持有100 aUSDC,调用 withdraw() 并收到100 USDC,销毁100 aUSDC。
与 supply 类似,withdraw 直接调用 SupplyLogic.sol 中的内部函数 executeWithdraw:
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]
})
);
executeWithdraw 内部有两个主要部分:
ValidationLogic.validateWithdraw(reserveCache, amountToWithdraw, userBalance);
reserve.updateInterestRates(reserveCache, params.asset, 0, amountToWithdraw);
与 executeSupply 一样,它验证提供的参数以更新利率。
然后它检查 isUsingAsCollateral(),如果为真且要提取的数量等于 userBalance,则将取消现有抵押品:
if (isCollateral && amountToWithdraw == userBalance) {
userConfig.setUsingAsCollateral(reserve.id, false);
}
销毁用户拥有的aToken,并将相应数量的基础资产发送到 params.to 中指定的地址:
IAToken(reserveCache.aTokenAddress).burn(
msg.sender,
params.to,
amountToWithdraw,
reserveCache.nextLiquidityIndex
);
function borrow(
address asset,
uint256 amount,
uint256 interestRateMode,
uint16 referralCode,
address onBehalfOf
) external;
允许用户借入特定 amount 的储备金基础资产,前提是借款人已供应足够的抵押品,或通过信用委托人在相应的债务代币(StableDebtToken 或 VariableDebtToken)上获得了足够的许可。
例如,用户借入100 USDC,将他自己的地址作为 onBehalfOf 传入,他的钱包收到100 USDC,并根据 interestRateMode 收到100个稳定/可变债务代币。
borrow 内部直接调用 BorrowLogic.sol 中的内部函数 executeBorrow:
BorrowLogic.executeBorrow(
_reserves,
_reservesList,
_eModeCategories,
_usersConfig[onBehalfOf],
DataTypes.ExecuteBorrowParams({
asset: asset,
user: msg.sender,
onBehalfOf: onBehalfOf,
amount: amount,
interestRateMode: DataTypes.InterestRateMode(interestRateMode),
referralCode: referralCode,
releaseUnderlying: true,
maxStableRateBorrowSizePercent: _maxStableRateBorrowSizePercent,
reservesCount: _reservesCount,
oracle: ADDRESSES_PROVIDER.getPriceOracle(),
userEModeCategory: _usersEModeCategory[onBehalfOf],
priceOracleSentinel: ADDRESSES_PROVIDER.getPriceOracleSentinel()
})
);
这里需要强调的一点是 interestRateMode。它是一个枚举,决定将铸造哪种债务代币:
enum InterestRateMode {
NONE,
STABLE,
VARIABLE
}
如果是 STABLE,它将执行:
IStableDebtToken(reserveCache.stableDebtTokenAddress).mint(
params.user,
params.onBehalfOf,
params.amount,
currentStableRate
);
如果是 VARIABLE:
IVariableDebtToken(reserveCache.variableDebtTokenAddress).mint(
params.user,
params.onBehalfOf,
params.amount,
reserveCache.nextVariableBorrowIndex
);
一旦代币被铸造,基础资产的实际转移就会发生:
IAToken(reserveCache.aTokenAddress).transferUnderlyingTo(
params.user,
params.amount
);
function repay(
address asset,
uint256 amount,
uint256 interestRateMode,
address onBehalfOf
) external returns (uint256);
偿还特定储备金上的借入 amount,销毁相应的债务代币。例如,用户偿还100 USDC,销毁 onBehalfOf 地址的100个可变/稳定债务代币。
与其余函数一样,repay 调用 BorrowLogic.sol 中的内部函数 executeRepay:
BorrowLogic.executeRepay(
_reserves,
_reservesList,
_usersConfig[onBehalfOf],
DataTypes.ExecuteRepayParams({
asset: asset,
amount: amount,
interestRateMode: DataTypes.InterestRateMode(interestRateMode),
onBehalfOf: onBehalfOf,
useATokens: false
})
);
请注意,这里的 useATokens 设置为 false — 还有另一个方法 repayWithATokens() 允许用户使用aToken进行偿还,而不会留下利息零头。
用户通过销毁其稳定/可变债务代币来偿还债务:
IStableDebtToken(reserveCache.stableDebtTokenAddress).burn(
params.onBehalfOf,
paybackAmount
);
IVariableDebtToken(reserveCache.variableDebtTokenAddress).burn(
params.onBehalfOf,
paybackAmount,
reserveCache.nextVariableBorrowIndex
);
或者通过销毁通过 supply() 函数提供流动性时收到的aToken:
IAToken(reserveCache.aTokenAddress).burn(
msg.sender,
reserveCache.aTokenAddress,
paybackAmount,
reserveCache.nextLiquidityIndex
);
主要区别:
withdraw() 中所述,aToken代表用户供应的等价物。IERC20().safeTransferFrom() 执行以从 msg.sender 转账指定金额:IERC20(params.asset).safeTransferFrom(
msg.sender,
reserveCache.aTokenAddress,
paybackAmount
);
IAToken(reserveCache.aTokenAddress).handleRepayment(
msg.sender,
params.onBehalfOf,
paybackAmount
);
在代码层面理解像Aave这样的协议是区分专业审计员的关键。在Zealynx,我们以同样的深度对待每一次合作——剖析DeFi最常被分叉的代码库中的核心逻辑、库交互和边缘情况。联系我们以讨论我们如何保护你的协议。
- 原文链接: zealynx.io/blogs/aave-v3...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!