AAVE
利率模型中,几个参数之间互相影响,关系错综复杂,其中,固定利率比较绕,尤其是计算平均固定利率的公式,尤其难懂。不过,在实际的借贷中,有很多token不支持固定利率借贷,而且,在支持固定利率借贷token中,使用固定利率的借贷比例也很小,几乎不到1%,因此,对于固定利率部分,对于初学者来说可以先跳过。
在 AAVE
的利率计算中, 有几个地方值得说明:
AAVE
的利率更新是基于时间戳来计算利息的增加,Compound
是根据块数来计算利息的增加;两者触发利率更新的方式相同,都是由存款,取款,借款,还款等几个动作触发,每个块只计算一次;- 存款利率的增加是线性的,贷款利率的增加是复利,指数级;
- 贷款收益的 reserve factor% (默认10%) 纳入平台金库;
- aToken, debtToken 中的
balanceOf
方法,返回的 amount 都是存款/贷款对应的 token 数量;
如果你只需要知道结论,AAVE
的几个核心流程可以简单的总结如下(代码在 LendingPool
合约中):
存款 Deposit
- 校验数据(ValidationLogic.validateDeposit)
- 更新状态: updateState
- 更新利率: updateInterestRates
- 将 token 从用户转入
LendingPool
合约 - mint aToken给用户: aToken amount = token amount / liquidityIndex
- emit Deposit事件
取款 Withdraw
- 查询用户token资产数量,注意: 是token资产数量, 而不是 aToken 数量. 虽然这里调用的是 aToken.balanceOf(), 看上去好像是 aToken 的余额,但事件上,这个函数会计算当前块 aToken 对应的 token 的数量;
- 校验数据(ValidationLogic.validateWithdraw)
- 更新状态: updateState
- 更新利率: updateInterestRates
- 销毁用户的aToken,将对应的token转给用户
- emit Withdraw 事件
借款 Borrow
- 通过价格预言机, 计算出借款对应的 ETH 价值
amountInETH
(AAVE中所有的token都要换算为对应的ETH价值) - 校验数据(ValidationLogic.validateBorrow)
- 更新状态: updateState
- 至关重要的一步:mint 对应的 debtToken, AAVE 的债务是通过 debtToken 来记录的,每个token 都会有对应的 stableDebtToken, variableDebtToken, 分别对应于定息借款和活息借款
- 更新利率: updateInterestRates
- 将用户借出的token转给用户
- emit Borrow 事件
还款 Repay
- 查询定息借款和活息借款额度, 都是贷款 debtToken 对应的 token 的数量;
- 校验数据 (ValidationLogic.validateRepay)
- 更新状态: updateState
- 销毁 debtToken. 这里有一个很关键、很重要的细节,就是还款额度的计算: paybackAmount, 必须要非常仔细, 一旦出错, 就会造成重大损失
- 更新利率: updateInterestRates
- 将用户的 token 转入合约,完成还款
- emit Repay 事件
最基本的操作就是以上这几个,其他的还有清算(专门讨论), 转换借款方式(定息转活息,活息转定息)
从上面的流程可以看出:
- updateState 和 updateInterestRates 是两个非常关键的函数,所有的操作都要调用这两个方法;
- 对这两个方法的调用稍有不同,例如 存款,取款时,两个方法是一起调用的; 而借款中,在两个方法的调用之间,还有其他操作;为什么会这样呢?
所有利率的变化,都是由 updateState 和 updateInterestRates 计算并更新的,下面我们来看看这两个函数的实现细节。
updateState updateInterestRates
两个函数定义如下:
function updateState(DataTypes.ReserveData storage reserve) internal;
function updateInterestRates(
DataTypes.ReserveData storage reserve,
address reserveAddress,
address aTokenAddress,
uint256 liquidityAdded,
uint256 liquidityTaken
) internal;
这两个函数在 library ReserveLogic.sol
中, 传入的参数中都有 DataTypes.ReserveData storage reserve
, ReserveData 我们前面介绍过,是储存利率相关字段的结构体; 这个参数的存储类型是 storage
, 说明这两个函数可以直接修改合约中的状态。
updateState
updateState 主要作用: 0. 获取最新的活息贷款数量, 即 variableDebtToken 的总数量;
- 更新存款利率指数(ReserveData.liquidityIndex)和活息贷款利率指数(ReserveData.variableBorrowIndex), 通过
_updateIndexes
方法 - 将产生的增量贷款利息的一部分(reserve factor) 存入小金库, 具体实现的方式是 mint Atoken 给 treasy 地址
updateInterestRates
updateInterestRates 主要作用: 0. 更新 currentLiquidityRate;
- 更新 currentStableBorrowRate
- 更新 currentVariableBorrowRate
关键函数
_updateIndexes
一个细节是,虽然资金变化都每次都调用,但由于在一个块内,只有第一次调用时 liquidityIndex 和 variableBorrowIndex 发生变化,后续调用都不会变化,因为 timestamp 相同, calculateCompoundedInterest 和 calculateLinearInterest 都是返回 Ray(1e27).
不知道是否可以优化, 我提了一个issue: https://github.com/aave/protocol-v2/issues/237
function _updateIndexes(
DataTypes.ReserveData storage reserve,
uint256 scaledVariableDebt,
uint256 liquidityIndex,
uint256 variableBorrowIndex,
uint40 timestamp // 上次更新的时间戳
) internal returns (uint256, uint256) {
// currentLiquidityRate 是在 updateInterestRates 中更新的
uint256 currentLiquidityRate = reserve.currentLiquidityRate;
uint256 newLiquidityIndex = liquidityIndex;
uint256 newVariableBorrowIndex = variableBorrowIndex;
//only cumulating if there is any income being produced
if (currentLiquidityRate > 0) {
// 存款利率根据 currentLiquidityRate 线性累加
// 公式: cumulatedLiquidityInterest = currentLiquidityRate * (block.timestamp-timestamp)/SECONDS_PER_YEAR
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);
}
updateInterestRates
更新年化利率
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();
// 活息贷款, 折算为 token
//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);
// 根据利率策略,计算新的存款年化利率 活息贷款年化利率 定息贷款年化利率. calculateInterestRates 见下文分析
(
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
);
}
calculateInterestRates
代码如下:
struct CalcInterestRatesLocalVars {
uint256 totalDebt;
uint256 currentVariableBorrowRate;
uint256 currentStableBorrowRate;
uint256 currentLiquidityRate;
uint256 utilizationRate;
}
/**
* @dev Calculates the interest rates depending on the reserve's state and configurations.
* NOTE This function is kept for compatibility with the previous DefaultInterestRateStrategy interface.
* New protocol implementation uses the new calculateInterestRates() interface
* @param reserve The address of the reserve
* @param availableLiquidity The liquidity available in the corresponding aToken
* @param totalStableDebt The total borrowed from the reserve a stable rate
* @param totalVariableDebt The total borrowed from the reserve at a variable rate
* @param averageStableBorrowRate The weighted average of all the stable rate loans
* @param reserveFactor The reserve portion of the interest that goes to the treasury of the market
* @return The liquidity rate, the stable borrow rate and the variable borrow rate
**/
function calculateInterestRates(
address reserve,
uint256 availableLiquidity,
uint256 totalStableDebt,
uint256 totalVariableDebt,
uint256 averageStableBorrowRate,
uint256 reserveFactor
)
public
view
override
returns (
uint256,
uint256,
uint256
)
{
CalcInterestRatesLocalVars memory vars;
// 总借款 = 活息借款 + 定息借款
vars.totalDebt = totalStableDebt.add(totalVariableDebt);
vars.currentVariableBorrowRate = 0;
vars.currentStableBorrowRate = 0;
vars.currentLiquidityRate = 0;
// 资金利用率 = 总借款 / (总存款+总借款)
// 忽略 rayDiv rayMul, 可以简单的认为是 div mul 即可
vars.utilizationRate = vars.totalDebt == 0
? 0
: vars.totalDebt.rayDiv(availableLiquidity.add(vars.totalDebt));
vars.currentStableBorrowRate = ILendingRateOracle(addressesProvider.getLendingRateOracle())
.getMarketBorrowRate(reserve);
// 利率曲线: 两段式
// 资金利用率高于最佳利用率时,利率的计算
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)
);
}
// currentLiquidityRate的计算 = (加权平均借款利率/总借款) * 资金利用率 * (1-储备率)
// 加权平均借款利率 = (活息借款*活息利率 + 定息借款*平均定息利率) / 总借款
vars.currentLiquidityRate = _getOverallBorrowRate(
totalStableDebt,
totalVariableDebt,
vars
.currentVariableBorrowRate,
averageStableBorrowRate
)
.rayMul(vars.utilizationRate)
.percentMul(PercentageMath.PERCENTAGE_FACTOR.sub(reserveFactor));
return (
vars.currentLiquidityRate,
vars.currentStableBorrowRate,
vars.currentVariableBorrowRate
);
}
/**
* @dev Calculates the overall borrow rate as the weighted average between the total variable debt and total stable debt
* @param totalStableDebt The total borrowed from the reserve a stable rate
* @param totalVariableDebt The total borrowed from the reserve at a variable rate
* @param currentVariableBorrowRate The current variable borrow rate of the reserve
* @param currentAverageStableBorrowRate The current weighted average of all the stable rate loans
* @return The weighted averaged borrow rate
加权平均借款利率: (活息借款*活息利率 + 定息借款*平均定息利率) / 总借款
**/
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;
}
}
为什么要先调用 updateState
, 然后再调用 updateInterestRates
简单的说,是因为 liquidityIndex 和 variableBorrowIndex 是由 currentLiquidityRate 和 currentVariableBorrowRate 计算出来的,而先调用 updateState
, 然后再调用 updateInterestRates
,就是说,在时刻 t1, 当存款或贷款无论变化多少次,liquidityIndex 和 variableBorrowIndex只变化一次; 而 currentLiquidityRate, currentVariableBorrowRate 和 currentStableBorrowRate 会随着金额的变化而变化。
在一个块中,liquidityIndex 和 variableBorrowIndex只变化一次, 这里 AAVE 的代码不如 Compound, 如果是同一个块,多了很多无用的运算;
就是说,你存款后,下一个块才开始有利息;同理,贷款也是如此。
定息贷款的 averageStableBorrowRate
averageStableBorrowRate 对于定息贷款很重要, 影响到 资金使用率,进而影响到 存款利率。
averageStableBorrowRate
在 StableDebtToken.sol
合约中定义,在 mint
和 burn
是改变该值。
mint:
// _calculateBalanceIncrease 返回值: 贷款本金,本息合计,利息
(, uint256 currentBalance, uint256 balanceIncrease) = _calculateBalanceIncrease(onBehalfOf);
// 用户新的平均贷款利率 = 某种加权平均???
vars.newStableRate = _usersStableRate[onBehalfOf]
.rayMul(currentBalance.wadToRay())
.add(vars.amountInRay.rayMul(rate)) // rate 是入参, 值为 reserve.currentStableBorrowRate
.rayDiv(currentBalance.add(amount).wadToRay());
require(vars.newStableRate <= type(uint128).max, Errors.SDT_STABLE_DEBT_OVERFLOW);
// 每个用户都有一个_usersStableRate, 保存用户贷款时的利率
_usersStableRate[onBehalfOf] = vars.newStableRate;
//solium-disable-next-line
_totalSupplyTimestamp = _timestamps[onBehalfOf] = uint40(block.timestamp);
// 该 token 总的定息贷款平均利率, 与上面的更新逻辑一致
// Calculates the updated average stable rate
vars.currentAvgStableRate = _avgStableRate = vars
.currentAvgStableRate
.rayMul(vars.previousSupply.wadToRay())
.add(rate.rayMul(vars.amountInRay))
.rayDiv(vars.nextSupply.wadToRay());
burn:
if (previousSupply <= amount) {
// 当定息贷款被还完时,清零
// 由于可能存在计算误差, 这里不能使用 previousSupply - amount
_avgStableRate = 0;
_totalSupply = 0;
} else {
nextSupply = _totalSupply = previousSupply.sub(amount);
uint256 firstTerm = _avgStableRate.rayMul(previousSupply.wadToRay());
uint256 secondTerm = userStableRate.rayMul(amount.wadToRay());
// For the same reason described above, when the last user is repaying it might
// happen that user rate * user balance > avg rate * total supply. In that case,
// we simply set the avg rate to 0
if (secondTerm >= firstTerm) {
newAvgStableRate = _avgStableRate = _totalSupply = 0;
} else {
newAvgStableRate = _avgStableRate = firstTerm.sub(secondTerm).rayDiv(nextSupply.wadToRay());
}
}
由于定息贷款用户的利息和平台的利息是独立计算的,感觉这里两个数据会存在不一致。