Aave v3源代码解析

aave是一个分布式的借贷协议,提供固定利率、浮动利率的抵押借款,更创新性地推出了无抵押的闪电贷功能,衍生出了很多的Defi应用场景。

我们从aave的核心智能合约代码库aave-v3-core来解析一下这些功能是如何实现的。

代码梗概

IPool定义了aave借贷池的接口,下面是借贷池的核心方法。

supply(提供流动性): 用户向aave协议的某个资产储备(reserve)存入token提供流动性,得到对应的aToken作为存储凭证,比如存入USDC,将得到aUSDC。

setUserUseReserveAsCollateral: 用户更新某种资产的抵押逻辑。

borrow(借款): 用户基于在平台的抵押物价值或他人授信额度(通过debt token表示)借出对应的资产。

withdraw: 用户提现。

repay: 用户偿还借款。

flashLoan: 用户进行无抵押借款,并在同一区块偿还借款和手续费。

liquidationCall: 清算不健康的借款。

逻辑详解

这一部分我们来分析各个核心方法的内部逻辑

supply

  • 更新资产状态,包括liquidityIndex、variableBorrowIndex、lastUpdateTimestamp,累计资产总量

  • 检查资产储备中资产量是否达到上限

  • 更新资产相关利率,包含LiquidityRate(流动性利率)、StableBorrowRate(固定借款利率)、VariableBorrowRate(动态借款利率)

  • 转入用户的资产token到对应的aToken地址

  • 铸造aToken到用户地址

setUserUseReserveAsCollateral

  • 检查用户存款状态和对应资产状态

  • 设置资产为抵押物

    • 检查用户在当前资产是否处于隔离模式

    • 设置资产为抵押物

  • 取消资产为抵押物

    • 取消资产为抵押物

    • 检查取消后用户在当前资产的healthFactor和LTV是否符合要求

borrow

  • 更新资产状态

  • 检查是否满足借贷条件

    • 资产是否Active,是否Paused,是否Frozen,是否borrowingEnabled

    • 预言机是否设置了允许借贷

    • 计息模式检查

    • 借款总额是否超过借款上限,借款总额=固定利率总借款+动态利率总借款+当前请求借款

    • 资产隔离(isolation)模式:检查资产是否是可借款状态(borrowableInIsolation),隔离模式总债务是否小于隔离模式总债务上限

    • 用户EModeCategory不为空:检查资产的EModeCategory是否和用户的一致

    • 计算总抵押物价值,检查是否大于0

    • 当前LTV是否大于0

    • healthFactor(用户总抵押物/总债务)是否大于阈值(1)

    • 需要的抵押物(用户总债务+需要借款金额)/所有资产平均LTV,是否小于用户当前的总抵押物价值

    • 固定利率借款:借款金额是否小于当前资产剩余可借金额

  • 稳定利率借款:mint稳定债务token到用户地址

  • 动态利率借款:mint动态债务token到用户地址

  • 更新资产相关利率

  • 转出资产token到用户地址

withdraw

  • 更新资产状态

  • 检查用户持有的aToken是否足够覆盖请求提现的金额

  • 更新资产相关利率

  • 销毁用户持有的aToken

  • 转出资产token到用户的地址

repay

  • 更新资产状态

  • 检查是否满足还款条件(对应利率模式的债务大于0)

  • 销毁对应债务的debtToken(StableDebtToken、VariableDebtToken)

  • 更新资产相关利率

  • 使用aToken还款:销毁对应的aToken

  • 使用原始资产还款:将资产token转到对应的aToken合约地址

flashLoan

  • 检查请求借款的每个资产的状态

  • 设置IFlashLoanReceiver对象(如果要使用闪电贷功能,调用方需要实现IFlashLoanReceiver接口)

  • 计算当前闪电贷请求借款的所有资产的费用

  • 将用户请求借款的资产转到用户地址

  • 回调用户自定义的智能合约逻辑

  • 如果调用者选择在同一区块还款(interestRateMode == InterestRateMode.NONE),处理还款逻辑

    • 计算给aave协议的费用

    • 计算给流动性池的费用

    • 更新资产状态

    • 更新资产相关利率

    • 将本金和所有费用转到资产对应的atoken地址

  • 如果调用者选择不返还资产,将执行一次借款逻辑,生成一笔债务,并向调用者发放debtToken( opens a debt position)

