AAVE V2 学习笔记

AAVE V2 白皮书和源码学习笔记

这几天在学习 AAVE,资料看了 V1 和 V2 的白皮书,代码只看了 V2 版本,另外感谢大佬分享: [AAVE v2 - white paper](https://learnblockchain.cn/article/3099) [Dapp-Learning: AAVE](https://github.com/rebase-network/Dapp-Learning/blob/main/defi/Aave/contract/readme.md) [aave小组分享:白皮书解读](https://www.bilibili.com/video/BV1UF411Y7oU) 这里简单记下学习笔记 需要说明的是,个人不太适应白皮书自上而下的展开结构,所以笔记反向记录 ## 利率 rate ### 当前时刻的浮动/稳定借款利率 variable/stable rate #### 公式 $$ \#R_{t}^{asset} = \begin{aligned} \begin{cases} \#R_{base}^{asset} + \frac{U_{t}^{asset}}{U_{optimal}} \times \#R_{slope1}^{asset} &\ if \ U_{t}^{asset} \lt U_{optimal} \\ \#R_{base}^{asset} + \#R_{slope1}^{asset} + \frac{U_{t}^{asset} - U_{optimal}}{1 - U_{optimal}} \times \#R_{slope2}^{asset} &\ if \ U_{t}^{asset} \geq U_{optimal} \end{cases} \end{aligned} $$ 这里的 $\#$ 可以为 `V` 或 `S`,代入后得到 $VR_{t}$ 或 $SR_{t}$,分别表示浮动利率和稳定利率 换句话说,$VR_{t}$ 与 $SR_{t}$ 计算公式相同,只是系统参数不同 其中 资金利用率 等于 总债务 占 总储蓄 的比例 $$ U_{t}^{asset} = \begin{aligned} \begin{cases} 0 &\ if \ L_{t}^{asset} = 0 \\ \frac{D_{t}^{asset}}{L_{t}^{asset}} &\ if \ L_{t}^{asset} \gt 0 \end{cases} \end{aligned} $$ 总债务 等于 浮动利率债务 与 稳定利率债务 之和 $$ D_{t}^{asset} = VD_{t}^{asset} + SD_{t}^{asset} $$ 综合上面三个公式,可以看到,利率 $\#R_{t}$ 与 资金利用率 $U_{t}$ 正相关 也就是说,借贷需求旺盛时,利率随着资金利用率上升;借贷需求萎靡时,利率随着资金利用率下降 #### 代码 存储 ```js library DataTypes { struct ReserveData { //the current variable borrow rate. Expressed in ray uint128 currentVariableBorrowRate; //the current stable borrow rate. Expressed in ray uint128 currentStableBorrowRate; } } ``` 更新 ```js interface IReserveInterestRateStrategy { function calculateInterestRates( address reserve, address aToken, uint256 liquidityAdded, uint256 liquidityTaken, uint256 totalStableDebt, uint256 totalVariableDebt, uint256 averageStableBorrowRate, uint256 reserveFactor ) external view returns ( uint256 liquidityRate, uint256 stableBorrowRate, uint256 variableBorrowRate ); } ``` ### 最新浮动借款利率 即上面的 $VR_{t}$ ### 稳定借款利率 #### 平均稳定借款利率 overall stable rate #### 借款 `mint()` 假设利率为 $SR_t$ 时发生一笔额度为 $SB_{new}$ 的借款,则 $$ \overline{SR}_{t} = \frac{SD_{t} \times \overline{SR}_{t-1} + SB_{new} \times SR_{t}}{SD_{t} + SB_{new}} $$ $SD_{t}$ 表示此前债务 (不含 $SB_{new}$) 到当前时刻累计的本金+利息(即 `previousSupply`) 在白皮书中没有公式,但应该是 $$ SD_{t} = SB \times (1+\frac{\overline{SR}_{t-1}}{T_{year}})^{\Delta T} $$ -- 用户 $x$ 的 平均利率 (用于还款时的计算) $$ \overline{SR}(x) = \sum\nolimits_{i}^{}\frac{SR_{i}(x) \times SD_{i}(x)}{SD_{i}(x)} $$ 问题:未拆开 $SD_{i-1}(x)$ 与 $SB_{new}$ 实际:需要拆开分别乘以不同的利率 结论:不应简单以 $SD_{i}(x)$ 代替 结合源码,应该修正为 $$ \overline{SR}_{t}(x) = \frac{SD_{t}(x) \times \overline{SR}_{t-1}(x) + SB_{new} \times SR_{t}}{SD_{t}(x) + SB_{new}} $$ 其中 $$ SD_{t}(x) = SB(x) \times (1+\frac{\overline{SR}_{t}}{T_{year}})^{\Delta T} $$ 结合源码,应该修正为 $$ SD_{t}(x) = SB(x) \times (1+\frac{\overline{SR}_{t-1}(x)}{T_{year}})^{\Delta T} $$ 比较 $\overline{SR}_{t}$ 和 $\overline{SR}(x)$ 两个公式 - (x) 强调 用户 $x$ 的平均利率,只受其自身操作的影响,不受其他用户影响 比较 $\overline{SR}(x)$ 修正前后的两个公式 - 原公式不出现 $t$ 调强 用户 $x$ 的平均利率,自上次操作后,不受时间影响 - 修正后更好体现 rebalancing #### 还款 `burn()` 假设用户平均利率为 $\overline{SR}(x)$ 时发生一笔额度为 $SB(x)$ 的还款,则 $$ \overline{SR}_{t} = \begin{aligned} \begin{cases} 0 &\ if \ SD - SB(x) = 0 \\ \frac{SD_{t} \times \overline{SR}_{t-1} - SB(x) \times \overline{SR}(x)}{SD_t - SB(x)} &\ if \ SD - SB(x) \gt 0 \end{cases} \end{aligned} $$ #### 代码 存储 ```js contract StableDebtToken is IStableDebtToken, DebtTokenBase { uint256 internal _avgStableRate; // 池子平均利率 mapping(address => uint40) internal _timestamps; // 用户上次借款时间 mapping(address => uint256) internal _usersStableRate; // 用户平均利率 } ``` 更新 `StableDebtToken.mint()` 更新 `_avgStableRate`,`_timestamps[user]` 和 `_userStableRate[user]` `StableDebtToken.burn()` 更新 `_avgStableRate`,`_timestamps[user]`,如果还款金额未超过利息,则将剩余利息作为新增借款,进行 `mint()` 这将修改 `_userStableRate[user]`,也是 rebalancing TODO 举例 ```js function burn(address user, uint256 amount) external override onlyLendingPool { // Since the total supply and each single user debt accrue separately, // there might be accumulation errors so that the last borrower repaying // mght actually try to repay more than the available debt supply. // In this case we simply set the total supply and the avg stable rate to 0 // 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 } ``` ### 平均借款利率 overall borrow rate $$ \overline{R_{t}}^{asset} = \begin{aligned} \begin{cases} 0 &\ if \ D_t=0 \\ \frac{VD_t \times VR_t + SD_t \times \overline{SR}_t}{D_t} &\ if \ D_t > 0 \end{cases} \end{aligned} $$ ### 当前时刻的流动性利率 current liquidity rate 流动性利率 = 平均借款利率 X 资金利用率 $$ LR_{t}^{asset} = \overline{R}_{t} \times U_{t} $$ 结合 平均借款利率 和 资金利用率 的公式 $$ \overline{R_{t}}^{asset} = \begin{aligned} \begin{cases} 0 &\ if \ D_t=0 \\ \frac{VD_t \times VR_t + SD_t \times \overline{SR}_t}{D_t} &\ if \ D_t > 0 \end{cases} \end{aligned} $$ $$ U_{t}^{asset} = \begin{aligned} \begin{cases} 0 &\ if \ L_{t}^{asset} = 0 \\ \frac{D_{t}^{asset}}{L_{t}^{asset}} &\ if \ L_{t}^{asset} \gt 0 \end{cases} \end{aligned} $$ 同时考虑 超额抵押 的隐藏要求 $$ D_{t} < L_{t} $$ 得到 $$ LR_{t}^{asset} = \begin{aligned} \begin{cases} 0 &\ if \ L_{t} = 0 \ or \ D_{t} = 0\\ \frac{VD_t \times VR_t + SD_t \times \overline{SR}_t}{L_{t}} &\ if \ L_{t} \gt 0 \end{cases} \end{aligned} $$ 理解如下 分子 $VR_{t}$ 与 $SR_{t}$ 都是年化利率,所以 $LR_{t}$ 也是年化流动性利率 分母 $L_{t}$ 表示池子总存款本金,所以 $LR_{t}$ 表示 每单位存款本金,产生的借款利息 ## 指数 index ### 累计流动性指数 cumulated liquidity index 从池子首次发生用户操作时,累计到现在,每单位存款本金,变成多少本金(含利息收入) cumulated liquidity index $$ LI_t=(LR_t \times \Delta T_{year} + 1) \times LI_{t-1} \\ LI_0=1 \times 10 ^{27} = 1 \ ray $$ 其中 $$ \Delta T_{year} = \frac{\Delta T}{T_{year}} = \frac{T - T_{l}}{T_{year}} $$ $T_{l}$ 池子最近一次发生用户操作的的时间 $T_{l}$ is updated every time a borrow, deposit, redeem, repay, swap or liquidation event occurs. -- 每单位存款本金,未来将变成多少本金(含利息收入) reserve normalized income $$ NI_t = (LR_t \times \Delta T_{year} + 1) \times LI_{t-1} $$ 存储 ```js struct ReserveData { //the liquidity index. Expressed in ray uint128 liquidityIndex; } ``` 更新 ```js 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; //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); } reserve.lastUpdateTimestamp = uint40(block.timestamp); } } ``` ### 累计浮动借款指数 cumulated variable borrow index 从池子首次发生用户操作时,累计到现在,每单位借款债务,共变成多少债务 cumulated variable borrow index $$ VI_{t} = (1 + \frac{VR_t}{T_{year}})^{\Delta T} \times VI_{t-1} $$ -- 每单位浮动借款债务,未来将变成多少债务 normalised variable (cumulated) debt $$ VN_{t} = (1 + \frac{VR_{t}}{T_{year}})^{\Delta T} \times VI_{t-1} $$ 存储 ```js struct ReserveData { uint128 variableBorrowIndex; } ``` 计算 ```js // ReserveLogic.sol /** * @dev Returns the ongoing normalized variable debt for the reserve * A value of 1e27 means there is no debt. As time passes, the income is accrued * A value of 2*1e27 means that for each unit of debt, one unit worth of interest has been accumulated * @return The normalized variable debt. expressed in ray **/ function getNormalizedDebt(DataTypes.ReserveData storage reserve) internal view returns (uint256) { uint40 timestamp = reserve.lastUpdateTimestamp; if (timestamp == uint40(block.timestamp)) { return reserve.variableBorrowIndex; } uint256 cumulated = MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp).rayMul( reserve.variableBorrowIndex ); return cumulated; } ``` 更新 ```js function _updateIndexes( DataTypes.ReserveData storage reserve, uint256 scaledVariableDebt, uint256 liquidityIndex, uint256 variableBorrowIndex, uint40 timestamp ) internal returns (uint256, uint256) { uint256 currentLiquidityRate = reserve.currentLiquidityRate; uint256 newVariableBorrowIndex = variableBorrowIndex; //only cumulating if there is any income being produced if (currentLiquidityRate > 0) { //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); } } reserve.lastUpdateTimestamp = uint40(block.timestamp); return (newLiquidityIndex, newVariableBorrowIndex); } ``` ### 用户累计浮动借款指数 user cumulated variable borrow index $$ VI(x) = VI_t(x) $$ 因为是浮动利率,所以用户每次发生借款的利率,都是当时最新的浮动借款利率 ## 代币 token ### aToken $ScB_{t}(x)$ 用户 $x$ 在 $t$ 时刻发生存款或取回操作后,在 ERC20 Token 中记录的 balance 债务 存款 $$ ScB_{t}(x) = ScB_{t-1} + \frac{m}{NI_{t}} $$ 取回 $$ ScB_{t}(x) = ScB_{t-1} - \frac{m}{NI_{t}} $$ 在当前最新的 $t$ 时刻看,用户 $x$ 的总余额 $$ aB_{t}(x) = ScB_{t}(x) \times NI_{t} $$ ### Debt token #### 浮动借款代币 Variable debt token $ScVB_t(x)$ 用户 $x$ 在 $t$ 时刻发生借款或还款操作后,在 ERC20 Token 中记录的 balance 债务 借款 $$ ScVB_t(x) = ScVB_{t-1}(x) + \frac{m}{VN_{t}} $$ 还款 $$ ScVB_t(x) = ScVB_{t-1}(x) - \frac{m}{VN_{t}} $$ 在当前最新的 $t$ 时刻看,用户 $x$ 的总债务 $$ VD(x) = ScVB(x) \times D_{t} $$ 这里是个比较明显的 typo,应该修正为 $$ VD(x) = ScVB(x) \times VN_{t} $$ #### 稳定借款代币 Stable debt token 用户 $x$ 此前在 $t$ 发生操作后 $$ SD_{t}(x) = SB(x) \times (1+\frac{\overline{SR}_{t-1}(x)}{T_{year}})^{\Delta T} $$ ## 风险 ### 借贷利率参数 Borrow Interest Rate 参考 [Borrow Interest Rate](https://docs.aave.com/risk/liquidity-risk/borrow-interest-rate) | USDT | $U_{optimal}$ | $U_{base}$ | $Slope1$ | $Slope2$ | | :- | -: | -: | -: | -: | | Variable | 90\% | 0\% | 4\% | 60\% | | Stable | 90\% | 3.5\% | 2\% | 60\% | ### 风险参数 Risk Parameters 参考 [Risk Parameters](https://docs.aave.com/risk/asset-risk/risk-parameters#risk-parameters-analysis) | Name | Symbol | Collateral | Loan To Value | Liquidation Threshold | Liquidation Bonus | Reserve Factor | | :- | :- | -: | -: | -: | -: | -: | | DAI | DAI | yes | 75\% | 80\% | 5\% | 10\% | | Ethereum | ETH | yes | 82.5\% | 85\% | 5\% | 10\% | 实际用的是万分比 #### 抵押率 Loan to Value 抵押率,表示价值 1 ETH 的抵押物,能借出价值多少 ETH 的资产 最大抵押率,是用户抵押的各类资产的抵押率的加权平均值 $$ Max LTV = \frac{ \sum{{Collateral}_i \ in \ ETH \ \times \ LTV_i}}{Total \ Collateral \ in \ ETH} $$ 比如 用户存入价值为 1 ETH 的 DAI,和 1 ETH 的 Ethereum,那么 $$ Max LTV = \frac{1 \times 0.75 + 1 \times 0.825}{1+1} = 0.7875 $$ #### 清算阈值 Liquidation Threshold $$ LiquidationThreshold = \frac{\sum{{Collateral}_i \ in \ ETH \ \times \ {Liquidation \ Threshold}_i}}{Total \ Collateral \ in \ ETH} $$ 上面的例子为 $$ LiquidationThreshold = \frac{1 \times 0.8 + 1 \times 0.85}{1 + 1} = 0.825 $$ Loan-To-Value 与 Liquidation Threshold 之间的窗口,是借贷者的安全垫 #### 清算奖励 Liquidation Bonus 抵押物的拍卖折扣,作为清算者的奖励 #### 健康度 Health Factor $$ Hf = \frac{\sum{{Collateral}_i \ in \ ETH \ \times \ {Liquidation \ Threshold}_i}}{Total \ Borrows \ in \ ETH} $$ $Hf < 1$ 说明这个用户资不抵债,应该清算 (代码中,$Hf$ 单位为 ether) 假设用户抵押此前存入的2 ETH 资产,按最大抵押率借出价值 $0.7875 \times 2 = 1.575$ ETH 的债务,此时 $$ Hf = \frac{1 \times 0.8 + 1 \times 0.85}{1.575} = 1.0476 $$ 相关代码如下 ```js /** * @dev Calculates the user data across the reserves. * this includes the total liquidity/collateral/borrow balances in ETH, * the average Loan To Value, the average Liquidation Ratio, and the Health factor. **/ function calculateUserAccountData( address user, mapping(address => DataTypes.ReserveData) storage reservesData, DataTypes.UserConfigurationMap memory userConfig, mapping(uint256 => address) storage reserves, uint256 reservesCount, address oracle ) internal view returns (uint256, uint256, uint256, uint256, uint256){ CalculateUserAccountDataVars memory vars; for (vars.i = 0; vars.i < reservesCount; vars.i++) { if (!userConfig.isUsingAsCollateralOrBorrowing(vars.i)) { continue; } vars.currentReserveAddress = reserves[vars.i]; DataTypes.ReserveData storage currentReserve = reservesData[vars.currentReserveAddress]; (vars.ltv, vars.liquidationThreshold, , vars.decimals, ) = currentReserve .configuration .getParams(); vars.tokenUnit = 10**vars.decimals; vars.reserveUnitPrice = IPriceOracleGetter(oracle).getAssetPrice(vars.currentReserveAddress); if (vars.liquidationThreshold != 0 && userConfig.isUsingAsCollateral(vars.i)) { vars.compoundedLiquidityBalance = IERC20(currentReserve.aTokenAddress).balanceOf(user); uint256 liquidityBalanceETH = vars.reserveUnitPrice.mul(vars.compoundedLiquidityBalance).div(vars.tokenUnit); vars.totalCollateralInETH = vars.totalCollateralInETH.add(liquidityBalanceETH); vars.avgLtv = vars.avgLtv.add(liquidityBalanceETH.mul(vars.ltv)); vars.avgLiquidationThreshold = vars.avgLiquidationThreshold.add( liquidityBalanceETH.mul(vars.liquidationThreshold) ); } if (userConfig.isBorrowing(vars.i)) { vars.compoundedBorrowBalance = IERC20(currentReserve.stableDebtTokenAddress).balanceOf( user ); vars.compoundedBorrowBalance = vars.compoundedBorrowBalance.add( IERC20(currentReserve.variableDebtTokenAddress).balanceOf(user) ); vars.totalDebtInETH = vars.totalDebtInETH.add( vars.reserveUnitPrice.mul(vars.compoundedBorrowBalance).div(vars.tokenUnit) ); } } vars.avgLtv = vars.totalCollateralInETH > 0 ? vars.avgLtv.div(vars.totalCollateralInETH) : 0; vars.avgLiquidationThreshold = vars.totalCollateralInETH > 0 ? vars.avgLiquidationThreshold.div(vars.totalCollateralInETH) : 0; vars.healthFactor = calculateHealthFactorFromBalances( vars.totalCollateralInETH, vars.totalDebtInETH, vars.avgLiquidationThreshold ); return ( vars.totalCollateralInETH, vars.totalDebtInETH, vars.avgLtv, vars.avgLiquidationThreshold, vars.healthFactor ); } ``` #### 清算 继续上面的例子:用户抵押价值 2 ETH 的资产,借出 1.575 ETH 的债务,$Hf$ 为 1.0476 经过一段时间后,市场可能出现如下清算场景 场景一:用户借出的债务从价值为 1.575 ETH,上涨为 2 ETH,此时 $$ Hf = \frac{1 \times 0.8 + 1 \times 0.85}{2} = 0.825 $$ 场景二:用户抵押的资产 DAI,从价值为 1 ETH,下跌为 0.8 ETH,此时 $$ Hf = \frac{0.8 \times 0.8 + 1 \times 0.85}{1.575} = 0.9460 $$ 用户可能如下考虑: 假设,用户看多自己借出的债务,比如用户认为债务价值会继续上涨到 3 ETH,此时可以不做操作,任由清算 相反,用户看多自己抵押的资产,而认为债务升值无望,那么如果资产被低位清算,将得不偿失;此时用户可以追加抵押或偿还债务,避免清算 假设用户未及时操作,套利者先行一步触发清算,相关代码如下 ```js /** * @return collateralAmount: The maximum amount that is possible to liquidate given all the liquidation constraints * (user balance, close factor) * debtAmountNeeded: The amount to repay with the liquidation **/ function _calculateAvailableCollateralToLiquidate( DataTypes.ReserveData storage collateralReserve, DataTypes.ReserveData storage debtReserve, address collateralAsset, address debtAsset, uint256 debtToCover, uint256 userCollateralBalance ) internal view returns (uint256, uint256) { uint256 collateralAmount = 0; uint256 debtAmountNeeded = 0; IPriceOracleGetter oracle = IPriceOracleGetter(_addressesProvider.getPriceOracle()); AvailableCollateralToLiquidateLocalVars memory vars; vars.collateralPrice = oracle.getAssetPrice(collateralAsset); vars.debtAssetPrice = oracle.getAssetPrice(debtAsset); (, , vars.liquidationBonus, vars.collateralDecimals, ) = collateralReserve .configuration .getParams(); vars.debtAssetDecimals = debtReserve.configuration.getDecimals(); // This is the maximum possible amount of the selected collateral that can be liquidated, given the // max amount of liquidatable debt vars.maxAmountCollateralToLiquidate = vars .debtAssetPrice .mul(debtToCover) .mul(10**vars.collateralDecimals) .percentMul(vars.liquidationBonus) .div(vars.collateralPrice.mul(10**vars.debtAssetDecimals)); if (vars.maxAmountCollateralToLiquidate > userCollateralBalance) { collateralAmount = userCollateralBalance; debtAmountNeeded = vars .collateralPrice .mul(collateralAmount) .mul(10**vars.debtAssetDecimals) .div(vars.debtAssetPrice.mul(10**vars.collateralDecimals)) .percentDiv(vars.liquidationBonus); } else { collateralAmount = vars.maxAmountCollateralToLiquidate; debtAmountNeeded = debtToCover; } return (collateralAmount, debtAmountNeeded); } ``` 注意 `maxAmountCollateralToLiquidate`,表示可以被清算的最大的抵押资产的数量 它通过计算清算债务的价值,除以抵押资产的单位价格得到 由于清算者得到的是抵押资产,而抵押资产本身面临着市场波动风险,为了鼓励清算以降低系统风险,这里会凭空乘以 `liquidationBonus` 比如清算的抵押资产为 DAI,根据上面链接的数据,该资产当前的 `liquidationBonus` 为 10500 即清算者支付 `debtAmountNeeded` 的债务,可以多得到 5% 的 aDAI(或 DAI) 实际清算时,需要考虑资产的阈值与奖励各有不同;而且 $Hf$ 是整体概念,而清算需要指定某个资产和某个债务;比如用户抵押 A 和 B 资产,借出 C 和 D 债务,清算时可以有 4 种选择 清算参数,与清算资产的多样化,使得清算策略复杂起来~ // 别跳清算套利的坑

这几天在学习 AAVE,资料看了 V1 和 V2 的白皮书,代码只看了 V2 版本,另外感谢大佬分享:

AAVE v2 - white paper Dapp-Learning: AAVE

aave小组分享:白皮书解读

这里简单记下学习笔记

需要说明的是,个人不太适应白皮书自上而下的展开结构,所以笔记反向记录

利率 rate

当前时刻的浮动/稳定借款利率

variable/stable rate

公式

$$ #R{t}^{asset} = \begin{aligned} \begin{cases} #R{base}^{asset} + \frac{U{t}^{asset}}{U{optimal}} \times #R{slope1}^{asset} &\ if \ U{t}^{asset} \lt U{optimal} \ #R{base}^{asset} + #R{slope1}^{asset} + \frac{U{t}^{asset} - U{optimal}}{1 - U{optimal}} \times #R{slope2}^{asset} &\ if \ U{t}^{asset} \geq U_{optimal} \end{cases} \end{aligned} $$

这里的 $#$ 可以为 VS,代入后得到 $VR{t}$ 或 $SR{t}$,分别表示浮动利率和稳定利率

换句话说,$VR{t}$ 与 $SR{t}$ 计算公式相同,只是系统参数不同

其中

资金利用率 等于 总债务 占 总储蓄 的比例

$$ U{t}^{asset} = \begin{aligned} \begin{cases} 0 &\ if \ L{t}^{asset} = 0 \ \frac{D{t}^{asset}}{L{t}^{asset}} &\ if \ L_{t}^{asset} \gt 0 \end{cases} \end{aligned} $$

总债务 等于 浮动利率债务 与 稳定利率债务 之和

$$ D{t}^{asset} = VD{t}^{asset} + SD_{t}^{asset} $$

综合上面三个公式,可以看到,利率 $#R{t}$ 与 资金利用率 $U{t}$ 正相关

也就是说,借贷需求旺盛时,利率随着资金利用率上升;借贷需求萎靡时,利率随着资金利用率下降

代码

存储

library DataTypes {
  struct ReserveData {
    //the current variable borrow rate. Expressed in ray
    uint128 currentVariableBorrowRate;
    //the current stable borrow rate. Expressed in ray
    uint128 currentStableBorrowRate;
  }
}

更新

interface IReserveInterestRateStrategy {
  function calculateInterestRates(
    address reserve,
    address aToken,
    uint256 liquidityAdded,
    uint256 liquidityTaken,
    uint256 totalStableDebt,
    uint256 totalVariableDebt,
    uint256 averageStableBorrowRate,
    uint256 reserveFactor
  )
    external
    view
    returns (
      uint256 liquidityRate,
      uint256 stableBorrowRate,
      uint256 variableBorrowRate
    );
}

最新浮动借款利率

即上面的 $VR_{t}$

稳定借款利率

平均稳定借款利率

overall stable rate

借款 mint()

假设利率为 $SRt$ 时发生一笔额度为 $SB{new}$ 的借款,则

$$ \overline{SR}{t} = \frac{SD{t} \times \overline{SR}{t-1} + SB{new} \times SR{t}}{SD{t} + SB_{new}} $$

$SD{t}$ 表示此前债务 (不含 $SB{new}$) 到当前时刻累计的本金+利息(即 previousSupply

在白皮书中没有公式,但应该是

$$ SD{t} = SB \times (1+\frac{\overline{SR}{t-1}}{T_{year}})^{\Delta T} $$

--

用户 $x$ 的 平均利率 (用于还款时的计算)

$$ \overline{SR}(x) = \sum\nolimits{i}^{}\frac{SR{i}(x) \times SD{i}(x)}{SD{i}(x)} $$

问题:未拆开 $SD{i-1}(x)$ 与 $SB{new}$

实际:需要拆开分别乘以不同的利率

结论:不应简单以 $SD_{i}(x)$ 代替

结合源码,应该修正为

$$ \overline{SR}{t}(x) = \frac{SD{t}(x) \times \overline{SR}{t-1}(x) + SB{new} \times SR{t}}{SD{t}(x) + SB_{new}} $$

其中

$$ SD{t}(x) = SB(x) \times (1+\frac{\overline{SR}{t}}{T_{year}})^{\Delta T} $$

结合源码,应该修正为

$$ SD{t}(x) = SB(x) \times (1+\frac{\overline{SR}{t-1}(x)}{T_{year}})^{\Delta T} $$

比较 $\overline{SR}_{t}$ 和 $\overline{SR}(x)$ 两个公式

  • (x) 强调 用户 $x$ 的平均利率,只受其自身操作的影响,不受其他用户影响

比较 $\overline{SR}(x)$ 修正前后的两个公式

  • 原公式不出现 $t$ 调强 用户 $x$ 的平均利率,自上次操作后,不受时间影响
  • 修正后更好体现 rebalancing

还款 burn()

假设用户平均利率为 $\overline{SR}(x)$ 时发生一笔额度为 $SB(x)$ 的还款,则

$$ \overline{SR}{t} = \begin{aligned} \begin{cases} 0 &\ if \ SD - SB(x) = 0 \ \frac{SD{t} \times \overline{SR}_{t-1} - SB(x) \times \overline{SR}(x)}{SD_t - SB(x)} &\ if \ SD - SB(x) \gt 0 \end{cases} \end{aligned} $$

代码

存储

contract StableDebtToken is IStableDebtToken, DebtTokenBase {
  uint256 internal _avgStableRate; // 池子平均利率

  mapping(address => uint40) internal _timestamps; // 用户上次借款时间
  mapping(address => uint256) internal _usersStableRate; // 用户平均利率
}

更新

StableDebtToken.mint() 更新 _avgStableRate_timestamps[user]_userStableRate[user]

StableDebtToken.burn() 更新 _avgStableRate_timestamps[user],如果还款金额未超过利息,则将剩余利息作为新增借款,进行 mint() 这将修改 _userStableRate[user],也是 rebalancing

TODO 举例

function burn(address user, uint256 amount) external override onlyLendingPool {
    // Since the total supply and each single user debt accrue separately,
    // there might be accumulation errors so that the last borrower repaying
    // mght actually try to repay more than the available debt supply.
    // In this case we simply set the total supply and the avg stable rate to 0

    // 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
}

平均借款利率

overall borrow rate

$$ \overline{R_{t}}^{asset} = \begin{aligned} \begin{cases} 0 &\ if \ D_t=0 \ \frac{VD_t \times VR_t + SD_t \times \overline{SR}_t}{D_t} &\ if \ D_t > 0 \end{cases} \end{aligned} $$

当前时刻的流动性利率

current liquidity rate

流动性利率 = 平均借款利率 X 资金利用率

$$ LR{t}^{asset} = \overline{R}{t} \times U_{t} $$

结合 平均借款利率 和 资金利用率 的公式

$$ \overline{R_{t}}^{asset} = \begin{aligned} \begin{cases} 0 &\ if \ D_t=0 \ \frac{VD_t \times VR_t + SD_t \times \overline{SR}_t}{D_t} &\ if \ D_t > 0 \end{cases} \end{aligned} $$

$$ U{t}^{asset} = \begin{aligned} \begin{cases} 0 &\ if \ L{t}^{asset} = 0 \ \frac{D{t}^{asset}}{L{t}^{asset}} &\ if \ L_{t}^{asset} \gt 0 \end{cases} \end{aligned} $$

同时考虑 超额抵押 的隐藏要求

$$ D{t} < L{t} $$

得到

$$ LR{t}^{asset} = \begin{aligned} \begin{cases} 0 &\ if \ L{t} = 0 \ or \ D_{t} = 0\ \frac{VD_t \times VR_t + SD_t \times \overline{SR}t}{L{t}} &\ if \ L_{t} \gt 0 \end{cases} \end{aligned} $$

理解如下

分子 $VR{t}$ 与 $SR{t}$ 都是年化利率,所以 $LR_{t}$ 也是年化流动性利率

分母 $L{t}$ 表示池子总存款本金,所以 $LR{t}$ 表示 每单位存款本金,产生的借款利息

指数 index

累计流动性指数

cumulated liquidity index

从池子首次发生用户操作时,累计到现在,每单位存款本金,变成多少本金(含利息收入)

cumulated liquidity index

$$ LI_t=(LRt \times \Delta T{year} + 1) \times LI_{t-1} \ LI_0=1 \times 10 ^{27} = 1 \ ray $$

其中

$$ \Delta T{year} = \frac{\Delta T}{T{year}} = \frac{T - T{l}}{T{year}} $$

$T_{l}$ 池子最近一次发生用户操作的的时间

$T_{l}$ is updated every time a borrow, deposit, redeem, repay, swap or liquidation event occurs.

--

每单位存款本金,未来将变成多少本金(含利息收入)

reserve normalized income

$$ NI_t = (LRt \times \Delta T{year} + 1) \times LI_{t-1} $$

存储

struct ReserveData {
    //the liquidity index. Expressed in ray
    uint128 liquidityIndex;
  }

更新

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;

    //only cumulating if there is any income being produced
    if (currentLiquidityRate > 0) {
      uint256 cumulatedLiquidityInterest =
        MathUtils.calculateLinearInterest(currentLiquidityRate, timestamp);

      newLiquidityIndex = cumulatedLiquidityInterest.rayMul(liquidityIndex);
      require(newLiquidityIndex &lt;= type(uint128).max, Errors.RL_LIQUIDITY_INDEX_OVERFLOW);

      reserve.liquidityIndex = uint128(newLiquidityIndex);
    }

    reserve.lastUpdateTimestamp = uint40(block.timestamp);
  }
}

