本文是DeFi协议代码解读系列文章的第一部分,深入研究了Aave V3协议智能合约的Pool.sol合约。文章详细介绍了Aave V3的架构、用法,以及Pool.sol合约中supply()、withdraw()、borrow()和repay()等主要功能的实现原理和代码逻辑,通过图表展示了Pool合约的主要功能如何与底层函数交互,有助于读者更好地理解Aave V3协议。
继续通过代码学习 DeFi 协议,你也可以订阅该邮件,以便在你的邮箱中收到时事通讯。在这里,我开始了 Aave V3 协议智能合约代码的第一次讲解。
深入研究如此流行的协议的代码非常有帮助,因为首先你会更好地理解它,其次,所有从 Aave 派生的新协议都将基于相同的代码,如果你最终审计其中一个协议,你将会很熟悉,这将为你节省时间。
在本文中,我们将介绍:
supply()
withdraw()
borrow()
repay()
Aave 是一个去中心化的非托管流动性市场协议,用户可以作为供应商或借款人参与。供应商向市场提供流动性以赚取被动收入,而借款人能够借入资金以应对意外支出,从而利用其持有的资产。
流动性是 Aave 协议的核心,因为它支持协议的运营和用户体验。
协议的流动性通过资产的可获得性来衡量,这些资产用于基本的协议操作,例如借用由抵押品支持的资产以及索取提供的资产以及应计收益。缺乏流动性会阻止操作。
Aave 协议 V3 合约分为两个存储库:
-> aave-v3-core
托管核心协议 V3 合约,其中包含以下逻辑:供应、借款、清算、闪电贷、a/s/v 代币、门户、池配置、预言机和利率策略。
核心协议合约属于以下 4 个类别:
在这个存储库中,你将找到与奖励、UI 数据提供者、激励数据提供者、钱包余额提供者和 WETH 网关相关的合约。
合约分为 2 个类别:
通过供应,你将根据市场借贷需求赚取被动收入。
此外,供应资产允许你通过使用你提供的资产作为抵押品来借款。
我将把 Aave V3 系列的第 1 部分重点放在这个合约上,以便我们首先了解如何组成一个更高级别的智能合约,以及它如何与其他低级别的合约链接。
Pool.sol
合约是协议的面向用户的主要合约。它公开了流动性管理方法。
Pool.sol
由特定市场的 PoolAddressesProvider 拥有。所有管理函数都可以由 PoolConfigurator 合约调用,该合约在 PoolAddressesProvider 中定义。
我创建了这张图,显示了 Pool 的主要功能如何与低级别功能交互。
这里值得一提的是,Pool 合约本身主要调用内部函数,例如 SupplyLogic.sol
中的 executeSupply()
,或者像 DataTypes
这样的库,它在结构中保存和存储函数的主要参数。
函数 supply()
function supply(
address asset,
uint256 amount,
address onBehalfOf,
uint16 referralCode
) external;
将数量的基础资产供应到储备金中,作为回报收到叠加的 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
从作为参数传递的特定 asset
更新储备金。
紧接着,它会将此数据传递给验证,其中主要的检查是此资产是否满足以下条件:
require(isActive, Errors.RESERVE_INACTIVE);
require(!isPaused, Errors.RESERVE_PAUSED);
require(!isFrozen, Errors.RESERVE_FROZEN);
最后要更新的是利率,使用 updateInterestRates()
,它接收储备金缓存、特定资产和资产数量作为参数。
safeTransferFrom
函数完成 ERC20 的供应,以及 aToken 的 mint
(我们将在后续文章中介绍这是什么)。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);
}
}
函数 withdraw()
function withdraw(
address asset,
uint256 amount,
address to
) external returns (uint256);
从储备金中提取数量的基础资产,燃烧等效的 aToken 所有权
— 例如,用户拥有 100 aUSDC,调用 withdraw() 并收到 100 USDC,燃烧 100 aUSDC
这是 IPool
接口代码库中提供的描述,它很好地总结了此函数内部发生的事情。
同样,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()
,如果它是 true
并且想要提取的金额与 userBalance
一样高,那么它将取消现有的抵押品。
if (isCollateral && amountToWithdraw == userBalance) {
userConfig.setUsingAsCollateral(reserve.id, false);
}
从 user
燃烧 aToken,并将等效数量的基础资产发送到 params.to
参数中指定的地址。
IAToken(reserveCache.aTokenAddress).burn(
msg.sender,
params.to,
amountToWithdraw,
reserveCache.nextLiquidityIndex
);
函数 borrow()
function borrow(
address asset,
uint256 amount,
uint256 interestRateMode,
uint16 referralCode,
address onBehalfOf
) external;
允许用户借用储备基础资产的特定数量,前提是借款人已经提供了足够的抵押品,或者他获得了信用委托人在相应债务代币(StableDebtToken 或 VariableDebtToken)上的足够额度
— 例如,用户借用 100 USDC,并将
onBehalfOf
作为他自己的地址传递,在他的钱包中收到 100 USDC,并收到 100 个稳定/可变债务代币,具体取决于interestRateMode
在 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
);
函数 repay()
function repay(
address asset,
uint256 amount,
uint256 interestRateMode,
address onBehalfOf
) external returns (uint256);
偿还在特定储备金上借用的金额,燃烧等效的债务代币所有权 — 例如,用户偿还 100 USDC,燃烧
onBehalfOf
地址的 100 个可变/稳定债务代币
与其余函数一样,repay
也在调用其内部函数 executeRepay
(在 BorrowLogic.sol
中):
BorrowLogic.executeRepay(
_reserves,
_reservesList,
_usersConfig[onBehalfOf],
DataTypes.ExecuteRepayParams({
asset: asset,
amount: amount,
interestRateMode: DataTypes.InterestRateMode(interestRateMode),
onBehalfOf: onBehalfOf,
useATokens: false
})
);
我想从这些参数中强调 useATokens
,在这种情况下,它直接设置为 false
,这是因为还有另一种方法允许用户使用 aToken 偿还,而不会留下利息粉尘,它被称为 repayWithATokens()
。
所以,这意味着当用户想要偿还其债务时,将通过燃烧其稳定/可变债务代币,或者燃烧通过 Supply()
函数提供流动性时收到的 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
);
这里最大的区别是:
在使用 aToken 偿还的情况下,交易已完成(实际上没有任何转移),因为你还记得,我们提到过,如果用户想要提取供应的资产,它将从 aToken 中返回等效的资产。(请随意返回到上面的 withdraw 函数解释)。
否则,仍然需要执行 IERC20().safeTransferFrom()
,以从 msg.sender
中指定的金额执行转移
IERC20(params.asset).safeTransferFrom(
msg.sender,
reserveCache.aTokenAddress,
paybackAmount
);
IAToken(reserveCache.aTokenAddress).handleRepayment(
msg.sender,
params.onBehalfOf,
paybackAmount
);
你想实现你的目标吗?你准备好投资自己来实现它们吗?
两个月前,我开始了智能合约黑客课程,从那以后,它极大地促进了我在 Web3 安全领域的职业发展。
我可以向你保证,它也会对你的职业生涯产生重大影响。
这是我的推荐链接提供的 50 美元折扣。请确保你通过此链接购买本课程。并采取措施最终朝着你的目标努力。
smartcontractshacking.com
现在你可以理解为什么我决定将其分成多个部分了吧?如果你已经做到了这一点,并且喜欢这篇文章并学到了一些新东西,我将不胜感激,如果你可以在 Medium 上添加一些掌声并在你的 Twitter 帐户中分享它,以便让尽可能多的人看到它。
由于没有太多这样的材料,因此它对很多人都非常有用。所以,谢谢你!
请确保你订阅并且不要错过接下来的关于 Aave aToken 智能合约、Oracle 和 Flash Loans 的部分!敬请关注我的 Twitter 上的 https://twitter.com/TheBlockChainer
- 原文链接: medium.com/@bloqarl/aave...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!