liquidationCall

  • 更新债务资产状态

  • 计算用户的healthFactor

  • 计算用户总债务、动态利率债务、可清算债务额(healthFactor>0.95时为50%,小于等于0.95时为100%)

  • 检查清算调用是否满足条件

    • 抵押物和债务资产是否为active和paused的

    • 预言机是否设置为允许清算

    • healthFactor是否小于1

    • 清算阈值是否大于0,用户是否允许抵押

    • 用户是否有债务

  • 计算可被清算的抵押物

    • actualCollateralToLiquidate:实际被清算抵押物,当目标清算抵押物价值小于债务价值时为全部抵押物,如果抵押物大于债务价值时,为债务值加清算奖励(bonus),如果有平台手续费,需减去手续费

    • actualDebtToLiquidate:实际被清算债务,当目标清算抵押物价值小于债务价值时为全部抵押物价值减去清算奖励,如果抵押物大于债务价值,为清算债务值

    • liquidationProtocolFeeAmount:付给aave协议的费用

  • 消除债务,销毁用户债务token,优先销毁动态利率债务,再销毁固定利率债务

  • 更新债务资产利率

  • 如果用户设置债务资产为隔离模式,更新隔离债务

  • 如果清算者接受aToken,清算用户的aToken,将用户作为抵押物的aToken转到清算者地址

  • 如果清算者不接受aToken,销毁aToken,将抵押资产原始token转到清算者地址

  • 划转清算费用到aave清算费用地址

  • 从清算者地址转出债务资产token(actualDebtToLiquidate)到对应aToken地址

核心机制

核心模块

上一部分我们分析了各个核心业务方法的内部逻辑,对支撑这些功能的基础实体模块也有了一个初步了解,下图是aave的核心模块和相互之间的关系。

aave协议核心模块
aave协议核心模块

reserve对应着aave支持的每一种资产,每个reserve又会衍生出来三种token:aToken、stableDebtToken、variableDebtToken,分别用来记录用户在该资产提供的流动性、稳定利率债务和动态利率债务。基于这几个token,可以算出用户的healthFactor,用户的借款、提现、清算动作能否执行都要基于healthFactor去做判断,是用户在与协议交互过程中的关键风控变量。

流动性的利息如何计算

在aave协议中,主要通过liquidityIndex变量来实现流动性计息。

首先我们来看一下它的计算方式:

liquidityIndex = cumulatedLiquidityInterest * lastLiquidityIndex

cumulatedLiquidityInterest = currentLiquidityRate * durationSinceLastUpdate / secondsPerYear + 1

具体aave又是如何通过liquidityIndex来实现计息的呢?

当用户提供数量为amount1的流动性时,aave会为用户铸造对应的aToken,具体的铸造数量为:amount1 / currentLiquidityIndex

当过了一段时间,用户去提取amount2数量的资产时,aave会销毁对应的aToken,并将资产转给用户,具体销毁的aToken数量为:amount2 / currentLiquidityIndex

单独观察这两个式子,可能无法理解具体的计息逻辑,这里我们设提供流动性时的liquidityIndex为oldLiquidityIndex,经过的时间为t0,基于上面两个式子可以得出,经过t0时间以后,aToken的价值为:

amount1 * currentLiquidityIndex / oldLiquidityIndex

= amount1 * (currentLiquidityRate * t0 / secondsPerYear + 1) * oldLiquidityIndex / oldLiquidityIndex

= amount1 * (currentLiquidityRate * t0 / secondsPerYear + 1)

这就是我们一般意义上的利息计算公式,当跨越多个时间段或利率变化多次时,我们只需再乘几次(currentLiquidityRate * t / secondsPerYear + 1),该机制依旧有效。