累计浮动借款指数

cumulated variable borrow index

从池子首次发生用户操作时,累计到现在,每单位借款债务,共变成多少债务

cumulated variable borrow index

$$ VI_{t} = (1 + \frac{VRt}{T{year}})^{\Delta T} \times VI_{t-1} $$

--

每单位浮动借款债务,未来将变成多少债务

normalised variable (cumulated) debt

$$ VN{t} = (1 + \frac{VR{t}}{T{year}})^{\Delta T} \times VI{t-1} $$

存储

struct ReserveData {
    uint128 variableBorrowIndex;
}

计算

// ReserveLogic.sol

/**
* @dev Returns the ongoing normalized variable debt for the reserve
* A value of 1e27 means there is no debt. As time passes, the income is accrued
* A value of 2*1e27 means that for each unit of debt, one unit worth of interest has been accumulated
* @return The normalized variable debt. expressed in ray
**/
function getNormalizedDebt(DataTypes.ReserveData storage reserve) internal view returns (uint256) {
    uint40 timestamp = reserve.lastUpdateTimestamp;

    if (timestamp == uint40(block.timestamp)) {
        return reserve.variableBorrowIndex;
    }

    uint256 cumulated =
        MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp).rayMul(
        reserve.variableBorrowIndex
        );

    return cumulated;
}

