深入解析AAVE智能合约:取款

本文详细分析了AAVE智能合约中的`executeWithdraw`函数,包括缓存与更新、获取存款数额、确定取款数额、验证取款、利率更新、代币燃烧、质押属性处理和释放事件等步骤。讲解了取款过程中涉及的计算、校验和状态更新,深入探讨了关键函数的实现逻辑,以帮助读者理解AAVE智能合约的取款机制。
摘要由CSDN通过智能技术生成

概述

读者可前往我的博客获得更好的阅读体验。

本文主要介绍AAVE V3合约中的取款withdraw函数。在阅读本文前,请读者确保已经阅读过以下文章:

  1. AAVE交互指南,本文将大量使用此文中给出的各种数学计算公式
  2. 深入解析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]
            })
        );
}

此处各变量的具体含义如下:

  1. _reserves 资产地址和该资产的存款数据ReserveData的对应关系
  2. _reservesList 资产id及其地址之间的对应关系,设置此映射目的是节省gas
  3. _eModeCategories E-Mode资产ideModeCategoryId与EMode资产信息eModeCategory的映射
  4. _usersConfig 用户地址与其设置之间的对应关系
  5. _reservesCount 当前质押品的种类数量
  6. oracle 预言机地址
  7. 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完成了以下功能:

  1. 更新Index系列变量
  2. 更新风险准备金

获取存款数额

使用如下代码获得用户的存款余额情况:

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

此函数依次校验了以下内容:

  1. 用户取款数额是否为0,如果为 0 ,则报错
  2. 取款数额是否大于用户账户余额,如果大于则报错
  3. 存款池是否属于启用状态isActive,如果不属于则报错
  4. 存款池是否被暂停,如果被暂停则报错

关于isActiveisPaused等内容,请阅读上一篇文章中的特殊数据结构一节。

利率更新

使用以下函数实现利率更新:

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

其中,各变量含义如下:

  1. totalDebt 当前资产的总贷出数量
  2. currentLiquidityRate 存款利率
  3. currentVariableBorrowRate 浮动利率
  4. 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

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WongSSH

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值