AaveV2分析(一)准备aave官网(aave的官网十分注重用户的体验,提供了测试网去连接,感兴趣的可以去玩玩)aave源码仓库aave文档aave白皮书LendingPool.solLendingPool合约是Aave这个借贷DAPP的基石,里面实现了一个借贷Dapp的五
aave 官网(aave的官网十分注重用户的体验,提供了测试网去连接,感兴趣的可以去玩玩)
LendingPool合约是Aave这个借贷DAPP的基石,里面实现了一个借贷Dapp的五个基本操作:存款(Deposit), 取款(Withdrawal), 借贷(Borrow), 偿还(Repay) 和清算(Liquidation)。
function deposit(
address asset,
uint256 amount,
address onBehalfOf,
uint16 referralCode
) external override whenNotPaused {
DataTypes.ReserveData storage reserve = _reserves[asset];
ValidationLogic.validateDeposit(reserve, amount);
address aToken = reserve.aTokenAddress;
reserve.updateState();
reserve.updateInterestRates(asset, aToken, amount, 0);
IERC20(asset).safeTransferFrom(msg.sender, aToken, amount);
bool isFirstDeposit = IAToken(aToken).mint(onBehalfOf, amount, reserve.liquidityIndex);
if (isFirstDeposit) {
_usersConfig[onBehalfOf].setUsingAsCollateral(reserve.id, true);
emit ReserveUsedAsCollateralEnabled(asset, onBehalfOf);
}
emit Deposit(asset, msg.sender, onBehalfOf, amount, referralCode);
}
在deposit方法中首先获取了存入的资产合约的储备池对象,
DataTypes.ReserveData storage reserve = _reserves[asset];
然后验证储备池是否处于可用状态和存入金额大于0,
ValidationLogic.validateDeposit(reserve, amount);
再更新储备池中的复利因子,储备池的固定利率和浮动利率(UpdateState,updateInterestRates会在后面仔细讨论),
reserve.updateState();
reserve.updateInterestRates(asset, aToken, amount, 0);
再转移用户的资产,
IERC20(asset).safeTransferFrom(msg.sender, aToken, amount);
再根据流动性index铸造新的aToken代币并转入用户账户地址(会在后面仔细讨论),
bool isFirstDeposit = IAToken(aToken).mint(onBehalfOf, amount, reserve.liquidityIndex);
如果是第一次存款, 设置用户存入的金额可以作为借贷抵押品
if (isFirstDeposit) {
_usersConfig[onBehalfOf].setUsingAsCollateral(reserve.id, true);
emit ReserveUsedAsCollateralEnabled(asset, onBehalfOf);
}
对于每种代币,AAVE都会建立一个专门的储备池。针对该代币的存款,取款,借贷和偿还都是在这个储备池上进行。
储备池结构
struct ReserveData {
//stores the reserve configuration
ReserveConfigurationMap configuration;
//the liquidity index. Expressed in ray
uint128 liquidityIndex;
//variable borrow index. Expressed in ray
uint128 variableBorrowIndex;
//the current supply rate. Expressed in ray
uint128 currentLiquidityRate;
//the current variable borrow rate. Expressed in ray
uint128 currentVariableBorrowRate;
//the current stable borrow rate. Expressed in ray
uint128 currentStableBorrowRate;
uint40 lastUpdateTimestamp;
//tokens addresses
address aTokenAddress;
address stableDebtTokenAddress;
address variableDebtTokenAddress;
//address of the interest rate strategy
address interestRateStrategyAddress;
//the id of the reserve. Represents the position in the list of the active reserves
uint8 id;
}
储备池中比较重要的参数可以分成三部分,
1.第一部分是关于存款凭证和债务凭证的合约地址:
aTokenAddress:存款凭证的合约地址
stableDebtTokenAddress:固定贷款债务凭证的合约地址,
variableDebtTokenAddress:浮动利率债务凭证的合约地址
2.第二部分是计算复利用的序列索引, 我们会在updateState函数中介绍它的作用.
liquidityIndex:根据加权利率计算复利的序列索引
variableBorrowIndex:计算浮动利率复利的序列索引
3.第三部分是利率
currentLiquidtyRate: 加权了浮动利率和固定利率后的当前利率
currentVariableBorrowRate:当前浮动利率
currentStableBorrowRate:当前固定利率
interestRateStrategy: 利率模型合约地址
function updateState(DataTypes.ReserveData storage reserve) internal {
uint256 scaledVariableDebt =
IVariableDebtToken(reserve.variableDebtTokenAddress).scaledTotalSupply();
uint256 previousVariableBorrowIndex = reserve.variableBorrowIndex;
uint256 previousLiquidityIndex = reserve.liquidityIndex;
uint40 lastUpdatedTimestamp = reserve.lastUpdateTimestamp;
(uint256 newLiquidityIndex, uint256 newVariableBorrowIndex) =
_updateIndexes(
reserve,
scaledVariableDebt,
previousLiquidityIndex,
previousVariableBorrowIndex,
lastUpdatedTimestamp
);
_mintToTreasury(
reserve,
scaledVariableDebt,
previousVariableBorrowIndex,
newLiquidityIndex,
newVariableBorrowIndex,
lastUpdatedTimestamp
);
}
updateState函数的功能可以分成两部分,第一部分是更新计算复利的序列索引,第二部分是通过铸aToken。
先从第一部分更新序列索引开始。
function _updateIndexes(
DataTypes.ReserveData storage reserve,
uint256 scaledVariableDebt,
uint256 liquidityIndex,
uint256 variableBorrowIndex,
uint40 timestamp
) internal returns (uint256, uint256) {
uint256 currentLiquidityRate = reserve.currentLiquidityRate;
uint256 newLiquidityIndex = liquidityIndex;
uint256 newVariableBorrowIndex = variableBorrowIndex;
//only cumulating if there is any income being produced
if (currentLiquidityRate > 0) {
uint256 cumulatedLiquidityInterest =
MathUtils.calculateLinearInterest(currentLiquidityRate, timestamp);
newLiquidityIndex = cumulatedLiquidityInterest.rayMul(liquidityIndex);
require(newLiquidityIndex <= type(uint128).max, Errors.RL_LIQUIDITY_INDEX_OVERFLOW);
reserve.liquidityIndex = uint128(newLiquidityIndex);
//as the liquidity rate might come only from stable rate loans, we need to ensure
//that there is actual variable debt before accumulating
if (scaledVariableDebt != 0) {
uint256 cumulatedVariableBorrowInterest =
MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp);
newVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul(variableBorrowIndex);
require(
newVariableBorrowIndex <= type(uint128).max,
Errors.RL_VARIABLE_BORROW_INDEX_OVERFLOW
);
reserve.variableBorrowIndex = uint128(newVariableBorrowIndex);
}
}
//solium-disable-next-line
reserve.lastUpdateTimestamp = uint40(block.timestamp);
return (newLiquidityIndex, newVariableBorrowIndex);
}
可变利率复利可以通过不断相乘得到。例如当前是第n期(1+Rate_1),(1+Rate_2)(1+Rate_1),直到(1+Raten)(1+Rate(n-1))…(1+Rate_1)这形成了一个复利因子的序列。任何时刻,我们只需要把本金乘以对应的第i期复利因子,就可以计算得到第i期后吗,连本带利的值是多少。 index(i)就表示第i期的复利因子。AAVE 也使用了类似的做法计算复利。
接下来,我们看看Liquidity Index 是在何时,以何种方式被更新的。
Liquditiy Index的计算方式如下:
uint256 cumulatedLiquidityInterest =
MathUtils.calculateLinearInterest(currentLiquidityRate, timestamp);
newLiquidityIndex = cumulatedLiquidityInterest.rayMul(liquidityIndex);
require(newLiquidityIndex <= type(uint128).max, Errors.RL_LIQUIDITY_INDEX_OVERFLOW);
换成数学公式,就是newLiquidityIndex = (currentLiquidRatetimeinterval+1)LiquidityIndex。
浮动利率复利因子计算如下:
uint256 cumulatedVariableBorrowInterest =
MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp);
newVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul(variableBorrowIndex);
require(
newVariableBorrowIndex <= type(uint128).max,
Errors.RL_VARIABLE_BORROW_INDEX_OVERFLOW
);
换成数学公式,就是newVariableBorrowIndex=(1+currentVariableBorroowRate)Exp(time interval)* variableBorrowIndex。
第2部分铸aToken
function _mintToTreasury(
DataTypes.ReserveData storage reserve,
uint256 scaledVariableDebt,
uint256 previousVariableBorrowIndex,
uint256 newLiquidityIndex,
uint256 newVariableBorrowIndex,
uint40 timestamp
) internal {
MintToTreasuryLocalVars memory vars;
vars.reserveFactor = reserve.configuration.getReserveFactor();
if (vars.reserveFactor == 0) {
return;
}
//fetching the principal, total stable debt and the avg stable rate
(
vars.principalStableDebt,
vars.currentStableDebt,
vars.avgStableRate,
vars.stableSupplyUpdatedTimestamp
) = IStableDebtToken(reserve.stableDebtTokenAddress).getSupplyData();
//calculate the last principal variable debt
vars.previousVariableDebt = scaledVariableDebt.rayMul(previousVariableBorrowIndex);
//calculate the new total supply after accumulation of the index
vars.currentVariableDebt = scaledVariableDebt.rayMul(newVariableBorrowIndex);
//calculate the stable debt until the last timestamp update
vars.cumulatedStableInterest = MathUtils.calculateCompoundedInterest(
vars.avgStableRate,
vars.stableSupplyUpdatedTimestamp,
timestamp
);
vars.previousStableDebt = vars.principalStableDebt.rayMul(vars.cumulatedStableInterest);
//debt accrued is the sum of the current debt minus the sum of the debt at the last update
vars.totalDebtAccrued = vars
.currentVariableDebt
.add(vars.currentStableDebt)
.sub(vars.previousVariableDebt)
.sub(vars.previousStableDebt);
vars.amountToMint = vars.totalDebtAccrued.percentMul(vars.reserveFactor);
if (vars.amountToMint != 0) {
IAToken(reserve.aTokenAddress).mintToTreasury(vars.amountToMint, newLiquidityIndex);
}
}
首先定义了一个MintToTreasuryLocalVars结构体
struct MintToTreasuryLocalVars {
uint256 currentStableDebt;
uint256 principalStableDebt;
uint256 previousStableDebt;
uint256 currentVariableDebt;
uint256 previousVariableDebt;
uint256 avgStableRate;
uint256 cumulatedStableInterest;
uint256 totalDebtAccrued;
uint256 amountToMint;
uint256 reserveFactor;
uint40 stableSupplyUpdatedTimestamp;
}
这结构可以看做一张动态的银行资产负债表。
固定利率的债务获取可以通过StableDebtToken::getSupplyData () 函数获取。
(
vars.principalStableDebt,
vars.currentStableDebt,
vars.avgStableRate,
vars.stableSupplyUpdatedTimestamp
) = IStableDebtToken(reserve.stableDebtTokenAddress).getSupplyData();
浮动利率债务和固定利率债务有点不一样,使用了scaled缩放的乘积技巧。具体来说就是存储浮动利率债务除以variableBorrowIndex的值ScVB。每次借贷时改变ScVB,接着反向计算出改变后的浮动利率债务。
//calculate the last principal variable debt
vars.previousVariableDebt = scaledVariableDebt.rayMul(previousVariableBorrowIndex);
//calculate the new total supply after accumulation of the index
vars.currentVariableDebt = scaledVariableDebt.rayMul(newVariableBorrowIndex);
计算前一次固定利率债务本息和,AAVE直接使用了平均固定利率进行复利计算。我们可以认为平均固定利率可以看做一系列不同利率的时间加权平均值。
vars.cumulatedStableInterest = MathUtils.calculateCompoundedInterest(
vars.avgStableRate,
vars.stableSupplyUpdatedTimestamp,
timestamp
);
vars.previousStableDebt = vars.principalStableDebt.rayMul(vars.cumulatedStableInterest);
然后就是计算前后债务之差,差值就是本次调用前后的利息增加值。
vars.totalDebtAccrued = vars
.currentVariableDebt
.add(vars.currentStableDebt)
.sub(vars.previousVariableDebt)
.sub(vars.previousStableDebt);
最后一步就是乘以储备因子,并且铸造。
vars.amountToMint = vars.totalDebtAccrued.percentMul(vars.reserveFactor);
if (vars.amountToMint != 0) {
IAToken(reserve.aTokenAddress).mintToTreasury(vars.amountToMint, newLiquidityIndex);
}
此处值得注意的是,增加的利息额还要除以复利因子才是铸造的数量,这就像把利息用复利因子做了一次折现,换算成对应的aToken本金。
function updateInterestRates(
DataTypes.ReserveData storage reserve,
address reserveAddress,
address aTokenAddress,
uint256 liquidityAdded,
uint256 liquidityTaken
) internal {
UpdateInterestRatesLocalVars memory vars;
vars.stableDebtTokenAddress = reserve.stableDebtTokenAddress;
(vars.totalStableDebt, vars.avgStableRate) = IStableDebtToken(vars.stableDebtTokenAddress)
.getTotalSupplyAndAvgRate();
//calculates the total variable debt locally using the scaled total supply instead
//of totalSupply(), as it's noticeably cheaper. Also, the index has been
//updated by the previous updateState() call
vars.totalVariableDebt = IVariableDebtToken(reserve.variableDebtTokenAddress)
.scaledTotalSupply()
.rayMul(reserve.variableBorrowIndex);
(
vars.newLiquidityRate,
vars.newStableRate,
vars.newVariableRate
) = IReserveInterestRateStrategy(reserve.interestRateStrategyAddress).calculateInterestRates(
reserveAddress,
aTokenAddress,
liquidityAdded,
liquidityTaken,
vars.totalStableDebt,
vars.totalVariableDebt,
vars.avgStableRate,
reserve.configuration.getReserveFactor()
);
require(vars.newLiquidityRate <= type(uint128).max, Errors.RL_LIQUIDITY_RATE_OVERFLOW);
require(vars.newStableRate <= type(uint128).max, Errors.RL_STABLE_BORROW_RATE_OVERFLOW);
require(vars.newVariableRate <= type(uint128).max, Errors.RL_VARIABLE_BORROW_RATE_OVERFLOW);
reserve.currentLiquidityRate = uint128(vars.newLiquidityRate);
reserve.currentStableBorrowRate = uint128(vars.newStableRate);
reserve.currentVariableBorrowRate = uint128(vars.newVariableRate);
emit ReserveDataUpdated(
reserveAddress,
vars.newLiquidityRate,
vars.newStableRate,
vars.newVariableRate,
reserve.liquidityIndex,
reserve.variableBorrowIndex
);
}
AAVE的借贷利率,可以分成两部分。一部分是依赖于外部Oracle和固定债务数量而确定的固定利率,另一部分则是根据流动性的增减而变化的浮动利率。
首先还是定义了一个数据结构
struct UpdateInterestRatesLocalVars {
address stableDebtTokenAddress;
uint256 availableLiquidity;
uint256 totalStableDebt;
uint256 newLiquidityRate;
uint256 newStableRate;
uint256 newVariableRate;
uint256 avgStableRate;
uint256 totalVariableDebt;
}
具体作用显而易见,就不再赘述。
接下来,我们看看固定利率,浮动利率和加权后的流动性利率是如何计算的。
首先获取债务信息
(vars.totalStableDebt, vars.avgStableRate) = IStableDebtToken(vars.stableDebtTokenAddress)
.getTotalSupplyAndAvgRate();
vars.totalVariableDebt = IVariableDebtToken(reserve.variableDebtTokenAddress)
.scaledTotalSupply()
.rayMul(reserve.variableBorrowIndex);
然后更新利率,AAVE把利率更新操作包装在IReserveInterestRatesStrategy接口的calculateInterstRates函数中。这个接口由DefaultReserveInterestRateStrategy合约实现。
function calculateInterestRates(
address reserve,
address aToken,
uint256 liquidityAdded,
uint256 liquidityTaken,
uint256 totalStableDebt,
uint256 totalVariableDebt,
uint256 averageStableBorrowRate,
uint256 reserveFactor
)
external
view
override
returns (
uint256,
uint256,
uint256
)
{
uint256 availableLiquidity = IERC20(reserve).balanceOf(aToken);
//avoid stack too deep
availableLiquidity = availableLiquidity.add(liquidityAdded).sub(liquidityTaken);
return
calculateInterestRates(
reserve,
availableLiquidity,
totalStableDebt,
totalVariableDebt,
averageStableBorrowRate,
reserveFactor
);
}
这个函数首先获取了储备池对应的aToken的所有流动性(即总数)。然后更新了流动性。重载调用了calculateInterstRates函数,真正的逻辑全在里面。
首先计算了总债务与储备池的利用率。
vars.totalDebt = totalStableDebt.add(totalVariableDebt);
vars.utilizationRate = vars.totalDebt == 0
? 0
: vars.totalDebt.rayDiv(availableLiquidity.add(vars.totalDebt));
然后从外界oracle获取一个初始值,作为计算固定利率的基础。
vars.currentStableBorrowRate = ILendingRateOracle(addressesProvider.getLendingRateOracle())
.getMarketBorrowRate(reserve);
接下来就是利率的更新了
这里的#可以为V或S,带入后得到VRt或SRt分别表示浮动利率和稳定利率。 换句话说,VRt与SRt计算公式相同,只是系统参数不同。
if (vars.utilizationRate > OPTIMAL_UTILIZATION_RATE) {
uint256 excessUtilizationRateRatio =
vars.utilizationRate.sub(OPTIMAL_UTILIZATION_RATE).rayDiv(EXCESS_UTILIZATION_RATE);
vars.currentStableBorrowRate = vars.currentStableBorrowRate.add(_stableRateSlope1).add(
_stableRateSlope2.rayMul(excessUtilizationRateRatio)
);
vars.currentVariableBorrowRate = _baseVariableBorrowRate.add(_variableRateSlope1).add(
_variableRateSlope2.rayMul(excessUtilizationRateRatio)
);
} else {
vars.currentStableBorrowRate = vars.currentStableBorrowRate.add(
_stableRateSlope1.rayMul(vars.utilizationRate.rayDiv(OPTIMAL_UTILIZATION_RATE))
);
vars.currentVariableBorrowRate = _baseVariableBorrowRate.add(
vars.utilizationRate.rayMul(_variableRateSlope1).rayDiv(OPTIMAL_UTILIZATION_RATE)
);
}
这段代码完整地实现了白皮书上的二段式利率定价过程。
计算流动性利率。
vars.currentLiquidityRate = _getOverallBorrowRate(
totalStableDebt,
totalVariableDebt,
vars
.currentVariableBorrowRate,
averageStableBorrowRate
)
.rayMul(vars.utilizationRate)
.percentMul(PercentageMath.PERCENTAGE_FACTOR.sub(reserveFactor));
如果一个储备池利用率很低,那么大部分闲散资金对于借贷利率并无贡献。例如一家银行的借贷数量只有总存款的百分之一,即使它的贷款利率是40%,那么实际银行按照总存款量计算的收益率也仅仅是0.4%。流动性利率计算的就是这个实际的收益率。 计算流动性利率的功能被包装在函数_getOverallBorrowRate中。
function _getOverallBorrowRate(
uint256 totalStableDebt,
uint256 totalVariableDebt,
uint256 currentVariableBorrowRate,
uint256 currentAverageStableBorrowRate
) internal pure returns (uint256) {
uint256 totalDebt = totalStableDebt.add(totalVariableDebt);
if (totalDebt == 0) return 0;
uint256 weightedVariableRate = totalVariableDebt.wadToRay().rayMul(currentVariableBorrowRate);
uint256 weightedStableRate = totalStableDebt.wadToRay().rayMul(currentAverageStableBorrowRate);
uint256 overallBorrowRate =
weightedVariableRate.add(weightedStableRate).rayDiv(totalDebt.wadToRay());
return overallBorrowRate;
}
返回的加权借贷利率是算入了固定利率债务和可变利率债务。接着AAVE将利用率考虑进去,并扣除了移交给国库的部分利息,得到最后的流动性利率。这也可以此时该代币储备池的总收益率。
LiquidityRate=OverallBorrowRateUtilizationRate(1-ReserveFactor)
让我们回到之前LendingPool::Deposit函数,看看aave如何给用户mint
function mint(
address user,
uint256 amount,
uint256 index
) external override onlyLendingPool returns (bool) {
uint256 previousBalance = super.balanceOf(user);
uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.CT_INVALID_MINT_AMOUNT);
_mint(user, amountScaled);
emit Transfer(address(0), user, amount);
emit Mint(user, amount, index);
return previousBalance == 0;
}
函数首先查询用户当前的账户aToken余额。接着使用复利因子将amount对应进行贴现,得到一个折现后的amountScaled。然后才铸造amountScaled数量的aToken给用户。
why?
假设AAVE是2020年底开始计息,用户1在2020年底存入了100个USDC,年利息假设是10%,那么2021年底用户1持有的本金和是110个USDC。2022年底用户1持有的USDC本金和是121USDC = 100 *(1+0.1)(1+0.1)=Liquidity index(2)。
用户2在2021年底存入了100 USDC,那么2022年底用户2持有的本金和为110 USDC = 100 (1+0.1)=100 Liquidity index(1)。
那么如果AAVE直接分配给用户1和用户2各100个aUSDC,那么在2022年底,用户1的100个aUSDC可以获取121个USDC,用户2的100个USDC也可以获取121个USDC,这明显是不合理的。
因此为了计算合理,用户2在2021年存入100个USDC时,应该给与 100/Liqudity(1) 个aUSDC。这样在2022年底计息时,用户2的本金和就是 100/Liquidity(1) * Liquidity(2)=110 USDC。
function withdraw(
address asset,
uint256 amount,
address to
) external override whenNotPaused returns (uint256) {
DataTypes.ReserveData storage reserve = _reserves[asset];
address aToken = reserve.aTokenAddress;
uint256 userBalance = IAToken(aToken).balanceOf(msg.sender);
uint256 amountToWithdraw = amount;
if (amount == type(uint256).max) {
amountToWithdraw = userBalance;
}
ValidationLogic.validateWithdraw(
asset,
amountToWithdraw,
userBalance,
_reserves,
_usersConfig[msg.sender],
_reservesList,
_reservesCount,
_addressesProvider.getPriceOracle()
);
reserve.updateState();
reserve.updateInterestRates(asset, aToken, 0, amountToWithdraw);
if (amountToWithdraw == userBalance) {
_usersConfig[msg.sender].setUsingAsCollateral(reserve.id, false);
emit ReserveUsedAsCollateralDisabled(asset, msg.sender);
}
IAToken(aToken).burn(msg.sender, to, amountToWithdraw, reserve.liquidityIndex);
emit Withdraw(asset, msg.sender, to, amountToWithdraw);
return amountToWithdraw;
}
与Deposit类似,首先获取了存入的资产合约的储备池对象
DataTypes.ReserveData storage reserve = _reserves[asset];
然后验证取款是否合法
ValidationLogic.validateWithdraw(
asset,
amountToWithdraw,
userBalance,
_reserves,
_usersConfig[msg.sender],
_reservesList,
_reservesCount,
_addressesProvider.getPriceOracle()
);
再更新复利序列索引和流动性利率。(由于再Deposit中已经分析了,此处就不赘述了)
reserve.updateState();
reserve.updateInterestRates(asset, aToken, 0, amountToWithdraw);
最后销毁aToken,返回资产代币给用户。
IAToken(aToken).burn(msg.sender, to, amountToWithdraw, reserve.liquidityIndex);
function burn(
address user,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external override onlyLendingPool {
uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.CT_INVALID_BURN_AMOUNT);
_burn(user, amountScaled);
IERC20(_underlyingAsset).safeTransfer(receiverOfUnderlying, amount);
emit Transfer(user, address(0), amount);
emit Burn(user, receiverOfUnderlying, amount, index);
}
这里唯一要细说的地方是销毁的aToken数目是经过流动性复利因子贴现过的数目。
同样举个例子:
客户存入2020年底存入了100 USDC,此时AAVE刚刚开始计息,流动性利率假设一直为10%。
那么到了2022年,客户的本金和是1001.11.1=121 USDC = 100 * LiquidityIndex(2)。客户持有的aToken则一直是100
这里问题来了,客户此时打算全部提取走121 USDC。那么如果销毁121个aToken,这明显是不合适的。aToken作为存款凭证,并不会随着利息的增加而增加,它始终代表的是最初的100个USDC。
所以此时销毁的aToken数量是 121/LiquidityIndex(2)=100
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!