更新

function _updateIndexes(
    DataTypes.ReserveData storage reserve,
    uint256 scaledVariableDebt,
    uint256 liquidityIndex,
    uint256 variableBorrowIndex,
    uint40 timestamp
  ) internal returns (uint256, uint256) {
    uint256 currentLiquidityRate = reserve.currentLiquidityRate;

    uint256 newVariableBorrowIndex = variableBorrowIndex;

    //only cumulating if there is any income being produced
    if (currentLiquidityRate > 0) {
      //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 &lt;= type(uint128).max,
          Errors.RL_VARIABLE_BORROW_INDEX_OVERFLOW
        );

        reserve.variableBorrowIndex = uint128(newVariableBorrowIndex);
      }
    }

    reserve.lastUpdateTimestamp = uint40(block.timestamp);
    return (newLiquidityIndex, newVariableBorrowIndex);
  }

用户累计浮动借款指数

user cumulated variable borrow index

$$ VI(x) = VI_t(x) $$

因为是浮动利率,所以用户每次发生借款的利率,都是当时最新的浮动借款利率

代币 token

aToken

$ScB_{t}(x)$ 用户 $x$ 在 $t$ 时刻发生存款或取回操作后,在 ERC20 Token 中记录的 balance 债务

存款

$$ ScB{t}(x) = ScB{t-1} + \frac{m}{NI_{t}} $$

