概述
读者可前往我的博客获得更好的阅读体验。
本文主要介绍AAVE V3
合约中的取款withdraw
函数。在阅读本文前,请读者确保已经阅读过以下文章:
- AAVE交互指南,本文将大量使用此文中给出的各种数学计算公式
- 深入解析AAVE智能合约:存款,此篇文章内给出的部分函数和大部分数据结构在本文内页有所使用,重复部分在本文内不再解释
读者也可选读深入解析AAVE智能合约:计算和利率,此文介绍了数学计算底层实现逻辑,与代码逻辑关系不大,读者可选读此文。
本文可认为是对深入解析AAVE智能合约:存款的进一步补充,由于取款逻辑较为简单,所以此文的关键在于进一步深挖某些常用函数。这些函数在《存款》一文中虽有提及但未深入探讨的函数,如updateInterestRates
等。
代码分析
在src/protocol/pool/Pool.sol
合约内,我们可以找到如下函数:
function withdraw(
address asset,
uint256 amount,
address to
) public virtual override returns (uint256) {
return
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]
})
);
}
此处各变量的具体含义如下:
_reserves
资产地址和该资产的存款数据ReserveData
的对应关系_reservesList
资产id
及其地址之间的对应关系,设置此映射目的是节省gas
_eModeCategories
E-Mode资产ideModeCategoryId
与EMode资产信息eModeCategory
的映射_usersConfig
用户地址与其设置之间的对应关系_reservesCount
当前质押品的种类数量oracle
预言机地址userEModeCategory
用户启用E-Mode
的种类
此处使用的变量已经在深入解析AAVE智能合约:存款进行了相关讨论。
使用Solidity Visual Developer
插件对其进行调用分析,结果如下:
└─ Pool::withdraw
├─ SupplyLogic::executeWithdraw | [Ext] ❗️ 🛑
│ ├─ DataTypes.ReserveData::cache | [Int] 🔒
│ ├─ DataTypes.ReserveData::updateState | [Int] 🔒 🛑
│ ├─ SupplyLogic::type
│ ├─ ValidationLogic::validateWithdraw | [Int] 🔒
│ ├─ DataTypes.ReserveData::updateInterestRates | [Int] 🔒 🛑
│ ├─ DataTypes.UserConfigurationMap::isUsingAsCollateral | [Int] 🔒
│ ├─ DataTypes.UserConfigurationMap::isBorrowingAny | [Int] 🔒
│ ├─ ValidationLogic::validateHFAndLtv | [Int] 🔒
│ │ └─ ValidationLogic::validateHealthFactor | [Int] 🔒
│ │ ├─ GenericLogic::calculateUserAccountData | [Int] 🔒
│ │ │ ├─ GenericLogic::type
│ │ │ ├─ EModeLogic::getEModeConfiguration | [Int] 🔒
│ │ │ │ └─ IPriceOracleGetter::getAssetPrice | [Ext] ❗️
│ │ │ ├─ GenericLogic::_getUserBalanceInBaseCurrency | [Priv] 🔐
│ │ │ │ └─ DataTypes.ReserveData::getNormalizedIncome | [Int] 🔒
│ │ │ ├─ EModeLogic::isInEModeCategory | [Int] 🔒
│ │ │ └─ GenericLogic::_getUserDebtInBaseCurrency | [Priv] 🔐
│ │ │ ├─ userTotalDebt::rayMul | [Int] 🔒
│ │ │ └─ DataTypes.ReserveData::getNormalizedDebt | [Int] 🔒
│ │ └─ DataTypes::CalculateUserAccountDataParams
│ └─ DataTypes.UserConfigurationMap::setUsingAsCollateral | [Int] 🔒 🛑
├─ DataTypes::ExecuteWithdrawParams
└─ IPoolAddressesProvider::getPriceOracle | [Ext] ❗️
通过此调用流程,我们可以了解到withdraw
函数基本情况。
executeWithdraw
与supply
函数的逻辑基本一致,withdraw
函数仅通过入口作用,真正的逻辑代码位于executeWithdraw
函数内,在本节内,我们将详细分析此函数的实现。
缓存与更新
与executeSupply
函数一致,executeWithdraw
在函数最开始进行了缓存及更新状态的相关操作,代码如下:
DataTypes.ReserveData storage reserve = reservesData[params.asset];
DataTypes.ReserveCache memory reserveCache = reserve.cache();
reserve.updateState(reserveCache);
此处使用的基本都在深入解析AAVE智能合约:存款内进行了相关介绍和分析。总结来说,updateState
完成了以下功能:
- 更新
Index
系列变量 - 更新风险准备金
获取存款数额
使用如下代码获得用户的存款余额情况:
uint256 userBalance = IAToken(reserveCache.aTokenAddress)
.scaledBalanceOf(msg.sender)
.rayMul(reserveCache.nextLiquidityIndex);
其中,scaledBalanceOf
函数定义如下:
function scaledBalanceOf(address user)
external
view
override
returns (uint256)
{
return super.balanceOf(user);
}
显然,通过此函数,我们可以获得经过折现后的用户存款数额。然后,经过.rayMul(reserveCache.nextLiquidityIndex);
则可以得到当前用户存款的本息和。
关于此处为什么获得的是折现后的用户存款数额? 请阅读上一篇文章内讨论存款代币的铸造这一节。简单来说,我们记录用户存款数额时就使用了折现后的数额
确定取款数额
使用以下代码确定用户的取款数额:
uint256 amountToWithdraw = params.amount;
if (params.amount == type(uint256).max) {
amountToWithdraw = userBalance;
}
此处唯一需要注意的是,当用户取款数量为type(uint256).max
时,我们将取款数额设置为用户的账户余额。
验证取款
验证取款主要通过以下代码实现:
ValidationLogic.validateWithdraw(
reserveCache,
amountToWithdraw,
userBalance
);
其中validateWithdraw
函数实现代码如下:
function validateWithdraw(
DataTypes.ReserveCache memory reserveCache,
uint256 amount,
uint256 userBalance
) internal pure {
require(amount != 0, Errors.INVALID_AMOUNT);
require(
amount <= userBalance,
Errors.NOT_ENOUGH_AVAILABLE_USER_BALANCE
);
(bool isActive, , , , bool isPaused) = reserveCache
.reserveConfiguration
.getFlags();
require(isActive, Errors.RESERVE_INACTIVE);
require(!isPaused, Errors.RESERVE_PAUSED);
}
此函数依次校验了以下内容:
- 用户取款数额是否为
0
,如果为 0 ,则报错 - 取款数额是否大于用户账户余额,如果大于则报错
- 存款池是否属于启用状态
isActive
,如果不属于则报错 - 存款池是否被暂停,如果被暂停则报错
关于
isActive
、isPaused
等内容,请阅读上一篇文章中的特殊数据结构一节。
利率更新
使用以下函数实现利率更新:
reserve.updateInterestRates(
reserveCache,
params.asset,
0,
amountToWithdraw
);
在上一篇文章中的更新利率一节内,我们讨论了updateInterestRates
中的参数问题而没有讨论具体的calculateInterestRates
函数的具体实现,我们在本文中将分析此函数。
在具体分析函数的逻辑代码之前,我们首先给出函数的调用代码:
(
vars.nextLiquidityRate,
vars.nextStableRate,
vars.nextVariableRate
) = IReserveInterestRateStrategy(reserve.interestRateStrategyAddress)
.calculateInterestRates(
DataTypes.CalculateInterestRatesParams({
unbacked: reserveCache
.reserveConfiguration
.getUnbackedMintCap() != 0
? reserve.unbacked
: 0,
liquidityAdded: liquidityAdded,
liquidityTaken: liquidityTaken,
totalStableDebt: reserveCache.nextTotalStableDebt,
totalVariableDebt: vars.totalVariableDebt,
averageStableBorrowRate: reserveCache
.nextAvgStableBorrowRate,
reserveFactor: reserveCache.reserveFactor,
reserve: reserveAddress,
aToken: reserveCache.aTokenAddress
})
);
此函数使用的参数含义请参考上一篇文章。
在了解具体的输入参数后,我们着手分析函数的逻辑部分。在阅读以下内容前,请读者复习AAVE交互指南中的贷款利率计算一节。
在calculateInterestRates
函数内,我们首先定义了一系列初始变量,定义代码如下:
CalcInterestRatesLocalVars memory vars;
vars.totalDebt = params.totalStableDebt + params.totalVariableDebt;
vars.currentLiquidityRate = 0;
vars.currentVariableBorrowRate = _baseVariableBorrowRate;
vars.currentStableBorrowRate = getBaseStableBorrowRate();
其中,各变量含义如下:
totalDebt
当前资产的总贷出数量currentLiquidityRate
存款利率currentVariableBorrowRate
浮动利率currentStableBorrowRate
固定利率
此处
_baseVariableBorrowRate
即初始化浮动利率,而getBaseStableBorrowRate()
为初始化固定利率。在之前文章内,我们使用 R 0 R_0 R0 表示此数值。
接下来,我们需要获得 利用率 U U U 和 固定利率负债与浮动利率负债之比 r a t i o ratio ratio 这两个参数。其计算公式分别为:
U = T o t a l b o r r o w e d T o t a l s u p p l i e d U = \frac{Total\ borrowed}{Total\ supplied} U=Total suppliedTotal borrowed
r a t i o = T o t a l S t a b l e D e b t T o t a l D e b t ratio = \frac{Total\ Stable\ Debt}{Total\ Debt} ratio=Total DebtTotal Stable Debt
具体实现代码如下:
if (vars.totalDebt != 0) {
// 计算 ratio 变量值
vars.stableToTotalDebtRatio = params.totalStableDebt.rayDiv(
vars.totalDebt
);
// 计算当前流动性池的资产
vars.availableLiquidity =
IERC20(params.reserve).balanceOf(params.aToken) +
params.liquidityAdded -
params.liquidityTaken;
// 计算当前总供给 Total supplied
// 使用 资产 + 负债
vars.availableLiquidityPlusDebt =
vars.availableLiquidity +
vars.totalDebt;
// 计算 U (未考虑跨链数据)
vars.borrowUsageRatio = vars.totalDebt.rayDiv(
vars.availableLiquidityPlusDebt
);
// 计算 考虑跨链数据后修正的 U
vars.supplyUsageRatio = vars.totalDebt.rayDiv(
vars.availableLiquidityPlusDebt + params.unbacked
);
}
我们首先处理二阶段利率问题,公式如下:
R b a s e = { R 0 + U t