aave v3 去中心化借贷
aave是去中心化Defi协议,用户可以在上面存款、贷款、闪电贷等。主要是依靠算法保障系统运行。官网https://aave.com
下面是以v3版本进行分析
以在银行存款进行举例,我们将现金存入银行账户,银行会给我们一个存储凭证。同样我们将资金存到aave协议中,aave会给我们1:1 mint aToken做为凭证。
存款流程整体分两步
在SupplyLogic.executeSupply中,部分代码如下
// 1. 将资金从user转给aave的aToken中
IERC20(params.asset).safeTransferFrom(msg.sender, reserveCache.aTokenAddress, params.amount);
// 2. 给用户1:1的mint aToken
bool isFirstSupply = IAToken(reserveCache.aTokenAddress).mint(
msg.sender,
params.onBehalfOf,
params.amount,
reserveCache.nextLiquidityIndex
);
在银行存款都是有利息的,利率是固定的比如:1年定期2%、3年定期2.5%等。在aave中存款的利率是和资金使用率相关的,也就是与资金池中的钱被借出去多少有关,借出去的越多,贷款利息越高,对应的存储利息越高。所以存款利率是由资金使用率和借款利率决定的。
下图是借款利率曲线图,分两个阶段。以资金最佳使用率80%做为分界线(80%的存款都借出去了),最高借款利率达79%。
以黑色线为准计算
资金使用率为 40% 小于 80% 在第一阶段 利率 = 基本利率 + $$\frac{40\%}{80\%}$$ 4%
资金使用率为 90% 大于 80% 在第二阶段 利率 = 基本利率 + 4% + $$\frac{90\% - 80\%}{20\%}$$ 75%
图1
存款利率 = 流动性利率 = 资金使用率 借款利率 = $$\frac{借出去的钱}{总存储的钱}$$ $$\frac{借出去的钱产生的利息}{借出去的钱}$$ = $$\frac{借出去的钱产生的利息}{总存储的钱}$$
它的含义就是每存入一份钱,可产生的收益。
在DefaultReserveInterestRateStrategy.calculateInterestRates中,部分计算存款利率代码如下:
vars.currentLiquidityRate = _getOverallBorrowRate( // 借款利率
params.totalStableDebt,
params.totalVariableDebt,
vars.currentVariableBorrowRate,
params.averageStableBorrowRate
).rayMul(vars.supplyUsageRatio).percentMul( // 资金使用率
// 预留金,应对客户提款和其他风险,比如最多90%的资金使用率
PercentageMath.PERCENTAGE_FACTOR - params.reserveFactor
);
用户余额 = 用户存入的钱 + 产生的利息
图2
下面举一个例子,比如要获取 t2 时刻的用户余额
b2 = m + $$\frac{mr}{1 year seconds}$$(t2 - t1) = m[1 +$$\frac{r}{1 year seconds}$$ (t2 - t1) ]
可以看出在 t2 时刻的余额等于 m 乘以一个收益率,这个收益率只与 t1 时刻的利率和时间间隔有关。
为了方便计算,aave使用累计流动性指数字段来保存这个系数的乘积,在每次更新合约状态时都会累计。
在 t1 时刻,累计流动性指数 = q = x1 x2
在 t3 时刻,累计流动性指数 = p = x1 x2 x3
那么$$\frac{p}{q}$$ 等于 $$\frac{x1 x2 x3 }{x1 x2}$$,x3就是上面m乘以的收益率
在 t3 时刻,用户余额 b3 = m * $$\frac{p}{q}$$
这种方式需要合约记录用户在 t1 时刻的累计流动性指数所以需要一个 map<address,int256>结构,这也是 aaveV1 版本的计算存储方式 v1 AToken 代码如下
// 用户存款时的累计指数
mapping (address => uint256) private userIndexes;
function calculateCumulatedBalanceInternal(
address _user,
uint256 _balance
) internal view returns (uint256) {
return _balance // 上次操作后的余额
.wadToRay()
.rayMul(core.getReserveNormalizedIncome(underlyingAssetAddress)) // 计算自从上次操作到现在的收益率
.rayDiv(userIndexes[_user]) // 除以之前累计的指数
.rayToWad();
}
观察上面的 b3 = m$$\frac{p}{q}$$,可以在简化下,用户在 t1 时刻存入 m 资金,这时 q 是已知的。那直接存储$$\frac{m}{q}$$的值。在 t3 时刻计算用户余额时直接乘以 p 就可以了。
b3 = $$\frac{m}{q}$$p
这个$$\frac{m}{q}$$也叫流动性缩放余额,这也是在 v2、v3中使用的计算方式,这样做有个好处是可以去掉 userIndexes 的存储降低 gas 费。
aaveV3 代码如下
// 存储时用缩放余额
function _mintScaled(
address caller,
address onBehalfOf,
uint256 amount,
uint256 index
) internal returns (bool) {
// 存储的amount数量除以流动性指数
uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0,Errors.INVALID_MINT_AMOUNT);
}
function balanceOf(
address user
) public view virtual override(IncentivizedERC20,IERC20) returns (uint256) {
// 计算用户余额时直接乘以当前指数即可,这里就不需要再除以用户的userIndex了
return super.balanceOf(user).rayMul(POOL.getReserveNormalizedIncome(_underlyingAsset));
}
我们到银行取款拿着凭证输入密码取出现金。同样的在aave中取款时,使用 aToken 做为凭证取回我们存入的资产。
取回资产后资金池的总量发生了变化,导致资金使用率发生变化 --> 贷款利率发生变换 --> 存储利率发生变化。所以需要重新计算贷款利率、存款利率。
贷款利率(根据上图1计算),代码如下:
// 如果资金使用率大于80%
if (vars.borrowUsageRatio > OPTIMAL_USAGE_RATIO) {
uint256 excessBorrowUsageRatio = (vars.borrowUsageRatio - OPTIMAL_USAGE_RATIO).rayDiv(
MAX_EXCESS_USAGE_RATIO
);
// 稳定利率
vars.currentStableBorrowRate +=
_stableRateSlope1 +
_stableRateSlope2.rayMul(excessBorrowUsageRatio);
// 可变利率
vars.currentVariableBorrowRate +=
_variableRateSlope1 +
_variableRateSlope2.rayMul(excessBorrowUsageRatio);
} else {
// 资金使用率小于等于80%
vars.currentStableBorrowRate += _stableRateSlope1.rayMul(vars.borrowUsageRatio).rayDiv(
OPTIMAL_USAGE_RATIO
);
vars.currentVariableBorrowRate += _variableRateSlope1.rayMul(vars.borrowUsageRatio).rayDiv(
OPTIMAL_USAGE_RATIO
);
}
在aave中贷款利率分两种,稳定利率和可变利率。
稳定利率: 在借入资产后到归还借款的这段时间都是按照借入时的利率计算,不论中间有多少次存款、借款操作导致资金使用率变化。(如果贷款利率比存款利率还低会被官方强制 rebalance)。
可变利率: 其他用户的存款、借款会导致资金的使用率变化,这就会导致借款利率发生变化,可变利率就是当其他用户改变资金利率用时,以当时的借款利率计算利息累计和。
借款条件:在银行借款需要有一定的条件,比如要有稳定的工作,稳定的居住所,民事偿还能力,这样就可以无需抵押借款。但是在去中心化里它本身就是匿名,隐藏个人信息,所以无法提供信用贷。
在aave中只需要你存储的资产价值大于你要借款的资产价值即可,也就是超额抵押。比如你存储了价值 1000eth 的资产,你只能贷款 800eth 的另一种资产。
这里存在一个问题,既然是超额抵押那就说明了我有超过 800eth 的资产,那我为啥还有贷款呢,直接兑换成想要的货币不就行了。
有几个使用情况:
1、比如我有 1000eth,存入aave赚利息,同时我再借出 800eth 做其他投资赚取额外收益。
2、我有好多币种,存储在aave中赚取利息并且很看好它们的未来趋势不想售卖,但是又急需另一种币度应对短暂问题。
存款和可变利率借款的存储计算方式一样,都是通过累计指数与缩放余额计算。
但它俩利息的计算方式不同,存款是单利计算,借款是复利计算。
比如在 t1-t2 时间段内,本金 m,利率是 r,最终计算出的余额是不一样的。
b = m + $$\frac{mr}{1 year seconds}$$ \ (t2 - t1)
可以做个简单推导
利率 R = 5%
本金 b0 = 10000元
复利周期:天
求: 存3天后的的余额
R = 5% b0 = 10000 元
b1= $ \frac{\left(b0\cdot\ R\right)}{365}1 + b0$ => b0$(\frac{R}{365}+1)$
b2= $ \frac{\left(b1\cdot\ R\right)}{365}1 + b1$ => b1$(\frac{R}{365}+1)$
b3= $ \frac{\left(b2\cdot\ R\right)}{365}1 + b2$ => b2$(\frac{R}{365}+1)$
b3 = b0 * $(\frac{R}{365}+1)^{3}$
所以在 t1-t2 时间段内的余额如下
b = m * $(\frac{r}{1 year seconds}+1)^{t2-t1}$
v3 合约代码,在MathUtils.calculateCompoundedInterest中,为节省gas将指数运算简化成如下形式。
(1+x)^n = 1+n*x+[n/2*(n-1)]*x^2+[n/6*(n-1)*(n-2)*x^3...
function calculateCompoundedInterest(
uint256 rate,
uint40 lastUpdateTimestamp,
uint256 currentTimestamp
) internal pure returns (uint256) {
//solium-disable-next-line
uint256 exp = currentTimestamp - uint256(lastUpdateTimestamp);
if (exp == 0) {
return WadRayMath.RAY;
}
uint256 expMinusOne;
uint256 expMinusTwo;
uint256 basePowerTwo;
uint256 basePowerThree;
unchecked {
// n-1
expMinusOne = exp - 1;
// n-2
expMinusTwo = exp > 2 ? exp - 2 : 0;
// x^2
basePowerTwo = rate.rayMul(rate) / (SECONDS_PER_YEAR * SECONDS_PER_YEAR);
// x^3
basePowerThree = basePowerTwo.rayMul(rate) / SECONDS_PER_YEAR;
}
// 第二项
// n * (n-1) * x^2
uint256 secondTerm = exp * expMinusOne * basePowerTwo;
unchecked {
secondTerm /= 2;
}
// 第三项
// n * (n-1) * (n-2) * x^3
uint256 thirdTerm = exp * expMinusOne * expMinusTwo * basePowerThree;
unchecked {
thirdTerm /= 6;
}
// 1+nx+[n/2(n-1)]x^2+[n/6(n-1)*(n-2)*x^3
return WadRayMath.RAY + (rate * exp) / SECONDS_PER_YEAR + secondTerm + thirdTerm;
}
计算贷款余额 = 本金(缩放余额) * variableBorrowIndex(借款指数) * calculateCompoundedInterest(xxx)
// 计算用户借款总额 = 本金 * 利息率
userTotalDebt = userTotalDebt.rayMul(reserve.getNormalizedDebt());
// 获取借款利率
function getNormalizedDebt(
DataTypes.ReserveData storage reserve
) internal view returns (uint256) {
uint40 timestamp = reserve.lastUpdateTimestamp;
//solium-disable-next-line
if (timestamp == block.timestamp) {
//if the index was updated in the same block, no need to perform any calculation
return reserve.variableBorrowIndex;
} else {
return
// variableBorrowIndex(借款指数) * calculateCompoundedInterest(xxx)
MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp).rayMul(
reserve.variableBorrowIndex
);
}
}
稳定利率计算余额时需要用户初始借款时的利率,无论后面怎么变都以当时的利率计算利息,代码如下:
function balanceOf(address account) public view virtual override returns (uint256) {
// 计算上次操作后的本金
uint256 accountBalance = super.balanceOf(account);
// 上次操作后的稳定利率
uint256 stableRate = _userState[account].additionalData;
if (accountBalance == 0) {
return 0;
}
// 输入参数: 稳定利率,上次时间戳
// 输出: 该时间段内利息增长指数
uint256 cumulatedInterest = MathUtils.calculateCompoundedInterest(
stableRate,
_timestamps[account]
);
// 再乘以本金就是当前余额,注意这里不需要再乘以index指数了,因为稳定利率借款不使用缩放存储。借多少就是多少。
return accountBalance.rayMul(cumulatedInterest);
}
aave协议中借款使用的是超额抵押,比如你想借价值 800eth 的资产,需要存储价值 1000eth 的资产到协议中。在贷款时会遍历所有资金池看用户是否有足够的抵押。代码如下
function calculateUserAccountData(
mapping(address => DataTypes.ReserveData) storage reservesData,
mapping(uint256 => address) storage reservesList,
mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories,
DataTypes.CalculateUserAccountDataParams memory params
) internal view returns (uint256,uint256,uint256,uint256,uint256,bool) {
// 遍历所有资产,累计抵押资产
// ...中间略
// 健康度计算
vars.healthFactor = (vars.totalDebtInBaseCurrency == 0)
? type(uint256).max
: (vars.totalCollateralInBaseCurrency.percentMul(vars.avgLiquidationThreshold)).wadDiv(
vars.totalDebtInBaseCurrency
);
return (
vars.totalCollateralInBaseCurrency,
vars.totalDebtInBaseCurrency,
vars.avgLtv,
vars.avgLiquidationThreshold,
vars.healthFactor,
vars.hasZeroLtvCollateral
);
还款之后资金使用率发生变化,需要重新计算借贷利率。
被隔离的资产风险系数比较高,或者出现问题时影响比较大。所以在使用隔离资产做抵押贷款时有一些限制,具体如下:
当抵押品和借入的资产有相关性时,比如 usdt dai usdc 都和美元锚定,如果美元跌了 usdt dai usdc 都会跌。这种情况下在计算健康度时也不会有太大影响 $$\frac{抵押资产价值清算阈值}{借入资产价值}$$ 所以这种情况下可以提升一下Ltv,清算阈值、手续费等,这样可以提升贷款能力。 目前有两种类型 稳定币 *ETH相关
当用户抵押的资产价值,不足借款价值时(健康度小于1),会触发清算机制。清算人归还借款,然后将借款人的抵押资产转给清算人。
清算计算逻辑如下:
1、存入10eth,借入价值5eth 的dai. 在清算时只能清算50%的dai奖金是清算价值的5% 也就是2.5eth * 5% = 0.125eth 所以清算人一共获得2.5eth + 0.125eth
2、存入5eth和价值5eth的fyi,借入5eth价值的dai。在清算时 归还50%的dai,fyi的清算奖金是15% eth 清算奖金是 5%,所以选用 fyi 的奖金(只能选一种) 2.5eth * 0.15 = 0.375eth 所以清算人一共会得到价值 2.5eth + 0.375eth价值的fyi资产
贷款-抵押比,比如要抵押房子去借款房子价值100w,只能借款70w,ltv=70%
当房子价值跌倒78w时,就要触发清算机制,清算阈值就是78%
用来衡量用户当前资金安全度,是否有抵押不足的情况。 健康度 = $$\frac{抵押资产价值*清算阈值}{借入资产价值}$$ 代码如下:
vars.healthFactor = (vars.totalDebtInBaseCurrency == 0) // 没有借款,健康度最高
? type(uint256).max
: (vars.totalCollateralInBaseCurrency.percentMul(vars.avgLiquidationThreshold)).wadDiv(
vars.totalDebtInBaseCurrency
);
在一个交易中,完成借款和还款。同时提供一些交易手续费。 用户实现自定义合约,然后用户发送交易到aave合约,将控制权交给aave闪电贷合约,在闪电贷合约中将资金转移给用户,然后再调用户自定义的合约,调用完成后检查状态,将用户贷款转附带利息转给 aave。如果这中间发生异常则终止交易。转回来的收益一部分给官方,一部分做为收益存入流动性池子。还有一种模式在闪电贷结束后,不需要归还而是转为在aave中的借款,这种模式下没有闪电贷的利息。
v3版本支持垮链,只有在白名单中的地址可以操作如下步骤 1、铸造aToken,参与存储利息收益 2、将aToken对应的标的资产转到资金池
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!