取回

$$ ScB{t}(x) = ScB{t-1} - \frac{m}{NI_{t}} $$

在当前最新的 $t$ 时刻看,用户 $x$ 的总余额

$$ aB{t}(x) = ScB{t}(x) \times NI_{t} $$

Debt token

浮动借款代币

Variable debt token

$ScVB_t(x)$ 用户 $x$ 在 $t$ 时刻发生借款或还款操作后,在 ERC20 Token 中记录的 balance 债务

借款

$$ ScVBt(x) = ScVB{t-1}(x) + \frac{m}{VN_{t}} $$

还款

$$ ScVBt(x) = ScVB{t-1}(x) - \frac{m}{VN_{t}} $$

在当前最新的 $t$ 时刻看,用户 $x$ 的总债务

$$ VD(x) = ScVB(x) \times D_{t} $$

这里是个比较明显的 typo,应该修正为

$$ VD(x) = ScVB(x) \times VN_{t} $$

稳定借款代币

Stable debt token

用户 $x$ 此前在 $t$ 发生操作后

$$ SD{t}(x) = SB(x) \times (1+\frac{\overline{SR}{t-1}(x)}{T_{year}})^{\Delta T} $$

风险

借贷利率参数

Borrow Interest Rate

参考 Borrow Interest Rate

