这几天在学习 AAVE,资料看了 V1 和 V2 的白皮书,代码只看了 V2 版本,另外感谢大佬分享:
AAVE v2 - white paper
aave小组分享:白皮书解读
Dapp-Learning: 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}
这里的 \#
可以为 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}
正相关
也就是说,借贷需求旺盛时,利率随着资金利用率上升;借贷需求萎靡时,利率随着资金利用率下降
代码
存储
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()
假设利率为 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}
代码
存储
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=(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}
存储
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 <= 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}
存储
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 <= 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
USDT | U_{optimal} |
U_{base} |
Slope1 |
Slope2 |
---|---|---|---|---|
Variable | 90% | 0% | 4% | 60% |
Stable | 90% | 3.5% | 2% | 60% |
风险参数
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 < 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.946
用户可能如下考虑:
假设,用户看多自己借出的债务,比如用户认为债务价值会继续上涨到 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 种选择
清算参数,与清算资产的多样化,使得清算策略复杂起来~
// 别跳清算套利的坑
Hi 作者大大,calculateUserAccountData()里面:
vars.compoundedLiquidityBalance = IERC20(currentReserve.aTokenAddress).balanceOf(user);
vars.compoundedLiquidityBalance = IERC20(currentReserve.aTokenAddress).balanceOf(user);
vars.compoundedBorrowBalance = vars.compoundedBorrowBalance.add(
IERC20(currentReserve.variableDebtTokenAddress).balanceOf(user)
为什么用的都是IERC20的balanceOf()实现,这样算出来的LiquidityBalance和浮动利率债务就是Scaled版本,这样ok吗