Aave V3 — DeFi协议代码详解第一部分 — Pool.sol

  • zealynx
  • 发布于 2023-04-19 14:42
  • 阅读 36

本文是Aave V3协议智能合约系列文章的第一部分,深入解析了Aave V3协议的核心机制。文章详细介绍了Aave协议的架构,并重点分析了Pool.sol合约中的supply()withdraw()borrow()repay()这四个主要函数的工作原理和内部调用流程。

通过代码继续DeFi协议的传奇,在此我开始了Aave V3协议智能合约的演练第一部分。

深入研究这些流行协议的代码非常有帮助,首先你会更好地理解它,其次所有从Aave分叉的新协议都将基于相同的代码,如果你最终审计其中一个,你将已经熟悉,这将为你节省时间。

本文将涵盖:

  • 介绍与架构

  • 用途

  • Pool.sol主要函数:

    • supply()
    • withdraw()
    • borrow()
    • repay()
    • *

什么是 Aave?

Aave是一个去中心化的非托管流动性市场协议,用户可以作为供应者或借款人参与。供应者向市场提供流动性以赚取被动收入,而借款人则能够借款应对意外开支,利用其持有的资产。

流动性是Aave协议的核心,因为它支持协议的运作和用户体验。

协议的流动性通过资产的可用性来衡量,这些资产用于基本的协议操作,例如借入以抵押品为后盾的资产,以及提取已供应资产和累计收益。缺乏流动性将阻碍操作。

架构

Aave协议V3合约分为两个代码仓库:

aave-v3-core — 托管核心协议V3合约,其中包含供应、借款、清算、闪电贷、a/s/v代币、Portal、池配置、预言机和利率策略的逻辑。

核心协议合约分为4类:

  • 配置
  • 池逻辑
  • 代币化
  • 其他

aave-v3-periphery — 在这个代码仓库中,你会找到与奖励、UI数据提供者、激励数据提供者、钱包余额提供者和WETH网关相关的合约。

合约分为2类:

  • 奖励
  • 其他

我们能用 Aave 协议做什么?

供应与赚取。通过供应,你将根据市场借款需求赚取被动收入。

借款。此外,供应资产允许你通过将你供应的资产用作抵押品来进行借款。


Pool.sol 智能合约

Aave V3 系列的第一部分,我将重点关注这个合约,以便我们首先了解一个更高级别的智能合约是如何组成的,以及它如何与低级合约关联。

Pool.sol合约是协议中面向用户的主要合约。它暴露了流动性管理方法。

Pool.sol 由特定市场的 PoolAddressesProvider 拥有。所有管理功能都可由 PoolConfigurator 合约调用,该合约在 PoolAddressesProvider 中定义。

我创建了这个图表,展示了Pool的主要功能如何与低级功能交互。

Pool.sol function interactions diagram

这里值得一提的是,在Pool合约本身中,它主要会调用内部函数,例如 SupplyLogic.sol 合约内的 executeSupply(),或者像 DataTypes 这样的库,后者将函数的主要参数存储在结构体中。


函数 supply()

function supply(
  address asset,
  uint256 amount,
  address onBehalfOf,
  uint16 referralCode
) external;
  • asset:供应到池中的资产地址。
  • amount:供应的资产数量。
  • onBehalfOf:将接收相应aToken的地址。只有 onBehalfOf 地址才能从池中提取资产。
  • referralCode:用于第三方推荐程序集成的唯一代码。不使用推荐时请用0。

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() 内部,我们可以将其分为三个部分:

1. 更新和验证

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() 更新利率,该函数接收缓存的储备金、特定资产和资产数量作为参数。

2. 主要操作 — 供应和铸造

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
);

3. 设置抵押品价值

这仅在发送者首次供应时发生。因此,我们有 isFirstSupply 的条件,在修改任何内容之前进行验证,最后是设置器:

if (isFirstSupply) {
  if (ValidationLogic.validateUseAsCollateral()) {
    userConfig.setUsingAsCollateral(reserve.id, true);
  }
}

函数 withdraw()

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 内部有两个主要部分:

1. 更新和验证

ValidationLogic.validateWithdraw(reserveCache, amountToWithdraw, userBalance);

reserve.updateInterestRates(reserveCache, params.asset, 0, amountToWithdraw);

executeSupply 一样,它验证提供的参数以更新利率。

然后它检查 isUsingAsCollateral(),如果为真且要提取的数量等于 userBalance,则将取消现有抵押品:

if (isCollateral && amountToWithdraw == userBalance) {
  userConfig.setUsingAsCollateral(reserve.id, false);
}

2. 销毁 aToken

销毁用户拥有的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;

允许用户借入特定 amount 的储备金基础资产,前提是借款人已供应足够的抵押品,或通过信用委托人在相应的债务代币(StableDebtTokenVariableDebtToken)上获得了足够的许可。

例如,用户借入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
);

函数 repay()

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
);

主要区别:

  • 使用aToken偿还:交易完成 — 没有额外的转账,因为如 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
zealynx
zealynx
江湖只有他的大名,没有他的介绍。