USDT $U_{optimal}$ $U_{base}$ $Slope1$ $Slope2$
Variable 90\% 0\% 4\% 60\%
Stable 90\% 3.5\% 2\% 60\%

风险参数

Risk Parameters

参考 Risk Parameters

Name Symbol Collateral Loan To Value Liquidation Threshold Liquidation Bonus Reserve Factor
DAI DAI yes 75\% 80\% 5\% 10\%
Ethereum ETH yes 82.5\% 85\% 5\% 10\%

实际用的是万分比

抵押率

Loan to Value 抵押率,表示价值 1 ETH 的抵押物,能借出价值多少 ETH 的资产

最大抵押率,是用户抵押的各类资产的抵押率的加权平均值

$$ Max LTV = \frac{ \sum{{Collateral}_i \ in \ ETH \ \times \ LTV_i}}{Total \ Collateral \ in \ ETH} $$

比如 用户存入价值为 1 ETH 的 DAI,和 1 ETH 的 Ethereum,那么

$$ Max LTV = \frac{1 \times 0.75 + 1 \times 0.825}{1+1} = 0.7875 $$

清算阈值

Liquidation Threshold

$$ LiquidationThreshold = \frac{\sum{{Collateral}_i \ in \ ETH \ \times \ {Liquidation \ Threshold}_i}}{Total \ Collateral \ in \ ETH} $$

