aave v3 存款与借款利息的计算方式
摘要:AAVE采用流动性指数(liquidityIndex)和标准化余额(scaledBalance)机制优化动态利率计算。流动性指数记录资产累积增长率,通过(currentLiquidityRate,lastUpdateTimestamp,liquidityIndex)三个参数实时更新。用户存款时按当前指数将本金转换为scaledBalance,提款时用scaledBalance×最新指数计算本
存款
首先我们假设小明在aave里面存了10000usdt,存的时候年化收益率是5%,那么半年后其存款的利息是多少呢?常规的计算方式如下:
利息=10000*5%*(存款的时长/一年的时长)
这么做有什么问题呢?假设现在的存款利率有变化,存的时候是5%,过了两个月变成了6%,那么现在到了第6个月用户提款的时候其利息就会变为:
利息=10000*0.05*(2/12)+10000*0.06*(4/12)
如果利率多变动几次,分步计算的数量会更多,并且需要把用户存款的时间点,以及每次利率变化的时间点和当时对应的利率记录下来,这样才能完整的计算利息。
aave的利率本身是动态的,每次有人存款,取款,借贷,还款都会触发利率的变化,如果把这些都记录下来的话对于智能合约来说是个很大的负担,不仅耗费存储量空间,在最终结息时候的计算量也是巨大的,随着合约的运行,其gas费用会越来越高!
liquidityIndex
aave使用了引入流动性指数这个参数来解决这个问题,aave中存储的每一种资产都有一个liquidityIndex的变量记录储备资产的累积流动性增长率。
一个用户最终的结息金额和三个条件相关:本金,储蓄时间,利率。实际上我们的期望是不管我存了多长时间,结息的时候乘以存款的最终利率是就好,打个比方在利率不变的情况下,年化利率5%,5%/12就是每个月的利率,存半年的最终利率就是5%/12*6=2.5%,那如果每个月利率都在变的情况下最终利率怎么算呢,按照上面的举例前面两个月是5%,后面4个月是6%,半年后取款的最终利率是多少(aave对利率计算的时间精确到秒,这里方便说明使用月代替),计算方式如下:
把时间作为权重,乘以相应的利率在累加就是最后的利率。结息的时候用本金乘以最终的利率就可以得到最后的利息金额。
这意味着每一种资产只需要记录3个值,当前的利率(currentLiquidityRate);上一次更新利率的时间戳(lastUpdateTimestamp);从存款开始到上一次更新利率所累计的总利率(liquidityIndex)
liquidityIndex的初始值为1,代表尚未产生任何利润,lastUpdateTimestamp就是存款发生的时间,随着时间的推移,liquidityIndex的值会逐渐变大,因为每时每刻都在累积利息,但其并不会被更新,直到发生了利率变化,比如当前资产发生了存取,借贷,资金池的金额发生变化就会促使利率发生变化。这个时候就会发生liquidityIndex的累加,还是用上面的例子,前面两个月利率是5%:
后面4个月利率是6%则:
如果此时取款,直接用本金乘以liquidityIndex就可以得到本息的金额。
我们进一步思考一个问题,如果每次利率改变的时候都要计算复利呢,比如上面的例子,0.83%的利润需要带入到下一轮作为本金计算利息,该如何做呢?
其实只需要把上面的公司稍微改变一下即可
后面4个月利率是6%则后面4个月对于上一轮本金的增长率为
那么6个月后总的利率就是
比之前略多,实际上aave就是采用复利的方式计算liquidityIndex
scaledBalance
细心的同学此时可能会有个疑问,上面的例子阐述了一种资产一个用户进行储蓄时的利息计算,如果有多个用户在不同的时间点存入金额,只用一个公共的liquidityIndex,如何精准计算每个人的利息呢?这就涉及到scaledBalance参数的应用,前面说到影响最终利息的三大要素本金,储蓄时间,利率,scaledBalance这个参数就是本金的变种,每个用户都会存储一份。接着上面的示例,假设现在的liquidityIndex已经从最初的1变成了1.01,那么是不是我存入10000立马就能取出10100出来呢,显然是太美好的想象,实际上每个用户存入资金的时候都会根据当前的liquidityIndex对本金进行换算,得到scaledBalance。比如此时的liquidityIndex为1.01,存入10000元,换算出的scaledBalance为10000/1.01=9900.99,真正计算本息的时候是用scaledBalance*liquidityIndex,所以存入立刻取出是不可能有任何利润的。
最后我们用示例总结下上面的内容:
场景:三个用户在不同时间存款
时间线
T0: liquidityIndex = 1.0, Alice 存入 10,000 USDC
T6: liquidityIndex = 1.025, Bob 存入 5,000 USDC
T12: liquidityIndex = 1.051, Charlie 存入 8,000 USDC
T18: liquidityIndex = 1.078, 查询所有用户余额
计算过程
Alice:
- scaledBalance = 10,000 ÷ 1.0 = 10,000
- 最终余额 = 10,000 × 1.078 = 10,780 USDC
- 利息收入 = 780 USDCBob:
- scaledBalance = 5,000 ÷ 1.025 = 4,878.05
- 最终余额 = 4,878.05 × 1.078 = 5,258.54 USDC
- 利息收入 = 258.54 USDCCharlie:
- scaledBalance = 8,000 ÷ 1.051 = 7,611.80
- 最终余额 = 7,611.80 × 1.078 = 8,205.52 USDC
- 利息收入 = 205.52 USD
借款
有了存款利率的原理做基础,理解借款利率的原理就很容易了,相应的借款最终利息的三要素借款金额,利率,时间,借款也有自己的变量存储随时间不断变大的累计利率variableBorrowIndex和经过转换后的借款金额scaledDebt,基本思想和存款基本一致!
总还款金额=variableBorrowIndex*scaledDebt
但是这里我们需要思考一个问题,在存款中每一次更新利率的时候都会对liquidityIndex都会进行复利计算,也就是
liquidityIndex =currLiquidityIndex * calculateLinearInterest
但是这样的计算方式整的足够精确吗?比如我的本金是10000,年化收益率是5%,3天后由于利率的变化,我们进行了一次累积利率的计算
calculateLinearInterest=1+3*5%/365
但实际上我的10000存款并不是第三天利率变化的时候才产生利息的,其每一秒都在不断地产生利息,然后不断的利滚利才对,正确的做法应该是加上一天的有n秒
这就是variableBorrowIndex 借款累计利率的计算方式,其精确程度比存款的要高得多,这么做得结果就是用户存款获取得利润略少,而借款需要付出得利息是按照秒级利滚利的,怎么说呢,符合银行的盈利模式。
合约代码分析
有了前面的数学推导做铺垫,我们进一步分析合约代码,对于非程序员的同学了解其数学原理已经足够,后面可以跳过!
存款利息
function _updateIndexes(
DataTypes.ReserveData storage reserve,
DataTypes.ReserveCache memory reserveCache
) internal {
// Only cumulating on the supply side if there is any income being produced
// The case of Reserve Factor 100% is not a problem (currentLiquidityRate == 0),
// as liquidity index should not be updated
if (reserveCache.currLiquidityRate != 0) {
uint256 cumulatedLiquidityInterest = MathUtils.calculateLinearInterest(
reserveCache.currLiquidityRate,
reserveCache.reserveLastUpdateTimestamp
);
reserveCache.nextLiquidityIndex = cumulatedLiquidityInterest.rayMul(
reserveCache.currLiquidityIndex
);
reserve.liquidityIndex = reserveCache.nextLiquidityIndex.toUint128();
}
// Variable borrow index only gets updated if there is any variable debt.
// reserveCache.currVariableBorrowRate != 0 is not a correct validation,
// because a positive base variable rate can be stored on
// reserveCache.currVariableBorrowRate, but the index should not increase
if (reserveCache.currScaledVariableDebt != 0) {
uint256 cumulatedVariableBorrowInterest = MathUtils.calculateCompoundedInterest(
reserveCache.currVariableBorrowRate,
reserveCache.reserveLastUpdateTimestamp
);
reserveCache.nextVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul(
reserveCache.currVariableBorrowIndex
);
reserve.variableBorrowIndex = reserveCache.nextVariableBorrowIndex.toUint128();
}
}
这段代码是 Aave V3 协议中负责更新储备金索引的核心方法。分为两个部分,上面是计算存款利率,下面是计算借款利率,首先是非0判断,当该资产池没有任何借贷时,利率为0,此时无需更新利率。
第一步是通过calculateLinearInterest计算从上次利率更新的时间点到现在所产生的利率累积。
uint256 internal constant RAY = 1e27;
function calculateLinearInterest(
uint256 rate,
uint40 lastUpdateTimestamp
) internal view returns (uint256) {
//solium-disable-next-line
uint256 result = rate * (block.timestamp - uint256(lastUpdateTimestamp));
unchecked {
result = result / SECONDS_PER_YEAR;
}
return WadRayMath.RAY + result;
}
aave对于利率时间的处理精确到了秒,利率的精度为27位小数,上面的代码翻译成公式就是
当前累计利率=1 +(从上次更新利率到现在的秒数 / 一年的总秒数)* 当前的利率
第二部分就是计算总的liquidityIndex
function wadMul(uint256 a, uint256 b) internal pure returns (uint256 c) {
// to avoid overflow, a <= (type(uint256).max - HALF_WAD) / b
assembly {
if iszero(or(iszero(b), iszero(gt(a, div(sub(not(0), HALF_WAD), b))))) {
revert(0, 0)
}
c := div(add(mul(a, b), HALF_WAD), WAD)
}
}
翻译成公式就是
按照之前的推导currLiquidityIndex和culateLinearInterest直接相乘就能得到总的li利率累积,先加上再除以
是出于四舍五入的考虑
假设a*b=3.456 如果保留两位小数会直接把最后一位舍弃变成3.45,如果想要达到四舍五入的效果可以现在最后一位加上五也就是3.456+0.005=3.461,然后舍弃最后一位变成3.46。
借款利息
根据前面的公式推导可以看出,借款利息和存款利息的计算方式有些许不同,存款利息是每次利率变动的时候进行复利计算,而借款利息则会计算每一秒的滚动利息。
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;
}
unchecked {
// this can't overflow because rate is always fits in 128 bits and exp always fits in 40 bits
uint256 x = (rate * exp) / SECONDS_PER_YEAR;
return WadRayMath.RAY + x + x.rayMul(x / 2 + x.rayMul(x / 6));
}
}
rate 是当前的利率;exp为时间差,单位是秒;SECONDS_PER_YEAR是一年的秒数
理想状态下计算的是
由于区块链中无法直接计算指数函数,这里只能计算近似值,使用泰勒展开到三阶
这个公式懂的自然懂,不懂的自己去复习下微积分!!!
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)