随着去中心化金融(DeFi)生态系统的迅速发展,AAVE v2 作为领先的去中心化借贷协议之一,在提供创新借贷与流动性管理解决方案方面始终处于行业前沿。其独特的无信任机制和高效的资本利用率吸引了大量用户和机构的参与。然而,随着其应用的普及及所涉及的资金规模逐步扩大,安全审计和风控措施的重要性日益凸显。本文将深入探讨 AAVE v2 协议的核心设计和关键功能,同时为开发人员和安全研究人员提供相关审计要点,以便帮助更好地识别潜在风险并确保协议的安全运行。
AAVE v2 是一个基于以太坊区块链构建的开放式借贷平台,允许用户存入各种 ERC-20 代币并从中赚取利息,同时也允许以支付利息的形式借用市场中的代币。通过引入 "利率市场" 的概念,AAVE v2 实现了去中心化的资金池管理和自动化的利率调整机制。此外,AAVE v2 还提供了闪电贷、抵押贷款和代币交换等高级功能,以满足用户的多样化需求,进一步巩固了其在 DeFi 领域的领先地位。
AAVE v2 的整体架构设计围绕用户、资金流动管理、抵押机制、清算流程以及利率策略等关键功能展开,旨在提供高效且安全的去中心化借贷服务。以下是综合分析:
- 用户:用户可以进行存款、借款、偿还、提取、借贷利率模式交换、闪电贷以及委托信用等多种操作。用户与协议交互时,会根据其操作自动铸造或销毁相应的 aTokens,代表其在协议中的存款权益,并根据利率策略获得收益。
- 委托信用:用户可以将自己的信用额度委托给其他用户,扩展了协议的灵活性和使用场景。
- LendingPool:作为核心模块负责处理所有用户的操作请求,包括存款、借款、偿还、借贷利率模式交换、闪电贷和清算,并更新利率和状态。
- Collateral Manager:管理抵押资产,确保用户借款行为安全可控。当抵押资产不足时,会触发清算流程来保护系统的整体流动性。
- Libraries:封装储蓄金逻辑,验证逻辑,通用功能逻辑,如清算和借贷操作的计算,为 LendingPool 提供支持。
-
GenericLogic 计算并验证用户状态包括资产评估、抵押品价值计算、健康系数等操作。
//检查用户余额是否可以减少而不被清算 function balanceDecreaseAllowed( ... ) external view returns (bool) { ... } //计算用户的账户数据,包括总抵押、总债务价值(ETH)、债务和健康因子 function calculateUserAccountData( ... ) internal view returns (...) { ... } //根据抵押物、债务和清算阈值计算健康因子 function calculateHealthFactorFromBalances( ... ) internal pure returns (uint256) { ... } //根据抵押物、债务和贷款价值比(LTV)计算用户可借的最大金额 function calculateAvailableBorrowsETH( ... ) internal pure returns (uint256) { ... }
-
ReserveLogic 用于管理储备金池,追踪和更新每种资产的存款量、借款量以及当前利率情况。
//获取储备的当前标准化收入 function getNormalizedIncome(DataTypes.ReserveData storage reserve) internal view returns (uint256){ ... } //获取储备的当前标准化变量债务 function getNormalizedDebt(DataTypes.ReserveData storage reserve) internal view returns (uint256){ ... } //更新储备的流动性累积指数和变量借贷指数 function updateState(DataTypes.ReserveData storage reserve) internal { ... } //将预定义的资产金额累积到储备的流动性指数中 function cumulateToLiquidityIndex( ... ) internal { ... } //更新储备的稳定借贷利率、变量借贷利率和流动性利率 function updateInterestRates( ... ) internal { ... }
-
ValidationLogic 负责验证用户的操作是否符合协议规则,在用户进行存款、借款、还款、清算、闪电贷、切换债务模式等操作时,对抵押品和负债进行严格检查。
//验证提现操作,需要满足 balanceDecreaseAllowed 的检查 function validateWithdraw( ... ) external view { ... } //验证借款操作,要求用户有足够的抵押品价值覆盖新的借款请求且健康因子需要大于清算阀值 function validateBorrow( ... ) external view { ... } //验证还款操作,储蓄为活跃状态且存在债务 function validateRepay( ... ) external view { ... } //验证借款利率模式切换操作,要求储蓄为活跃状态且未被冻结,防止借款与抵押品之间的循环依赖 function validateSwapRateMode( ... ) external view { ... } //验证闪电贷操作传参 function validateFlashloan(address[] memory assets, uint256[] memory amounts) internal pure { ... } //验证清算操作是否合法,要求用户的健康系数一定是小于清算阀值的 function validateLiquidationCall( ... ) internal view returns (uint256, string memory) { ... }
-
Debt Tokens:用来跟踪用户的借款负债,与贷出资金数额 1:1。债务代币分别有固定利率和可变利率选项(如 DebtDAI Stable、DebtDAI Variable 等),且债务代币不可转移。
-
aTokens:用户存入资产时会生成 1:1 的 aTokens 锚定底层资产,这些 aTokens 会不断增值以反映存款所赚取的利息。其中由此引入与本金余额一起存储为一个比率,称为缩放余额 scaled balance(ScB)。
公式如下:举例:
- 初始阶段 (指数 = 1.0): 存入 100 DAI → 得到 100 aDAI 简单 1:1 兑换
- 一段时间后 (指数 = 1.1): 第一笔存款时的 100 DAI 已经增值到 110 aDAI 新存入 100 DAI 的计算: Scaled Balance = 100/1.1 ≈ 90.91 实际获得 = 90.91 × 1.1 = 100 aDAI
- 继续存款 (存入 55 DAI): 新的 Scaled Balance = 100 + 55/1.1 = 150 实际余额 = 150 × 1.1 = 165 aDAI
- 取款操作 (取出 88 DAI): 最终 Scaled Balance = 150 - 88/1.1 = 70 剩余数量 = 70 × 1.1 = 77 aDAI 关键公式:实际余额 = ScB × 利息指数
- Oracles Proxy:依赖外部预言机(Chainlink)提供资产市场价格数据,用于评估用户抵押资产的价值,确保借贷行为的定价准确性和系统的稳定性。
- Lending Rate Oracle:根据系统的状态和市场情况,提供动态的借贷利率,优化资本利用率和流动性。
- Configurator:用于配置系统参数,如不同资产的风险参数和借贷限额,管理储备金的各种操作,包括激活、借款、抵押、冻结、更新参数及在紧急情况下启用或禁用功能。确保协议可以根据市场变化进行动态调整。
- Liquidation Manager:当用户抵押品价值下降至清算门槛以下时,管理清算操作,保护系统的资金安全。清算人可以通过清算操作获得奖励。
- Reserves Balances:存储系统的储备资金数据,用于计算和调整利率策略。
struct ReserveConfigurationMap {
//bit 0-15: LTV
//bit 16-31: Liq. threshold
//bit 32-47: Liq. bonus
//bit 48-55: Decimals
//bit 56: Reserve is active
//bit 57: reserve is frozen
//bit 58: borrowing is enabled
//bit 59: stable rate borrowing enabled
//bit 60-63: reserved
//bit 64-79: reserve factor
uint256 data;
}
uint256 data = 156797690026148679393338;
// 对于每个字段,我们使用掩码提取相应位
// 1. LTV (bits 0-15): 掩码 0xFFFF
uint256 ltv = data & 0xFFFF; // = 8250 (82.50%)
// 2. Liquidation threshold (bits 16-31): 掩码 0xFFFF0000,右移16位
uint256 liqThreshold = (data & 0xFFFF0000) >> 16; // = 8600 (86.00%)
// 3. Liquidation bonus (bits 32-47): 掩码 0xFFFF00000000,右移32位
uint256 liqBonus = (data & 0xFFFF00000000) >> 32; // = 500 (5.00%)
// 4. Decimals (bits 48-55): 掩码 0xFF000000000000,右移48位
uint256 decimals = (data & 0xFF000000000000) >> 48; // = 18
// 5. Flags (bits 56-63):
bool isActive = (data & (1 << 56)) != 0; // = true
bool isFrozen = (data & (1 << 57)) != 0; // = false
bool borrowingEnabled = (data & (1 << 58)) != 0; // = true
bool stableBorrowEnabled = (data & (1 << 59)) != 0; // = true
// 6. Reserve factor (bits 64-79): 掩码 0xFFFF000000000000000,右移64位
uint256 reserveFactor = (value >> 64) & 0xFFFF;; // = 8500 (85.00%)
-
Interest Rate Strategy:根据市场和用户需求,动态调整利率以实现最佳资本配置,同时考虑流动性风险,确保系统在不同市场条件下的灵活性和稳定性。
尽管存在两种利率模型稳定型和浮动型,但是其模型计算都类似于一个的拐点型模型。在拐点最优利用率下的 slope1 和超过最优利用率的 slope2 分段计算。且在这个条件行也分为固定利率模型和可变利率模型。
function calculateInterestRates(
…
)
Public view override returns (…){
…
// 使用率 = 总借款 / (可用流动性 + 总借款)
vars.utilizationRate = vars.totalDebt == 0
? 0
: vars.totalDebt.rayDiv(availableLiquidity.add(vars.totalDebt));
vars.currentStableBorrowRate = ILendingRateOracle(addressesProvider.getLendingRateOracle())
.getMarketBorrowRate(reserve);
if (vars.utilizationRate > OPTIMAL_UTILIZATION_RATE) {
// excessUtilizationRateRatio = (U - U_OPTIMAL) / (RAY - U_OPTIMAL)
uint256 excessUtilizationRateRatio = vars.utilizationRate.sub(OPTIMAL_UTILIZATION_RATE).rayDiv(EXCESS_UTILIZATION_RATE);
//稳定借款利率 = 当前稳定借款利率 + 利率_slope1 + 利率_slope2 * excessUtilizationRateRatio
vars.currentStableBorrowRate = vars.currentStableBorrowRate.add(_stableRateSlope1).add(
_stableRateSlope2.rayMul(excessUtilizationRateRatio)
);
//浮动借款利率 = 当前可变借款利率 + 利率_slope1 + 利率_slope2 * excessUtilizationRateRatio
vars.currentVariableBorrowRate = _baseVariableBorrowRate.add(_variableRateSlope1).add(
_variableRateSlope2.rayMul(excessUtilizationRateRatio)
);
} else {
//稳定借款利率 = 当前稳定借款利率 + 利率_slope1 * (U / U_OPTIMAL)
vars.currentStableBorrowRate = vars.currentStableBorrowRate.add( _stableRateSlope1.rayMul(vars.utilizationRate.rayDiv(OPTIMAL_UTILIZATION_RATE))
);
//变动借款利率 = 当前可变借款利率 + 利率_slope1 * (U / U_OPTIMAL)
vars.currentVariableBorrowRate = _baseVariableBorrowRate.add( vars.utilizationRate.rayMul(_variableRateSlope1).rayDiv(OPTIMAL_UTILIZATION_RATE)
);
}
//当前流动性利率 = 加权借款利率 * 使用率 * (1 - 储备金率)
vars.currentLiquidityRate = _getOverallBorrowRate(
totalStableDebt,
totalVariableDebt,
vars
.currentVariableBorrowRate,
averageStableBorrowRate
)
.rayMul(vars.utilizationRate)
.percentMul(PercentageMath.PERCENTAGE_FACTOR.sub(reserveFactor));
return (
…
);
}
用户通过调用LendingPool合约的deposit函数进行存款,该函数接受四个参数:资产地址、存款金额、接收方地址及推荐码。首先验证合约未处于启用状态,然后通过 ValidationLogic.validateDeposit 验证存款金额必须大于 0,同时确认确认储备处于激活状态且未被冻结。接着系统会更新储备状态,调用 reserve.updateState() 更新流动性累积指数和可变借款指数, 并计算时间段内产生的利息,其中一部分利息会被铸造并转入协议国库。
function _updateIndexes(
...
) internal returns (uint256, uint256) {
...
// 更新流动性指数
if (currentLiquidityRate > 0) {
uint256 cumulatedLiquidityInterest =
// 计算简化为 1 + ratePerSecond * (∆t / 365 days)
MathUtils.calculateLinearInterest(currentLiquidityRate, timestamp);
newLiquidityIndex = cumulatedLiquidityInterest.rayMul(liquidityIndex);
...
// 更新可变借款指数
if (scaledVariableDebt != 0) {
uint256 cumulatedVariableBorrowInterest =
// 也就是 (1 + ratePerSecond) ^ ∆t MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp);
newVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul(variableBorrowIndex);
...
}
}
...
}
其对应公式如下
随后通过 reserve.updateInterestRates 根据最新的供需关系动态调整流动性利率、稳定借款利率和可变借款利率(都由 DefaultReserveInterestRateStrategy.calculateInterestRates 函数计算更新)。资产转移环节,系统将用户的基础资产转入 aToken 合约,同时铸造等额 aToken 给用户所提交的 onBehalfOf 地址。其中,aToken 采用缩放机制 (scaled balance) 处理利息累积。如果是用户首次存款,系统会自动将该资产标记为用户的抵押品。
与Compound 相比,AAVE V2 的存款过程有以下主要特点: 支持指定接收方地址(onBehalfOf)。 通过 ValidationLogic 合约进行存款验证。 更新流动性累积指数和可变借款指数计算并分配协议国库利息。 同时调整流动性、稳定借款和可变借款三种利率。 使用aToken的缩放机制(scaled balance)处理利息。 首次存款自动标记为抵押品。
用户通过调用 withdraw 函数进行提现操作。首先取指定资产的储备数据,包括对应的 aToken 地址,检查此用户在 aToken 中的余额。接下来,调用 ValidationLogic.validateWithdraw 函数来验证提现请求,包括检查提现金额是否有效、用户余额是否足够、储备是否处于活动状态等。其中通过 GenericLogic.balanceDecreaseAllowed 中对用户的健康系数以及提现是否影响抵押品进行检查(类似于 compound 中 getHypotheticalAccountLiquidityInternal)函数的作用。在 balanceDecreaseAllowed 函数中 calculateUserAccountData 和 calculateHealthFactorFromBalances 函数计算出取出资金后的清算阀值并检查用户总抵押,总借贷数额以及用户当前的健康系数,以此来判断是否用户健康系数处于流动性阀值的安全状态。
function balanceDecreaseAllowed(
...
) external view returns (bool) {
...
vars.liquidationThresholdAfterDecrease = vars
.totalCollateralInETH
.mul(vars.avgLiquidationThreshold)
.sub(vars.amountToDecreaseInETH.mul(vars.liquidationThreshold))
.div(vars.collateralBalanceAfterDecrease);
uint256 healthFactorAfterDecrease =
calculateHealthFactorFromBalances(
vars.collateralBalanceAfterDecrease,
vars.totalDebtInETH,
vars.liquidationThresholdAfterDecrease
);
//要求取款后的健康因子需要大于清算阀值
return healthFactorAfterDecrease >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD;
}
function calculateHealthFactorFromBalances(
uint256 totalCollateralInETH,
uint256 totalDebtInETH,
uint256 liquidationThreshold
) internal pure returns (uint256) {
if (totalDebtInETH == 0) return uint256(-1);
return
// 健康因子 = 总质押inETH * 清算阀值 / 总债务inETH
(totalCollateralInETH.percentMul(liquidationThreshold)).wadDiv(totalDebtInETH);
}
HF 计算公式如下:
随后更新储备的状态,并更新利率,将提现金额传递给函数。若用户请求的提现金额等于其当前余额,则更新用户配置,将该储备标记为不再作为抵押使用。最后销毁用户的 aToken,并将提现的资产转账到指定的地址。
与Compound相比,AAVE V2的提现过程有以下主要特点:
使用 aToken 代表用户在协议中的存款,提现实际上是销毁 aToken。
允许用户提现到指定地址(通过to参数),增加了灵活性。
提供了部分提现和全额提现的选项。
在提现验证中,AAVE使用了更复杂的 balanceDecreaseAllowed 函数来检查提现对用户整体抵押品状况的影响。
AAVE的提现过程直接更新了利率,而不是像 Compound 那样通过 accrueInterest 函数来更新。
用户借款通过 borrow 函数进行借贷,执行借款会先从价格预言机获取资产的当前价格,将借款金额转换为 ETH 等价。随后通过 ValidationLogic.validateBorrow 检查以及 GenericLogic.calculateUserAccountData 用户借款是否合法,计算包括 onBehalfOf 地址的总抵押资产、总债务、当前贷款价值比率(LTV)、清算阈值和健康因子以及市场的稳定性等(类似于Compound的getHypotheticalAccountLiquidityInternal),是否有足够的抵押资产借贷。reserve.updateState 更新储备状态,如利率和借款指数(这一步类似于 compound 中的 accrueInterest),用于计算并更新利息。
function validateBorrow(
…
) external view {
...
require(vars.userCollateralBalanceETH > 0, Errors.VL_COLLATERAL_BALANCE_IS_0);
//验证计算后的健康因子是需要大于清算阀值
require(
vars.healthFactor > GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD,
Errors.VL_HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD
);
//add the current already borrowed amount to the amount requested to calculate the total collateral needed.
vars.amountOfCollateralNeededETH = vars.userBorrowBalanceETH.add(amountInETH).percentDiv(
vars.currentLtv
); //LTV is calculated in percentage
//检查用户抵押品价值是足够覆盖新的借款请求
require(
vars.amountOfCollateralNeededETH <= vars.userCollateralBalanceETH,
Errors.VL_COLLATERAL_CANNOT_COVER_NEW_BORROW
);
...
}
function calculateUserAccountData(
...
)internal view returns (uint256,uint256,uint256,uint256,uint256){
...
//遍历所有资产
for (vars.i = 0; vars.i < reservesCount; vars.i++) {
if (!userConfig.isUsingAsCollateralOrBorrowing(vars.i)) {
continue;
}
...
//以 ETH 价值计算用户总抵押品价值、加权平均贷款价值比率、加权平均清算阈值
if (vars.liquidationThreshold != 0 && userConfig.isUsingAsCollateral(vars.i)) {
vars.compoundedLiquidityBalance = IERC20(currentReserve.aTokenAddress).balanceOf(user);
uint256 = 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)
);
}
//以 ETH 价值计算总债务价值
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;
//计算健康因子,并在 validateBorrow 进行验证
vars.healthFactor = calculateHealthFactorFromBalances(
vars.totalCollateralInETH,
vars.totalDebtInETH,
vars.avgLiquidationThreshold
);
...
}
随后根据用户选择进行的 interestRateMode 稳定利率或浮动利率)生成债务。选择不同的利率模型的代币合约来铸造代币。同时,铸造代币时会检查如果 onBehalfOf 地址不是调用者,则会在 代币合约中减去其对调用用户的借贷授权。如果是用户的首次借款,会将其配置为活跃借款者。DebtToken 铸造给用户后,协议会通过 updateInterestRates 更新借款利率,反映借款后的新利率和储备池的变化。如果用户请求释放借款的底层资产,协议会将资产直接转移给用户。
与Compound相比,AAVE V2的借贷过程有以下主要特点: 支持稳定和可变两种利率模式。 使用单独的验证逻辑合约进行借贷验证。 使用债务代币(DebtToken)来表示用户的借款。 支持信用委托,允许用户代表其他地址进行借款。
参数 | 定义和用途 | 计算函数/模块 | 存储位置 |
---|---|---|---|
APY | 年化收益率 | calculateInterestRates | ReserveData (currentLiquidityRate) |
Total Borrowed | 总借款量,变量和稳定借款之和( 债务代币的 totalSupply) | calculateUserAccountData / updateState | ReserveData (totalStableDebt, totalVariableDebt) |
Reserve Factor | 储备金比例,影响存款人收益和储备金积累 | calculateInterestRates | ReserveConfigurationMap (bit 64-79) |
Borrow APR | 年化借款利率 | calculateInterestRates | ReserveData (currentStableBorrowRate, currentVariableBorrowRate) |
Total Supplied | 总供应量,表示当前储备中所有用户的存款总和。 | calculateUserAccountData / updateState | ReserveData.totalATokenSupply |
Supply APR (Annual Percentage Rate) | 存款 APR,是年化存款利率,不考虑复利。 | ReserveLogic.calculateInterestRates | ReserveData.currentLiquidityRate(直接读取) |
用户通过 repay 函数进行还款,首先获取用户的当前债务(包括稳定债务 stableDebt 和浮动债务 variableDebt)。根据用户选择的利率模式(稳定或浮动),由 ValidationLogic.validateRepay 验证用户的还款操作合法性。包括用户的债务余额是否足够进行还款,根据用户选择的利率模式来确定还款的具体债务类型(稳定利率或浮动利率)。如果用户要还的金额小于当前债务余额,系统会使用用户提供的还款金额进行部分还款;否则,将偿还所有债务。更新储备的状态 updateState,用于计算并更新协议中的利息、借贷量以及借贷指数。随后燃烧相应的稳定债务代币,并通过 updateInterestRates 更新借款利率。此时,如果用户的所有债务(包括稳定和浮动债务)在还款后为零,则会将该用户的借款状态标记为 false,表示用户不再借款。最后用户将还款金额从其账户转移到协议的 aToken 合约地址。
与Compound 相比,AAVE V2的还款过程有以下主要特点:
支持稳定和浮动两种利率模式的还款。
使用DebtToken来表示和管理债务,还款时燃烧对应债务代币。
支持部分还款和全额还款,并分别处理稳定债务和浮动债务。
支持用户通过信用委托为其他地址还款。
用户通过 lendingpool 的 liquidationCall 函数进行清算,函数通过代理模式调用 LendingPoolCollateralManager 的 liquidationCall 函数,确保函数的成功执行。首先 GenericLogic.calculateUserAccountData 获取抵押品资产及债务资产的储备数据和用户的配置信息,计算用户的健康因子,并通过 getUserCurrentDebt 获取用户的当前稳定和可变负债。 ValidationLogic.validateLiquidationCall 函数验证清算调用的合法性,包括检查用户的健康因子、债务状态和抵押品配置。若健康因子小于阀值,已作为抵押品,且两种债务都不为 0 则验证通过。接着计算用户的最大可清算债务,并确定实际需要清算的债务数量。如果清算的债务超过用户的可用抵押物,将调整清算金额。
function _calculateAvailableCollateralToLiquidate(
DataTypes.ReserveData storage collateralReserve,
DataTypes.ReserveData storage debtReserve,
address collateralAsset,
address debtAsset,
uint256 debtToCover,
uint256 userCollateralBalance
) internal view returns (uint256, uint256) {
...
//清算人根据要清算的债务金额 (debtToCover) 和抵押品的价格,计算出的理论最大可以清算的抵押品数量
vars.maxAmountCollateralToLiquidate = vars
.debtAssetPrice
.mul(debtToCover)
.mul(10**vars.collateralDecimals)
.percentMul(vars.liquidationBonus)
.div(vars.collateralPrice.mul(10**vars.debtAssetDecimals));
//判断实际清算的抵押品数量,true 清算用户的全部抵押品,false 完全清算
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);
}
如果清算人选择接收被清算人抵押的底层资产,需要确保抵押物储备中有足够的流动性。更新债务储备的状态,并根据清算人是否接收 aToken 情况,燃烧相应数量的可变和稳定债务代币。更新债务的利率,反映清算后的市场情况。清算人奖励如果选择接收 aToken,清算人将获得相应数量的 aToken。如果不接受 aToken,则更新其抵押状态和抵押物的利率,从用户账户中燃烧掉对应数量的 aToken ,将底层资产转移给清算人。最后,将清算所需的债务资产从清算人转移到相应的储备 aToken 中,完成清算过程。
与Compound相比,AAVE V2的清算过程有以下主要区别:
支持多种抵押品和债务资产的组合清算。
允许清算人选择接收 aToken 或 底层资产。
清算过程更加模块化,将验证逻辑、计算逻辑等分离到不同的函数中。
支持稳定利率和可变利率两种债务类型的清算。
用户通过 lendingpool 的 flashLoan 函数进行闪电贷。作为借贷协议的闪电贷,可以允许当前闪电贷立刻还款或是作为债务来后续还款,其中以传入的 modes 参数不同而决定。0 为立刻还款,1 为作为稳定型债务,2 为浮动型债务。 函数首先通过 ValidationLogic.validateFlashloan 检查输入参数匹配,计算闪电贷所需的溢价成本,并直接将所需 aToken 转给接收者地址。调用接受者的 executeOperation 操作实现预设的闪电贷。AAVE 实现的闪电贷操作已包括了兑换,兑换清算,以及兑换偿还操作。再 executeOperation 以上操作完成后,记录需偿还闪电贷金额和相应的费用。如果用户选择以非债务模式归还资金:系统更新储备状态,累积储备流动性以及更新流动性指数。最后再从请求者转移资金和费用至储备池。 若用户选择以债务模式处理,则调用 _executeBorrow,开启相应的债务头寸。
在 AAVE v2 中,用户可以通过 swapBorrowRateMode 函数在稳定利率模式和浮动利率模式之间切换。首先通过 getUserCurrentDebt 函数获取用户在目标资产上的当前稳定利率债务和浮动利率债务,确定用户的债务状况。接着调用 ValidationLogic.validateSwapRateMode 函数验证切换操作是否合法。其中检查用户是否有足够的稳定或浮动债务以支持模式切换,确保切换目标模式符合资产的配置和用户的债务情况。调用 reserve.updateState 更新资产储备的状态,确保储备数据最新。随后就是对于两种债务代币的相互转换,燃烧稳定债务代币铸造浮动债务代币或是燃烧浮动债务代币铸造稳定债务代币。转换完成后 reserve.updateInterestRates 更新目标资产利率,确保反映当前市场状态和用户债务的变化。
在 AAVE 和 Compound 中,都存在空市场中精度损失而造成的漏洞问题。如果在一个空市场的情况(即没有用户在市场中进行借贷),由于 cumulateToLiquidityIndex 函数中 liquidityIndex 的值依赖于合约对应的底层资产代币的数量,所以可以通过闪电贷向合约存入大量的底层资产代币来操纵 aToken 的价格。 与之前 Compound fork 项目 Hundred Finance 第一次被黑相似,在 HopeLend 事件中攻击者先操纵(liquidityIndex)将 hETHWBTC 兑 WBTC 的价值控制为 1:1 后,通过兑换底层资产以及借贷的方式又提高 liquidityIndex 的值。随后又通过循环的调用闪电贷不断调用 _handleFlashLoanRepayment 函数。
在 cumulateToLiquidityIndex 函数中 rayDiv 的精度损失会再次放大 reserve.liquidityIndex 的数值,最终放大了能兑换出的 WBTC。 (攻击交易:[https://etherscan.io/tx/0x1a7ee0a7efc70ed7429edef069a1dd001fbff378748d91f17ab1876dc6d10392])
审计要点:在审计时,需要关注兑换率的计算方式是否容易被操控以及舍入的方式是否恰当,同时可以建议项目团队在新的市场创建后立刻铸造 aToken,以防止市场为空进而被操控。
与之前 Compound fork 项目 Hundred Finance 第二次被黑相同,在 Agave 攻击事件中,攻击者在没有任何负债的情况下调用了 liquidateCall 函数来清算自己。而清算的代币是 Gnosis Chain 链上使用的 ERC-677 标准代币, 该类代币转账时会外部调用接收地址的函数, 所以是得清算合约调用了攻击合约,攻击合约在此过程中存入了 2728 个通过闪电贷获取的 WETH,铸造出 2728 aWETH。并以此为抵押,借出了 Agave 项目中所有可用资产。外部调用结束后,liquidationCall 函数直接清算了攻击者之前存入的 2728 aWETH,并将其转给清算者。
function transfer(address _to, uint256 _value) public returns (bool) {
require(superTransfer(_to, _value));
callAfterTransfer(msg.sender, _to, _value);
return true;
}
function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
require(super.transferFrom(_from, _to, _value));
callAfterTransfer(_from, _to, _value);
return true;
}
function callAfterTransfer(address _from, address _to, uint256 _value) internal {
if (AddressUtils.isContract(_to) && !contractFallback(_from, _to, _value, new bytes(0))) {
require(!isBridge(_to));
emit ContractFallbackCallFailed(_from, _to, _value);
}
}
function contractFallback(address _from, address _to, uint256 _value, bytes _data) private returns (bool) {
return _to.call(abi.encodeWithSelector(ON_TOKEN_TRANSFER, _from, _value, _data));
}
(来源:[https://x.com/danielvf/status/1503756428212936710])
(攻击交易:[https://gnosis.blockscout.com/tx/0xa262141abcf7c127b88b4042aee8bf601f4f3372c9471dbd75cb54e76524f18e])
审计要点:在审计中,需要关注借贷功能的相关代码是否符合 CEI(Checks-Effects-Interactions) 规范或者是否存在防重入锁,并且需要考虑具有回调功能的代币造成的影响。
在 Blizz Finance 项目被黑事件中,由于当时 LUNA 的价格持续暴跌,协议使用的 Chainlink 价格信息变得不准确,导致可以用价格高昂的 LUNA 抵押品借入资金。同时项目没有现有的故障安全机制,尽管看起来已经提前发出警报,但并没有及时建立预防措施来防止损失。在当价格跌破该水平时,使得任何人都可以按市场价格购买(远低于 0.10 美元的价格)大量 LUNA,并将其作为抵押品(价值 0.10 美元)从平台借出资金。
function _executeBorrow(ExecuteBorrowParams memory vars) internal {
...
uint256 amountInETH =
IPriceOracleGetter(oracle).getAssetPrice(vars.asset).mul(vars.amount).div(
10**reserve.configuration.getDecimals()
);
ValidationLogic.validateBorrow(
vars.asset,
reserve,
vars.onBehalfOf,
vars.amount,
amountInETH,
vars.interestRateMode,
_maxStableRateBorrowSizePercent,
_reserves,
userConfig,
_reservesList,
_reservesCount,
oracle
);
...
}
function validateBorrow(
...
) external view {
...
(
vars.userCollateralBalanceETH,
vars.userBorrowBalanceETH,
vars.currentLtv,
vars.currentLiquidationThreshold,
vars.healthFactor
) = GenericLogic.calculateUserAccountData(
userAddress,
reservesData,
userConfig,
reserves,
reservesCount,
oracle
);
require(vars.userCollateralBalanceETH > 0, Errors.VL_COLLATERAL_BALANCE_IS_0);
require(
vars.healthFactor > GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD,
Errors.VL_HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD
);
vars.amountOfCollateralNeededETH = vars.userBorrowBalanceETH.add(amountInETH).percentDiv(
vars.currentLtv
); //LTV is calculated in percentage
require(
vars.amountOfCollateralNeededETH <= vars.userCollateralBalanceETH,
Errors.VL_COLLATERAL_CANNOT_COVER_NEW_BORROW
);
...
}
(参考来源:https://x.com/BlizzFinance/status/1524911400992243761)
审计要点:在审计时,需要关注计算抵押品价值时采用的预言机喂价机制是否容易被外部操控,可以建议项目方采用多种价格来源进行综合评估,以规避单一价格来源造成的风险。同时也需要注意项目是否存在合理的暂停机制,来预防此类突发情况。
在 AAVE 协议与 Para Swap 协议的交互中,Aave Collateral Repay Adapter V3 合约的 _buyOnParaSwap 函数存在多个安全隐患。该函数通过调用 safeApprove 方法,在 tokenTransferProxy 上设置 assetToSwapFrom 的限额为 maxAmountToSwap,但未考虑未进行兑换或部分兑换的情况,导致在未完全使用限额的情况下,存在剩余额度保持不变。此外,函数依赖外部合约调用 (augustus.call(buyCalldata)) 执行兑换,并且未对 paraswapData 传参进行充分验证与限制。从而允许攻击者通过恶意构造的 paraswapData 操控解码后的 buyCalldata 和 augustus 合约地址,绕过预期的兑换逻辑或完全避免兑换。由于该函数在兑换后未减少或检查 assetToSwapFrom 的代币限额,即使兑换失败或被绕过,攻击者仍可利用未变化的高额限额提取合约中的代币,从而实现未经授权的资金转移。出现因缺乏对输入数据和交换结果的验证,以及未能有效管理代币限额,而导致合约受攻击者利用并转移资金的漏洞利用。
(攻击交易:[https://etherscan.io/tx/0xc27c3ec61c61309c9af35af062a834e0d6914f9352113617400577c0f2b0e9de])
审计要点: 在审计时,需特别关注与外部第三方协议的交互代码。重点评估外部合约的输入和输出是否经过严格限制,交互逻辑是否对协议核心模型或资金安全产生潜在影响,输入数据是否经过清理和验证,防止恶意数据引发安全问题。通过严格审查外部交互的代码逻辑以及数据验证机制,可有效降低此类漏洞风险。
在 Polygon 链上,AAVE 部署过程中,由于 InterestRateStrategy 设置不兼容的问题导致功能异常,错误地为 WETH 设置了不兼容的利率策略。 错误设置的 InterestRateStrategy 合约中的 interface 如下
function calculateInterestRates(
address reserve,
uint256 utilizationRate,
uint256 totalStableDebt,
uint256 totalVariableDebt,
uint256 averageStableBorrowRate,
uint256 reserveFactor
)external view returns (…);
而 AAVE v2 的 LendingPool 实现的代码如下
function calculateInterestRates(
address reserve,
address aToken,
uint256 liquidityAdded,
uint256 liquidityTaken,
uint256 totalStableDebt,
uint256 totalVariableDebt,
uint256 averageStableBorrowRate,
uint256 reserveFactor
)external view returns (…);
(来源:[https://x.com/mookim_eth/status/1659589328727859205])
由于接口不兼容,新 InterestRateStrategy 无法正常被 LendingPool 调用,直接导致 AAVE v2 的 WETH 池功能中断,用户无法存入或提取 ETH。
审计要点: 在审计时,需确保代码(或 fork)中关键组件的接口完全兼容。同时,尽管以上问题并不是由多链特性导致的原因,但是审计时仍需要注意不同链的特性下是否会造成非预期的结果。
AAVE 的存款和取款会通过 setUsingAsCollateral 函数设置 usingAsCollateral 来实现以便灵活的抵押策略管理。当外部协议或合约通过 AAVE 借贷函数第一次借入资金时,借贷函数会将 usingAsCollateral 设置为 true。 而当外部协议或合约从 AAVE 完全提取资金时,AAVE 中协议处理程序的 usingAsCollateral 状态将被设置为 false。但实际上,AAVE 在计算取款需要烧掉的 aToken 数量时,此时如果由于算术精度误差,协议处理程序中可能还有极少的 aToken 剩余。因此,当协议处理程序下次向 AAVE 存款时,usingAsCollateral 将不会变动,依然设置为 true,由于协议处理程序合约中没有调用 setUserUseReserveAsCollateral 函数的接口,这可能导致协议处理程序无法再执行借款操作。
function deposit(
address asset,
uint256 amount,
address onBehalfOf,
uint16 referralCode
) external override whenNotPaused {
...
if (isFirstDeposit) {
_usersConfig[onBehalfOf].setUsingAsCollateral(reserve.id, true);
emit ReserveUsedAsCollateralEnabled(asset, onBehalfOf);
}
...
}
function withdraw(
address asset,
uint256 amount,
address to
) external override whenNotPaused returns (uint256) {
...
if (amountToWithdraw == userBalance) {
_usersConfig[msg.sender].setUsingAsCollateral(reserve.id, false);
emit ReserveUsedAsCollateralDisabled(asset, msg.sender);
}
...
}
function setUserUseReserveAsCollateral(address asset, bool useAsCollateral)
external
override
whenNotPaused
{
...
_usersConfig[msg.sender].setUsingAsCollateral(reserve.id, useAsCollateral);
...
}
审计要点: 在审计时,需要对所调用的协议有充分的熟悉度,充分了解其特性的情况下,判断是否对于其与外部协议交互存在一定的兼容性问题,如代币兼容性、调用实现逻辑兼容性等。
https://github.com/aave/protocol-v2
https://github.com/YAcademy-Residents/defi-fork-bugs
https://blog.solidityscan.com/aave-repay-adapter-hack-analysis-aafd234e15b9
https://gist.github.com/mookim-eth/72f185019e8c4df3a1edba637067f734