上面的例子为

$$ LiquidationThreshold = \frac{1 \times 0.8 + 1 \times 0.85}{1 + 1} = 0.825 $$

Loan-To-Value 与 Liquidation Threshold 之间的窗口,是借贷者的安全垫

清算奖励

Liquidation Bonus

抵押物的拍卖折扣,作为清算者的奖励

健康度

Health Factor

$$ Hf = \frac{\sum{{Collateral}_i \ in \ ETH \ \times \ {Liquidation \ Threshold}_i}}{Total \ Borrows \ in \ ETH} $$

$Hf < 1$ 说明这个用户资不抵债,应该清算 (代码中,$Hf$ 单位为 ether)

假设用户抵押此前存入的2 ETH 资产,按最大抵押率借出价值 $0.7875 \times 2 = 1.575$ ETH 的债务,此时

$$ Hf = \frac{1 \times 0.8 + 1 \times 0.85}{1.575} = 1.0476 $$

相关代码如下

/**
   * @dev Calculates the user data across the reserves.
   * this includes the total liquidity/collateral/borrow balances in ETH,
   * the average Loan To Value, the average Liquidation Ratio, and the Health factor.
   **/
  function calculateUserAccountData(
    address user,
    mapping(address => DataTypes.ReserveData) storage reservesData,
    DataTypes.UserConfigurationMap memory userConfig,
    mapping(uint256 => address) storage reserves,
    uint256 reservesCount,
    address oracle
  ) internal view returns (uint256, uint256, uint256, uint256, uint256){

    CalculateUserAccountDataVars memory vars;

    for (vars.i = 0; vars.i &lt; reservesCount; vars.i++) {
      if (!userConfig.isUsingAsCollateralOrBorrowing(vars.i)) {
        continue;
      }

      vars.currentReserveAddress = reserves[vars.i];
      DataTypes.ReserveData storage currentReserve = reservesData[vars.currentReserveAddress];

      (vars.ltv, vars.liquidationThreshold, , vars.decimals, ) = currentReserve
        .configuration
        .getParams();

      vars.tokenUnit = 10**vars.decimals;
      vars.reserveUnitPrice = IPriceOracleGetter(oracle).getAssetPrice(vars.currentReserveAddress);

      if (vars.liquidationThreshold != 0 && userConfig.isUsingAsCollateral(vars.i)) {
        vars.compoundedLiquidityBalance = IERC20(currentReserve.aTokenAddress).balanceOf(user);

        uint256 liquidityBalanceETH =
          vars.reserveUnitPrice.mul(vars.compoundedLiquidityBalance).div(vars.tokenUnit);

        vars.totalCollateralInETH = vars.totalCollateralInETH.add(liquidityBalanceETH);

        vars.avgLtv = vars.avgLtv.add(liquidityBalanceETH.mul(vars.ltv));
        vars.avgLiquidationThreshold = vars.avgLiquidationThreshold.add(
          liquidityBalanceETH.mul(vars.liquidationThreshold)
        );
      }

      if (userConfig.isBorrowing(vars.i)) {
        vars.compoundedBorrowBalance = IERC20(currentReserve.stableDebtTokenAddress).balanceOf(
          user
        );
        vars.compoundedBorrowBalance = vars.compoundedBorrowBalance.add(
          IERC20(currentReserve.variableDebtTokenAddress).balanceOf(user)
        );

        vars.totalDebtInETH = vars.totalDebtInETH.add(
          vars.reserveUnitPrice.mul(vars.compoundedBorrowBalance).div(vars.tokenUnit)
        );
      }
    }

    vars.avgLtv = vars.totalCollateralInETH > 0 ? vars.avgLtv.div(vars.totalCollateralInETH) : 0;
    vars.avgLiquidationThreshold = vars.totalCollateralInETH > 0
      ? vars.avgLiquidationThreshold.div(vars.totalCollateralInETH)
      : 0;

    vars.healthFactor = calculateHealthFactorFromBalances(
      vars.totalCollateralInETH,
      vars.totalDebtInETH,
      vars.avgLiquidationThreshold
    );
    return (
      vars.totalCollateralInETH,
      vars.totalDebtInETH,
      vars.avgLtv,
      vars.avgLiquidationThreshold,
      vars.healthFactor
    );
  }

清算

继续上面的例子:用户抵押价值 2 ETH 的资产,借出 1.575 ETH 的债务,$Hf$ 为 1.0476

经过一段时间后,市场可能出现如下清算场景

场景一:用户借出的债务从价值为 1.575 ETH,上涨为 2 ETH,此时

$$ Hf = \frac{1 \times 0.8 + 1 \times 0.85}{2} = 0.825 $$

场景二:用户抵押的资产 DAI,从价值为 1 ETH,下跌为 0.8 ETH,此时

$$ Hf = \frac{0.8 \times 0.8 + 1 \times 0.85}{1.575} = 0.9460 $$

用户可能如下考虑:

假设,用户看多自己借出的债务,比如用户认为债务价值会继续上涨到 3 ETH,此时可以不做操作,任由清算

相反,用户看多自己抵押的资产,而认为债务升值无望,那么如果资产被低位清算,将得不偿失;此时用户可以追加抵押或偿还债务,避免清算

假设用户未及时操作,套利者先行一步触发清算,相关代码如下

/**
   * @return collateralAmount: The maximum amount that is possible to liquidate given all the liquidation constraints
   *                           (user balance, close factor)
   *         debtAmountNeeded: The amount to repay with the liquidation
   **/
  function _calculateAvailableCollateralToLiquidate(
    DataTypes.ReserveData storage collateralReserve,
    DataTypes.ReserveData storage debtReserve,
    address collateralAsset,
    address debtAsset,
    uint256 debtToCover,
    uint256 userCollateralBalance
  ) internal view returns (uint256, uint256) {
    uint256 collateralAmount = 0;
    uint256 debtAmountNeeded = 0;
    IPriceOracleGetter oracle = IPriceOracleGetter(_addressesProvider.getPriceOracle());

    AvailableCollateralToLiquidateLocalVars memory vars;

    vars.collateralPrice = oracle.getAssetPrice(collateralAsset);
    vars.debtAssetPrice = oracle.getAssetPrice(debtAsset);

    (, , vars.liquidationBonus, vars.collateralDecimals, ) = collateralReserve
      .configuration
      .getParams();
    vars.debtAssetDecimals = debtReserve.configuration.getDecimals();

    // This is the maximum possible amount of the selected collateral that can be liquidated, given the
    // max amount of liquidatable debt
    vars.maxAmountCollateralToLiquidate = vars
      .debtAssetPrice
      .mul(debtToCover)
      .mul(10**vars.collateralDecimals)
      .percentMul(vars.liquidationBonus)
      .div(vars.collateralPrice.mul(10**vars.debtAssetDecimals));

    if (vars.maxAmountCollateralToLiquidate > userCollateralBalance) {
      collateralAmount = userCollateralBalance;
      debtAmountNeeded = vars
        .collateralPrice
        .mul(collateralAmount)
        .mul(10**vars.debtAssetDecimals)
        .div(vars.debtAssetPrice.mul(10**vars.collateralDecimals))
        .percentDiv(vars.liquidationBonus);
    } else {
      collateralAmount = vars.maxAmountCollateralToLiquidate;
      debtAmountNeeded = debtToCover;
    }
    return (collateralAmount, debtAmountNeeded);
  }

注意 maxAmountCollateralToLiquidate,表示可以被清算的最大的抵押资产的数量

它通过计算清算债务的价值,除以抵押资产的单位价格得到

由于清算者得到的是抵押资产,而抵押资产本身面临着市场波动风险,为了鼓励清算以降低系统风险,这里会凭空乘以 liquidationBonus

比如清算的抵押资产为 DAI,根据上面链接的数据,该资产当前的 liquidationBonus 为 10500

即清算者支付 debtAmountNeeded 的债务,可以多得到 5% 的 aDAI(或 DAI)

实际清算时,需要考虑资产的阈值与奖励各有不同;而且 $Hf$ 是整体概念,而清算需要指定某个资产和某个债务;比如用户抵押 A 和 B 资产,借出 C 和 D 债务,清算时可以有 4 种选择

清算参数,与清算资产的多样化,使得清算策略复杂起来~

// 别跳清算套利的坑

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 2021-10-22 18:14
  • 阅读 ( 464 )
  • 学分 ( 24 )
  • 分类:DeFi

评论