LOADING...
LOADING...
LOADING...
当前位置: 玩币族首页 > 行情分析 > Aave协议审计

Aave协议审计

2020-06-10 DeFi传教士 来源:区块链网络


Aave团队邀请我们检查并审计一份协议的预发布版本。我们调查了代码,现在发布我们的结果。
被审计的提交版本是1f8e5e65a99a887a5a13ad9af**86ebf93f57d02,所有在aave-tech/dlp/contracts/contracts目录的Solidity都已包含。值得注意的是审计报告中所有项目代码链接都是指向私有仓库,因此只有Aave的开发团队有权限访问。
在转到项目中已发现问题的完整列表前,对项目当前状态的一些介绍性说明是适当的。

项目状态

根据Aave协议,Aave开始基于“池策略”实现一个链上去中心化借贷平台(替代点对点借贷)。出借人通过向池中充值加密币成为流动提供者,这个池就是借贷人在放入足够的抵押品后可以从中借出贷款。利率是完全链上计算,从可用池的状态算法式导出的。协议的新奇功能包含了变化和稳定利率的借贷(能在它们中进行切换),以及闪电贷-一种在同一个交易中借出和偿还的类型。
Aave团队意识到审计代码的审计版本正在努力制作,没有准备好生产。鉴于项目的成熟度,第一轮的安全审计应该作为第一步,以达到处理大量金融资产的系统所要求的最高水平的代码质量和健壮性。我们认为在项目文档,测试用例,代码本身有许多提升的机会,这些已通过报告强调。他们不仅需要在几个代码片段指定补丁,也需要在测试和文档中付出努力。尽管这些问题可以被视为构建一个可持续的复杂金融体系固有的困难症状,但绝不能掉以轻心。对整个协议的进一步安全审查已准备就绪,这将与我们在本报告中的建议一起,有助于使项目进入就绪生产状态。

特权角色

Aave团队当前管理着协议的所有方面去决定哪些资产可以被借贷,如何喂价和获取市场利率。他们也控制着大量的经济参数,例如用于鼓励第三方清算担保不足贷款的激励规模。所有者/特权角色的所有敏感*作可即时强制的执行(以下列出的是相当敏感的),对于协议用户来说没有选择加入或退出机制。
LendingPoolParametersProvider合约的所有者可以:
* 更新最大固定利率借贷规模。
* 更新当前固定利率和用户固定利率之间的差值,在这个差值上任何借贷持仓都可以重新平衡。
FeeProvider合约的所有者可以:
* 更新所有借贷的收取的手续费(叫做“贷款手续费”)。
* 更新接收所有手续费的地址。
LendingPoolAddressesProvider(如果函数被标为onlyOwner,查看问题[H01]缺乏访问控制)合约的所有者可以:
* 更新LendingPool, DefaultReserveInterestRateStrategy, LendingPoolCore, LendingPoolConfigurator, LendingPoolLiquidationManager, LendingPoolDataProvider, LendingPoolParametersProvider, FeeProvider 以及 NetworkMetadataProvider合约的地址。换句话说,所有者被允许修改整个协议的逻辑。
* 更新出借利率和价格预言机的地址。
* 更新"出借池管理员"角色的地址。
设置在LendingPoolAddressesProvider合约的"出借池管理员"角色可以(无视协议的当前状态):
* 在任意储备中开启/关闭借贷。
* 开启/关闭储备作为抵押品的使用。
* 在任意储备中开启/关闭固定利率借贷。
* 激活/禁用储备。
* 更新储备的清算阈值,清算奖励,以及利率策略(如如何计算利率)。
这些决策和参数可以显著地影响到系统的可用性和安全。尽管Aave团队计划授权这些权限给一个未来可以看管协议的治理系统,但目前在审计时还不清楚单外部权属帐户还是多签将代表这些角色。至于现在,在治理系统引入前需要用户完全信任拥有特权角色的Aave团队。

预言机

Aave协议依赖两种不同预言机投喂利率和价格给系统。
首先是出借利率预言机(Lending Rate Oracle),这个提供其他借贷平台的实际市场利率的信息。这个预言机基于每个外部平台出借利率和借贷容量来计算平均的市场出借利率。与出借利率预言机的交互可以在DefaultReserveInterestRateStrategy合约的calculateInterestRates函数看到。
第二种是价格预言机(Price Oracle),当查询时提供一个资产的价格(以ETH形式)。与这个预言机的交互可以在以下场景看到:
* 借贷
* 跨储备计算用户数据
* 计算可被用于清算的可用抵押品
* 确定是否允许降低用户的储备余额
我们必须强调介绍的预言机都没有包含在本次审计范围内,因此它们被假设是无妥协的,可靠的,一直是可用的。关于信任的去中心化的预言机如何影响借贷平台的进一步参考,请阅读samczsun的一篇精辟的文章。

接下来,我们呈现所有发现列表。

审计更新

Aave团队根据我们的建议进行了几个修复。我们放入以下的修复并引作为首次审计的一部分。注意一些初始报告的问题已经确定无问题,但仍保存完整。写在审计修复后的一些最后评论:
* 我们仅审计了报告问题指定的补丁。基础代码经历的不相关改动我们未审计,可以在未来一轮审计中深度审查。
* 我们总是给予建议给所有审计的项目,开发团队应该努力在他们的软件开发生命周期中使用持续集成测试和强制同行评审的实践来保持高标准。这极大地提高了项目的整体质量,并防止了回归错误,如“[M11]在ETH中对请求借款金额的错误计算”中所报告的错误。
* 在代码开源和项目启动前,我们支持Aave团队在项目的主要README添加一个“安全”区,包括对项目中发现的任何安全漏洞负责任的披露说明。

严重问题

[C01]用户可以盗取资金

无论何时用户充值资产进一个指定的储备,除了赚取利息,他们可以选择这些资产是否可以作为未来贷款的抵押品。这决策存储在UserReserveData对象的一个标志中,处理特定帐户和储备合约之间的关联。然而,如果用户已经充值了相同的储备,已存在的标志简单的被覆盖。这可以被恶意用户利用来解锁和提取任何抵押品,即使是为了获得贷款。
已识别的攻击途径如下:
1.用户充值资产到储备设置_useAsCollateral标志为true,在交换中收到aTokens。
2.他们使用初始的充值作为抵押,从不同储备借出资产。这个阶段,他们没有转账他们的aTokens,因为balanceDecreaseAllowed函数将返回false,导致isTransferAllowed检查失败。
3.下一步,他们充值任意数量资产(甚至0)到初始储备设置_useAsCollateral标志为false。
4.现在用户可以成功转账他们的aTokens到新账户,因为balanceDecreaseAllowed函数将绕过偿付能力检查并返回true。
5.他们现在可以赎回(或卖掉)aTokens收到原始的抵押品。这时他们已经拿到了一笔无抵押的借贷,有效从出借协议盗取到的。
当用户从现有的储备存款要求获得一笔未偿贷款时可以考虑阻止用户设置_useAsCollateral标志为false。一旦修复实施,需要相关的单元测试,以避免未来修改基础代码时重新引入这个严重问题。
更新:已在MR#38修复。用户不再选择充值是否作为抵押品。取而代之的是当用户储备中的余额为零时,deposit函数当前默认标记下一次对储备的充值为抵押品。用户可以通过调用LendingPool合约的setUserUseReserveAsCollateral函数选择退出,这将进行适当的偿付能力检查。我们建议进行更好地文档说明这个场景以避免无法预期的行为。另外,在调用LendingPoolCore合约的setUserUseReserveAsCollateral函数后ReserveUsedAsCollateralEnabled事件应该在deposit函数发出。

[C02]借贷人可以避开清算

当一笔借贷被清算,剩余资本降低和总计借贷(固定或变化)相应的降低是通过从要偿还的金额中减去应计利息来计算的。
从概念上讲,这是两个独立*作的组合:在构建新贷款时,应计利息加到本金上,然后减去已偿还的金额.然而,无论何时还款少于应计利息,合并它们到一个*作将导致交易回滚。
除了阻止有效的还款,借款人还可以利用这种行为来阻止清算。
因为每一笔清算交易的规模都受到可从指定储备中提取的抵押品数量的限制,借贷人可以在多个不同资产铺开它们的抵押,为了确保最大清算量低于它们的应计利息。
作为选择,清算数量也受协议的关闭因子限制,这意味着用户可以简单地让它们的借贷增加直到利息超过这个阙值。实际上,这需要数年时间发生。
不论发生何种情况,当借款人不能被清算时,他们就没有任何动力继续保持抵押。
可以考虑更新资本和总计借贷成两个独立的步骤,分别计算应计利息及偿还贷款。
更新:已在MR#56和MR#58修复。资本和总计借贷已更新成两个步骤。

[C03]没有标为抵押品的充值仍然可以被清算

当用户充值资产到出借池,他们可以通过_useAsCollateral标记的方法选择是否将资产的功能作为抵押品。虽然预期的行为是只有标记为抵押品的资产可以被清算,但这一限制没有得到执行。
这个问题是由于liquidationCall函数的逻辑缺陷。这个函数要求储备被启用为抵押品,并且用户标记储备为抵押品。然而,92和93行的条件语句是错误的。结果,假设其他清算要求成立,那么以下其中一个成立liquidationCall就会成功:
* 储备的_collateral是启用的作为抵押品(无视用户的选择)。
* 储备的_collateral是不启用为抵押品并且用户没有标记它为抵押品。
可以考虑修改92和93行的条件语句为 core.isReserveUsageAsCollateralEnabled(_collateral) && core.isUserUseReserveAsCollateralEnabled(_collateral, _user);然后,执行彻底的相关单元测试是非常适当的。
更新:已在MR#59修复。特定的单元测试覆盖这个场景仍是缺失的。

[C04]恶意借贷人可以*控其他账号的借贷余额

LendingPool合约的rebalanceFixedBorrowRate函数意图是让任何人当确定的要求达到时重平衡借贷人的固定利率。所有的调用者需要指定储备(_reserver参数)和借贷人的账号(_user参数)来重平衡。
当查询借贷余额时,函数调用了getUserBorrowBalances函数错误地传递msg.sender作为参数。因此,compoundedBalance 和 balanceIncrease本地变量将控制调用者的借贷余额,而不是那些需要重平衡的账户(如_user的地址)。从那时起,balanceIncrease用于更新储备数据。需要注意的是 increaseUserPrincipalBorrowBalance函数的514行通过balanceIncrease增加了_user的借贷余额。
任何compoundedBalance大于0的恶意借贷人可利用这个严重安全隐患去*控另一个借贷人的借贷余额。攻击者控制大量借贷可以使用受害人的地址作为_user参数调用rebalanceFixedBorrowRate,因此增加了受害人资本的借贷余额。
攻击者可以进一步获利,特别是针对接近被清算的账户。他们借贷余额的增加将有效推动受害人至可清算的仓位,让攻击者清算他们。为了防止其他清算人的抢先*作,这种攻击可以通过一个恶意合约在单个原子交易中执行,该恶意合约首先增加受害者的借贷余额,然后对其进行清算。
第二种攻击途径让恶意借贷人歪曲应计利息。他们可以调用rebalanceFixedBorrowRate函数在_user参数传入他们的账号,这个账号有很小的借贷。这将有效地更新他们自己的lastUpdateTimestamp没有计入最近的利息和实际一样多。
为阻止恶意的借贷者利用这个漏洞,可以考虑用msg.sender替换_user作为参数传递给getUserBorrowBalances函数。请注意这个问题密切联系高风险问题“[H09]固定利率借贷能被重复重平衡”和“[H06]再平衡另一个账户的固定借款利率是不可能的”。
更新:已在MR#61修复。

[C05]ETH的借贷无法被偿还

任意用户从Aave协议拿出一笔借贷可以通过调用LendingPool合约的repay函数去偿还。然而,这个函数的缺陷让它无法偿还ETH借贷。
在偿还ETH借贷的过程中调用者预期是在交易中发生ETH。ETH应该被分成两份,首先支付贷款的手续费,然后付完借贷。这些*作在375和412行执行。两个转账发送到LendingPoolCore合约,在第一次转账中转手续费至“收费地址”。尽管LendingPoolCore合约是两种场景的目的地,它预期在第二次转账中收到还款部分。结果,LendingPool合约不够ETH完成第二次转账,将导致整个交易回滚。
可以考虑既在transferToFeeCollectionAddres函数返回超额的ETH给LendingPool合约,也可以确认ETH是否在repay函数一起发送,并在两次转账中转发准确的数额。
更新:已在MR#48修复。然而,注意引入了错误的内联注释说明“如果转账是ETH发送总计的msg.value”。在本例中,当函数执行ETH转账时,只有数量msg.value.sub(vars.originationFee)被转移。

高风险

[H01]缺乏访问控制

LendingPoolAddressesProvider和NetworkMetadataProviderhe合约包含了几个设置函数让调用者变更协议基础合约的地址和修改整个系统的参数。这些函数的主要目的是让协议的逻辑升级。然而,当前这些函数没有实现了任何访问控制机制,因此允许任何人去执行它们。受影响的函数有以下:
LendingPoolAddressesProvider合约中:setLendingPool, setInterestRateStrategy, setLendingPoolCore, setLendingPoolConfigurator, setLendingPoolManager, setLendingPoolDataProvider, setNetworkMetadataProvider, setLendingPoolParametersProvider, setPriceOracle, setLendingRateOracle, setFeeProvider 和 setLendingPoolLiquidationManager.
NetworkMetadataProvid合约中:setBlocksPerYear 和 setEthereumAddress。
Aave团队承认缺乏针对这些极敏感功能的访问控制(行内注释写着//TODO: add access control rules under DAO),并着手建立一个治理系统,这个系统是唯一允许的*作者去触发它们。我们必须强调这样一个治理系统当前未实现,并完全超出这次审计的范围。
不管所选择的访问控制机制性质如何,必须强调的是系统没有必要的限制(和相关的单元测试)就不能放到生产环境。
更新:通过增加缺失的onlyOwner修饰符已在MR#52修复。据Aave团队称:
“这些合约所有权将在部署到相应的治理基础设施时分配”。

[H02]任何人可以禁用闪电贷功能

Aave协议让借贷人借出一种特殊类型的贷款,它必须在一个相同的交易中偿还(包含手续费)。这个功能在LendingPool合约的flashLoan函数中实现。在有效发出贷款前的预先条件检查中,函数在require语句中验证可用的流动性是否与LendingPoolCore合约的实际余额相匹配。从上面检查的注释可以看出,这是“为了增加安全性”而做的。然而,这个require语句允许任何人永久禁用整个闪电贷机制。
恶意用户要想有效地实施攻击,只需要向LendingPoolCore合约发送少量资产。因此,没有增加协议流动性时合约的余额将会增加。不匹配的情况将禁用储备的闪电贷功能,因为624行的require语句注定失败。需要注意的是,禁用ETH储备的闪电贷功能,攻击者必须从合约发送ETH。
因LendingPool.sol624行的校验没强制确保flashLoan函数的正确行为,可以考虑删除它。
更新:已在MR#53修复。

[H03]停用的抵押品储备可被用于清算

Aave协议中所有资产储备可被高权限账号停用。当一个储备被停用,没有*作可以执行它。这个验证是通过onlyActiveReserve修饰符实现的。
LendingPool合约的liquidationCall函数使用这个修饰符,但只验证_reserve参数指定的储备是活跃的。然而,对于在_collateral参数中传递的储备地址,没有类似的验证。因此,清算可以在被停用且不会变动的抵押品储备执行。此外,由于LiquidationCall事件没有记录哪些抵押品储备受到清算的影响,这一问题变得更加严重,从而阻碍链下用户有效追踪停用抵押品储备变动的任务。
可以考虑通过liquidationCall函数阻止对停用抵押品储备的修改。这可以通过验证_collateral参数传递的地址对应活跃储备来实现。
更新:已在MR#**修复。

[H04]当有足够流动性时清算者无法回收标的资产

当调用liquidationCall函数清算借贷仓位时,清算者可以选择通过收回抵押或标的资产来获得抵押品。这是由函数的_receiveAToken布尔参数指示的,其中false表示调用方愿意接收标的资产。在这个场景,协议首先尝试验证抵押品储备中是否有足够的流动性,这需要比较可用的流动性和要清算抵押品的最大数量来获得。
然而,由于比较是反向的,当有足够流动性处理清算时函数返回错误。尤其是当currentAvailableCollateral >= maxCollateralToLiquidate,返回一个LiquidationErrors.NOT_ENOUGH_LIQUIDITY错误。
虽然这个问题本身并不构成安全风险,错误的比较破坏了整个功能,阻止了清算者清算仓位在交换中获得标的资产。应该注意的是,这个问题可以通过更彻底的单元测试来避免,但是目前只包括_receiveAToken为true的测试(查看LiquidationCall.spec.js)。
可以考虑反转提到的比较,因此当currentAvailableCollateral < maxCollateralToLiquidate时liquidationCall返回错误。一旦修复实施,高度建议加入相关的单元测试确保运转符合预期。此外,新的测试可防止未来基础代码修改中重新引入这个问题。
更新:已在MR#73修复。

[H05]固定利率贷款的最大规模可以被绕过

LendingPool协议的borrow函数尝试设置对固定利率贷款规模的硬性限制。这一限制取决于准备的可用流动性,只适用于固定利率贷款。一个让任何借贷人绕过限制的攻击途径已被确认。首先,借贷人在可变利率借出一笔任意大的贷款。然后,借贷人在同个储备借出第二笔贷款,甚至是零数量的——但这次设置利率模式为固定的。借贷人在固定利率上有效地获得了任意大的贷款。注意这个*作可以通过智能合约的一个交易完成。为阻止这个特殊的问题,可以考虑收紧由可变利率转固定利率贷款的限制。尽管如此,我们认为不应该孤立地分析这个问题。所描述的攻击途径的简单性源于Aave协议在设计级别上的更大缺陷,以及它如何处理固定和可变利率贷款。参考“[N03]固定利率贷款的特征是松散封装”了解更多细节。
更新:不是个问题。用Aave的话说:
“对稳定利率贷款实施最大规模限制的目的是避免借款人以过于具有竞争力的利率获取流动性。事实上,a)直接借出稳定利率的贷款,b)借出可变利率的贷款,再转成稳定的,这两种场景下是有根本性的区别。区别就在于利率。事实在a)场景,没有贷款规模的硬性限制,借贷人理论上可以在最有竞争力利率下获得整个流动性,因为稳定利率只有在贷款被借出后才增加。在b)场景,换句话说,可变利率贷款可以是任意大的,因为它不涉及利率问题(可变利率随着借贷人借出流动性越多而增加)。也在b)场景,当用户变动借出,它会导致稳定利率同样增长,特别是个数量相当的规模。因此,在这种情况下利率互换不会带来任何问题,因为借款人将互换为最新的,增加的稳定利率,而不是如果他直接以稳定利率借款就可以受益的较低的稳定利率。”

[H06]不能再平衡另一账户的固定借款利率

LendingPool协议的rebalanceFixedBorrowRate函数中,调用core.updateUserFixedBorrowRate执行中传递msg.sender地址作为参数。因此,成功的再平衡调用仅永远更新调用者的固定借出利率,而不是_user参数定义的目标地址。这打破了能够再平衡其他账户的初衷。
可以考虑修改用在537行的msg.sender地址为_user。还必须考虑到的有关问题有“[C04]恶意借贷人可以*控其他账号的借贷余额”和“[H09]固定利率贷款可以重复再平衡”
更新:已在MR#61修复。

[H07]用户不能从不再包含他们抵押品的储备进行固定利率借贷

为防止滥用协议,仅当借款金额大于抵押品时,才允许以固定利率(来自借贷人充值抵押品相同的储备)借款。这个限制的实现是在LendingPool.sol第216至221行的borrow函数实现的。
然而,这一限制目前不允许用户以固定利率从他们此前拥有抵押品储备中借款(现在已不是了)。在用户从储备提取出所有抵押品后,系统不会自动切换isUserUseReserveAsCollateralEnabled标志为false。当用户尝试从此储备借出时,220行的条件会报错,因此回滚交易。结果,即使用户目前没有持有任何抵押品,也无法从该储备中借款。
可以考虑一旦用户从储备提现所有抵押品时程序化切换useAsCollateral标志为false。
更新:在MR#74实施了一个补丁后,Aave团队正确地指出我们误解了函数的行为,这不是个问题。修复程序仍保留着,开发团队将包含相关的测试用例,以编程的方式确认这确实不是一个问题。

[H08]适得其反的激励

Aave协议要求所有贷款都必须有其他资产池超过100%的抵押品作为担保。为在资产价格变动时保持这种状态,有种安全机制让任何人在有破产风险时偿还贷款。通常情况,这鼓励套利者提升贷款的健康性但在某些情况下,安全机制的行为会适得其反,它会接受有风险的贷款,并积极地将它们推入资不抵债的境地。
可以考虑借贷人在不同的市场有价值C的抵押品和价值D的债务。在下图中,它们位于点(D, C):

在考虑了抵押品分布和与每个储备相关阙值后,它们被赋予了一个清算阙值。有了这些定义,我们可以描述重要的区域:
* 在绿色区域用户超过了清算阙值是完全抵押的。他们的抵押品在可接受的安全范围超过了他们的债务。
* 在红色区域用户是资不抵债的。他们的债务大于他们的抵押品,他们没有任何激励去偿还他们。这个区域的用户成为系统的主要问题,因为它们在不断积累利息的同时也从储备中移走流动性,这就确认到至少有一些aToken持有者将无法赎回他们的资产。
* 在紫色区域的用户是低于抵押的。他们在系统中有更多资产超过债务,但协议认为他们有破产风险。
当借贷人低于抵押,任何人都可以以高于市场的利率偿还部分债务作为抵押。这个机制由两个全局值参数化:
* 清算封闭因子百分比:可以清偿的原始贷款的最大比例(当前是50%)。
* 清算奖励L:套利者接收到多少抵押品(作为支付金额的乘数)。
图中的清算线有个斜率L,通过原点。当套利者偿还x债务,他们从借贷人获得x*L的抵押品。换句话说,它们迫使借款人沿着与清算线平行的轨迹走到图表左下方。关键的观察结果是,这一过程永远不会导致借贷人越过清算线。清算线以上的借贷人将被清算,直到他们得到完全抵押,但低于这一清算线的借贷人实际上会被清算程序进一步推向资不抵债的境地。
每当清算事件后,剩余债务超过剩余抵押品时,用户还是资不抵债的。如果我们用Cf表示封闭因子:
最大的清算数量为Cf*D,剩余债务为(1-Cf)*D。
清算者收到的抵押品(包括奖励)价值将为Cf * D * L,剩余的抵押品变成C - Cf * L * D。
假如剩余的债务大于剩余的抵押品还是变成资不抵债的。当( 1 - Cf ) * D > C - Cf * L * D或者D / C > 1 / ( 1 + Cf * (L - 1))重排时就发生了。如果我们假设有1.05的清算奖励和0.5的封闭因子,然后当借贷人的债务上升至少到1 / 1.05 = 95.2% 的抵押品价值,清算开始增加他们债务抵押品比率,在97.6%时有可能清算迫使他们进入资不抵债的境地。
理想上,在他们跨域阙值时已经被清算,但是这可能没发生,取决于协议外的各种因素,如价格变化的速度,预言机的活力,以太坊的拥塞以及套利者的反应能力和流动性。为了达到这个目的,对上述所有参数进行链下监控,以及自动清算机制,以尽早解决风险,这可能是有用的。
然而,如果借贷人越过清算线,协议将不再维持相同的奖励,因为每个清算事件现在给协议增加风险。这种观点有一个不幸的后果就是套利者清算高风险贷款的激励在其更为关键的时候变得更弱。即便如此,低效率的激励(或者根本没有激励)仍比适得其反的激励更可取。减少的激励可以通过增加高风险贷款的封闭因子或可能甚至让套利者清算100%贷款换取100%的剩余抵押品来进行补偿。
可以考虑调整清算机制的参数,以确保它总是将借贷人和协议推向有偿付能力方向。
更新:Aave团队承认这个问题:
“我们认为这个问题是协议动态的一部分,而且,没有简单的解决办法,因为在极端抵押不足的情况下,减少或取消清算激励将消除外部清算人执行清算的激励,这也会带来破产。未来我们计划用两个办法处理这个问题:1)通过去中心化交易所自动清算,与外部清算一起实现。2)清算风险保险基金。”

[H09]固定利率贷款被重复再平衡

如果利率掉出了可接受范围任何用户的固定利率可以再平衡。范围的最低边界是当前流动性利率,而意图中的上限边界应该是可配置百分数,高于储备固定的借出利率。
然而,上限阙值设置是低于储备固定借贷利率。这意味着新贷款开始将超出可接受范围并在再平衡后保持这种状态,这样“固定利率贷款”实际上很容易受到市场利率变化的影响。
可以考虑重定义上限阙值高于储备利率(通过可配置的下行率三角参数)而不是低于这个百分比。相关问题需要考虑的是“[H06]不能再平衡另一账户的固定借款利率”和“[C04]恶意借贷人可以*控其他账号的借贷余额”。
更新:已在MR#44修复。这个问题开始被标记为严重风险的因为它意味着固定利率贷款可以遵循可变利率。在和Aave团队讨论后,它被降级为高风险的。用Aave的话说:
“尽管逻辑缺陷暴露出不预想的行为需要进行修复,但这个问题实际没有给协议造成任何安全风险,并最终成为一个潜在的可接受的用例。”

中风险


[M01]无手续费贷款

FeeProvider合约的calculateLoanOriginationFee函数负责计算指定数额贷款的初始手续费。为了做到这点,它将给定的数额乘以originationFeePercentage——在构建期硬编码的值为0.0025 * 1e18。由于wadMul乘法在这种特殊情况下的工作方式,calculateLoanOriginationFee对于大于0的金额可以返回0。这种意外的行为让让用户借贷不需要支付初始手续费。尤其是,所有低于200的贷款不收取任何费用(查看这个简单的数学证明进行验证)。
预期的贷款总要收手续费的,可以考虑在calculateLoanOriginationFee实现必要的验证,因此当计算的手续费为0时让交易回滚。另外,可以考虑对这个行为进行清晰的文档记录以防止意想不到的结果。不管怎样,强烈建议执行与此功能相关的完整的单元测试,因为未发现calculateLoanOriginationFee函数的单元测试。
更新:已在MR#75修复。现在所有不支付手续费的贷款将被拒绝。

[M02]任何人可以为未受保护的接收人打开一个闪电贷

LendingPool合约中的flashLoan函数让任何人在Aave协议上执行闪电贷。调用者在_receiver参数指定任意实现了IFlashLoanReceiver接口合约地址。
如果接收方合约没有执行必要的验证来识别初始触发交易的是谁,攻击者可能会迫使任何IFlashLoanReceiver合约在Aave协议中打开任意的闪电贷,从而不可避免地支付相应的费用。它可能潜在地从实现了IFlashLoanReceiver接口的脆弱智能合约耗光所有的资金(ETH或代币)。必须强调的是提供的FlashLoanReceiverBase合约未包含任何安全措施,也没有警告文档来阻止这个问题。
为减少攻击面,建议修改flashLoan函数,仅让贷款的接收者可以执行它。如果代表IFlashLoanReceiver合约开启闪电贷是预期功能,可以考虑增加对用户友好的文档以提高意识,以及展示如何防御企图代表未受保护的IFlashLoanReceiver合约开启闪电贷的攻击者的样例实现。
更新:Aave团队的理解着不是Aave协议的实际安全问题,但仍需提供足够的文档给开发者们提高意识。用Aave的话说:
"我们认为这不该报告为一个协议的安全问题,因为它本身对协议实际上没有带来任何风险,而是IFlashLoanReceiver不安全实现的潜在风险。此外,已提出的解决方案是通过使用多个合约策略来大大降低了避免抢先运行的可能,这就是flashLoan方法不在函数调用者放置安全检查的原因。由于对于闪电贷接收人的开发者来说这个潜在的安全问题可能不是直接可见,我们将确保有合适的文档说明这个问题并给开发者们提供充足的代码样本。"

[M03]在还款中不正确的退款地址

repay函数允许代表其他帐户偿还贷款。在这个场景中调用者代表其他用户超额支付ETH,函数将退还超出部分的ETH到目标地址(如_onBehalfOf参数中传递的地址),而不是调用者。
无论何时超出支付的ETH,考虑要返回所有超出的ETH给实际的还款人,而不是代表其偿还贷款的账户。
更新:已在MR#76修复。所有超出的ETH现在已返回给实际的还款人(如调用者)。

[M04]借贷人不能部分偿还贷款的利息

LendingPool合约的repay函数允许借贷人偿还指定储备的贷款。然而,当前还不能让借贷人部分偿还贷款的利息。因为事实是无论何时borrowBalanceIncrease大于paybackAmountMinusFees,交易将被回滚。
如果这是函数的预期行为,可以考虑明确记录成文档串。不然考虑当borrowBalanceIncrease大于paybackAmountMinusFees时实现必要的逻辑阻止repay回滚,让借贷人部分偿还他们的利息。
更新:已在MR#77修复。

[M05]推送支付模式可能使ETH充值不能赎回

当赎回ETH充值时,LendingPoolCore协议遵循推送支付模式去返回充值的ETH。这个模式,用transfer实现的,当赎回者是智能合约时会有些明显的缺陷,这可能致使ETH充值无法赎回。尤其是在以下时赎回将无可避免的失败:
* 赎回者智能合约没有实现可支付的回退函数。
* 赎回者智能合约实现了可支付回退函数,使用了超过2300gas燃料单位。
* 赎回者智能合约实现了可支付回退函数,需要少于2300gas燃料单位,但通过代理调用增加调用的gas燃料使用超过2300。
注意接下来的伊斯坦布尔分叉更加加重这个问题,因为可支付回退函数现在没有消耗超过2300gas燃料单位,但在分叉后可有效地超过这个阙值。这是因为EIP 1884。
为阻止非预期行为和潜在资金损失,可以考虑在充值ETH进Aave协议前明确警告终端用户关于上述缺陷以提高意识。另外,注意低级调用call.value(_amount)("")可被用于转账赎回ETH而不没有2300gas燃料单位的限制。严格遵循“检查效果-交互”模式并使用OpenZeppelin的ReentrancyGuard合约,可以降低由于使用这个低级调用而产生的重入风险。
更新:已在MR#78部分修复。我们认为是“部分修复"因为实际上赎回者智能合约必须实现可支付回退函数没有文档说明。如之前建议,transferToUser, transferToFeeCollectionAddress和transferToReserve函数替换了调用,以低级调用的方式去转账。此外,对在ETH转账中硬编码一个gas燃料限制,我们持保留意见,但修复的工作按预期在做。用Aave的话说:
"作为进一步的检查,我们决定限制接收人默认函数的最大gas燃料消耗为50000,这低于LendingPool合约上任何面向用户的函数的最低成本。这个限制在未来重新修订并/或迁移到全局配置参数。"

[M06]在交换中成功的aTokens赎回可能无法支付资产

AToken合约的redeem函数允许aTokens的持有者为标的资产赎回它们。假设所有成功赎回的必要条件达成,预期的行为是每种代币赎回一定数量从池中拿出的标的资产并转账给赎回者。
然而,当要赎回的aTokens数量小于每个单位标的资产的aTokens系统交换数量时,交易不会回滚。在这个场景中,系统仍会拿走aTokens,销毁它们,转账0单位的标的资产给赎回者。因此,调用者将损失所有赎回aTokens,aTokens的总供应量在没有修改标的资产储备流动性情况下将减少。
为阻止导致aToken丢失的未预期行为,当赎回代币在交换时不够接收到标的资产的最少一个单位,可以考虑回滚交易。
更新:已在MR#79修复,当交换中的赎回代币数量不够接收到标的资产的最少一个单位时交易现在时回滚的。

[M07]在再平衡固定借贷利率后缺乏事件发送

LendingPool合约的rebalanceFixedBorrowRate函数允许任何人在特定情况下再平衡用户的固定利率。然而,在再平衡执行后函数没有发送事件。
这样一个敏感的变动对用户来说是非常重要的,为了通知客户了解它,可以考虑定义并发送一个事件。
更新:已在MR#80修复。RebalanceStableBorrowRate事件已定义,现在成功再平衡稳定利率借贷后会被发送。

[M08]难以预料的利息复利

在Aave协议中,在相关"利息累积"交易发生(固定利率和变动利率贷款的区别,已在"[N02]固定利率可能不会复利)后贷款利息是复利的。在两个交易之间,系统使用了简单的利率模型。
代码设计目的尽可能频繁地产生利息,但是这个要求将产生利息的职责扩展到了其他不相关的功能中。此外,计算利息与理论利息之间的差异大小取决于Aave协议处理的交易量,这就变得不可预料。
为提高预测性和功能封装,可以考虑使用复利公式,而不是通过重复交易来模拟它。modexp预编译可以帮助降低gas燃料费用。或者,可以考虑告知用户协议的利率只是估计,而不是准确的利率。
更新:Aave团队承认这个问题:
"我们承认这个问题,也与N02密切相关。因此,我们将在主网发布之前评估转换到复利公式的实施成本和收益。"

[M09]敏感的数学运算没有明确的文档说明

为上平台尽可能透明,Aave团队已经实现了Aave协议在其智能合约中所依赖的大部分计算。这类计算通常需要对余额、时间戳、利率、百分比、价格、小数等进行复杂的算术运算,这些运算以几个不同的单位度量。这些运算非常重要以让工作完美进行,考虑到Aave开始处理大额的贵重资产,任何错误可能导致显著的金融损失。然而,这些敏感的运算缺乏文档说明,最大的缺陷是每一项都没有明确的单位。
状态变量,参数和返回值缺乏明确单位严重妨碍审计的过程。尽管在整个代码库中尝试验证所有计算,但手工过程仍然不可靠且容易出错。映射公式到提供的白皮书也不简单,因为有许多不匹配的地方——已在[L18]白皮书问题报告。当无法直接理解每个计算中使用的单位时,无论其是否简单,都很难评估其正确性。这就是我们将这个问题列为中度风险的原因。
必须在文档记录计算和明确说明所涉术语的所有单位方面作出巨大努力。这将大大提高代码的可读性,增加平台的透明度和用户的整体体验。由于手工审计所有敏感算术*作的过程已被证明难以理解、不可靠且可能容易出错,因此对所有关键计算进行全面的单元测试是为了以编程方式确保代码的当前行为是符合预期的。
更新:部分修复。分享给我们的最新白皮书已经明显清晰了。然而,CoreLibrary.sol结构中的变量单位仍没有文档说明。

[M10]缺少测试覆盖报告

没有自动测试覆盖报告。没有此报告就不可能知道自动化测试是否有从未执行某些部分代码;因此,对于每一个更改,都必须执行一个完整的全套手动测试,以确保没有任何损坏或错误行为。在像Aave协议这样的项目中,高度测试覆盖率是非常重要的,因为大量有价值的资产被认为是需要安全处理的,而漏洞可能会导致重大的金融损失。
可以考虑增加测试覆盖报告,让其覆盖到95%的源代码。
更新:已确认,正在修复中。在我们审计期间,Aave团队正在努力为一个单独的分支建立覆盖。由于给出的工具还不成熟和不稳定,在过程中出现了一些问题,现在正在通过定制覆盖工具来解决Aave的需求。团队充分意识到高度测试覆盖率的重要性,并努力在发布之前达到95%的覆盖率。

[M11]以ETH请求借贷数量的错误计算

给读者的提示:这个问题是在我们检查第一轮审计的修复程序时发现的。引入问题的具体提交版本是8521bcd,这没有出现在审计的提交版本。必须注意的是,最终将这个提交合并到主分支的PR是由同一作者创建和合并的,没有任何同行评审或CI测试过程。在写文时我们链入问题描述的版本已在最新的master分支。
为了验证是否有足够的抵押品来支付借款,LendingPool合约的borrow函数首先计算借款数量所代表多少ETH。这个计算考虑到借款人所支付的借款手续费。然而,借出手续费仅在第一次使用计算。这意味这当以ETH请求借贷数量被计算时,vars.borrowFee变量为0.因此,以ETH计算的借贷数额将低于预期,不可避免地降低了ETH接受借入*作所需的实际抵押品数量。
可以考虑重构borrow函数首先计算借贷手续费,然后仅使用vars.borrowFee变量来进行后续*作。注意如果borrow函数和它的计算彻底测试(如我们最初所建议的),这个问题将被捕获到。

更新:已在MR#116修复。

低风险


[L01]一个区块内的最低利息

为了减少可能的无息贷款的影响(参考我们Compound审计中的第一个高严重性问题),Aave协议增加了1 wei作为“象征性应计利息”。然而,该机制不检查时间是否已经过去,因此它将在与贷款相同的区块中累积象征利息。
在添加1 wei的象征应计利息前,请考虑添加一个检查,以确保最后一次更新的时间戳与当前块时间戳不同。
更新:已在MR#81修复。在计入象征利息前getCompoundedBorrowBalance函数现在已检查区块的时间戳。

[L02]转换Ray到Wad时的截取

当转换输入的Ray值(精度为27位小数)成Wad值(只有18位精度)时WadRayMath库的rayToWad函数会进行截取。但是,在丢弃精度时库中的其他函数进行四舍五入而不会截取。
可以考虑创建第二个函数四舍五入到最近的Wad。这可以在CoreLibrary的getCompoundedBorrowBalance函数中使用,以返回稍微更精确的值,有利于为协议计入更多的利息。underlyingAmountToATokenAmount和aTokenAmountToUnderlyingAmount函数中的原始rayToWad功能应保留,因为它在计算要从协议发出多少代币时更倾向于四舍五入(截取)。
更新:已在MR#82修复。rayToWad函数已修改,现在四舍五入至最近的Wad。注意实施的修复并没有严格遵循我们提出的拥有两个独立函数的建议。

[L03]转换为aToken单位隐含假定有18位小数

AToken协议的underlyingAmountToATokenAmount函数永不考虑标的资产的小数部分(如aToken)。虽然它是在所有aToken都有18位小数的假设下工作的,但是代码中没有任何东西可以确保这个假设总是成立。
这个问题不会造成当前的安全风险,因为Aave开发人员可以控制aToken有多少位小数。但是,underlyingAmountToATokenAmount函数应该以编程方式强制执行正常运行所需的所有条件,因为否则可能会出现危险的意外行为。
为了支持明确性并防止将来修改基础代码时出现漏洞,可以考虑修改underlyingAmountToATokenAmount函数,以显式地说明标的aToken的小数位。否则,警告文档应该包含在函数文档字符串中。一种不太灵活的做法是通过编程强制所有在AToken合约构建器创建的AToken具有18位小数。
更新:已在MR#83修复。AToken构建现在程序化强制所有aToken有18位小数。

[L04]下溢预防冗余

函数decreaseUserPrincipalBorrowBalance不必要地实现了下溢预防以保证user.principalBorrowBalance大于等于_amount.OpenZeppelin SafeMath的sub函数已有次验证,可以考虑删掉它。
更新:不是个问题,因为代码正确地遵循了“早报错大报错”的原则。

[L05]禁用的储备固定利率借贷可以再平衡

LendingPool合约的rebalanceFixedBorrowRate函数不验证作为参数传递的储备是否有效。因此,禁用储备可能会在其参数中发生意外的改变。
如果行为是预期的,那么这个问题应该被忽略。否则,考虑通过onlyActiveReserve修饰符要求储备是活跃的。
更新:已在MR#84修复。

[L06]抵押品可以充值进已禁止使用的储备中

LendingPool合约的deposit函数允许用户将资产作为抵押品(通过将_useAsCollateral参数设置为true)存入储备中,这个储备在系统上可能被禁用作抵押品。特别是在usageAsCollateralEnabled为false的储备中。
这个问题不构成安全风险,因为当作为抵押品储备被系统禁用时,此类存款将不计入抵押品。然而,这样的行为会给用户带来困惑。当用户试图将储备中的资产作为抵押品(_useAsCollateral参数设置为true)时,可以考虑回滚交易,因为作为抵押品的储备使用在系统上是禁用的。
更新:不是个问题,Aave团队理解这是协议的预期行为:
"决定充值是否可用作抵押品的主要配置参数是usageAsCollateralEnabled。_useAsCollateral参数仅作为用户首选项。[通过修复C01]设置此首选项的可能性已从deposit函数中移除。我们认为,作为一个用户首选项,用户应该能够独立于平台配置以任何方式进行设置,尤其是考虑到平台配置可能在未来发生变化。我们将确保对这个函数进行更详细的文档记录,以确保更好地让用户理解标志的含义。"

[L07]潜在的零除法

LendingPoolDataProvider合约的balanceDecreaseAllowed函数被用来验证账户抵押品余额的递减。一旦它计算抵押余额下降后,该函数计算产生的清算阈值。然而,在最后的计算中,新的抵押余额被用作除数,而没有事先验证它是否为零。
虽然这不会造成安全问题,但是由于使用了OpenZeppelin SafeMath的div函数,需要注意的是,0的除法会导致交易返回一个来自SafeMath库意外的、对用户不友好的的错误消息。因此,为了避免意外的行为,如果collateralBalancefterDecrease变量为零,则考虑返回false。
更新:已在MR#85修复。

[L08]LiquidationCall事件中错误的数据记录

LendingPool合约中的liquidationCall函数发送liquidationCall事件,通知链下客户成功的清算。然而,它当前记录是错误的数据。
* 当事件记录清算人传递的_purchaseAmount参数时,该参数可能高于实际的清算金额(如LendingPoolLiquidationManager.sol的第110到114行所示)。记录的金额应该与实际结算的金额相匹配。
* 传递给事件的第一个参数应该是_collateral(而不是liquidationManager)。
必须强调的是目前还没有包含LiquidationCall事件的发送和记录的数据的单元测试。因此,可以考虑实现相关的单元测试,以防止在将来对基础代码更改中再次引入这些错误。
更新:已在MR#47修复。

[L09]Repay事件中错误的数据记录

LendingPool合约中的还款功能发出一个还款事件,通知链下客户成功还款。当用户在_amount参数中传递一个高于债务的值(包括UINT_MAX_VALUE)时,该函数假设用户愿意偿还全部借款(无论实际需要偿还的金额是多少)。在这种情况下,在成功还款完成后,函数将发出以_amount作为还款数额的Repay事件日志,其中应该实际记录有效支付的金额(即paybackAmount)。
可以考虑修改还款事件记录的数额,使其与用户实际支付的数额相匹配。应该注意的是,目前还没有覆盖还款事件的发送和记录数据的单元测试。因此,可以考虑实现相关的单元测试,以防止在将来对基础代码的更改中再次引入此错误。
更新:已在MR#47修复。

[L10]BurnOnRedeem事件冗余

AToken合约的burnOnRedeemInternal函数发出一个BurnOnRedeem事件,该事件记录帐户和销毁代币的数量。但是,紧接着调用了OpenZeppelin的ERC20合约_burn函数,该函数发出具有相同信息的Transfer事件。
为了简化和避免冗余*作,可以考虑删除burnonredemption事件。
更新:已在bc43147修复。

[L11]MintOnDeposit事件记录标的资产的数额

MintOnDeposit事件记录发送的标的资产数量,而不是生成的aToken数量。这可能被认为是有意的行为,因为_mint函数已经发出一个记录aToken数量的Transfer事件。但是,它与redeem函数的Redeem事件中记录的数据不一致。
可以考虑修改MintOnDeposit事件,以记录发送的标的资产数量和生成的aToken数量。
更新:已在MR#36修复。

[L12]无法满足的条件

LendingPool.sol的第211行中的require语句中的第一个条件(在borrow函数内),检查用户控制的参数_interestRateMode是否等于uint256(CoreLibrary.InterestRateMode.VARIABLE)。这是一个无法满足的条件,因为第207行中的if语句已经确保_interestRateMode将等于uint256(CoreLibrary.InterestRateMode.FIXED)。
为了提高可读性并避免不必要的验证,可以考虑删除无法满足的条件。
更新:已在MR#**修复。

[L13]不一致的验证

LendingPoolCore合约的transferToFeeCollectionAddress函数目前不能阻止用户像transferToReserve函数那样以ERC20转账*作发送ETH。为了保持一致性和防止意外行为,可以考虑在transferToFeeCollectionAddress函数中实现这个验证。
更新:已在MR#88修复。

[L14]测试不通过

按照项目README文件中的说明,测试套件完成了六个失败的测试。测试是这样的:
- Contract: LendingPool - token economy tests
- BORROW - Test user cannot borrow using the same currency as collateral

- Contract: LendingPool FlashLoan function
- FLASH LOAN - Takes ETH Loan, returns the funds correctly:

- Contract: LendingPool FlashLoan function
- FLASH LOAN - Takes ETH Loan, does not return the funds:

- Contract: LendingPool FlashLoan function
- FLASH LOAN - Takes out a 500 DAI Loan, returns the funds correctly:

- Contract: LendingPool FlashLoan function
- FLASH LOAN - Takes out a 500 DAI Loan, does not return the funds:

- Contract: LendingPool liquidation
- LIQUIDATION - Liquidates the borrow
由于测试套件超出了审计的范围,请考虑彻底检查测试套件,以确保所有测试都能成功运行。此外,建议只合并那些不会破坏现有测试(也不会减少覆盖率)的代码。
更新:Aave团队承认了这个问题,并对测试套件进行了改进:
"由于迁移脚本中的问题,测试会在已审计的提交版本上中断。随着测试系统的完全更新,这些测试不再失败,并且增加了60多个其他单元测试。"

[L15]缺少全面的文档字符串

基础代码中的许多合约和函数缺乏全面的文档说明。这阻碍了评审人员对代码意图的理解,而这对于正确地评估安全性和正确性都是至关重要的。另外,文档字符串提高了可读性并简化了维护。一般来说,文档字符串应该明确地解释函数及其参数的目的或意图、可能出现故障的场景、允许调用它们的角色、返回的值和发送的事件。
可以考虑对合约公共API中的所有函数(及其参数)进行全面的文档记录。实现敏感功能的函数,即使不是公共的,也应该清楚地用文档记录下来。在编写文档字符串时,考虑始终遵循以太坊原生规范格式(NatSpec)。
更新:Aave团队已承认这个问题:
"们将确保按照NatSpec格式添加改进的文档。"

[L16]错误的文档字符串和注释

在整个基础代码中,有几个文档字符串和行内注释被发现是错误的,应该进行修复。特别是:
* 在CoreLibrary.sol的93和94行中,getNormalizedIncome函数的文档字符串似乎是前一个函数getReserveUtilizationRate的副本。
* CoreLibrary.sol的第70行应该是“usageAsCollateralEnabled”而不是“usageAsCollateral”。
* 在LendingPool合约的borrow函数中,行内注释中的步骤枚举从第2步跳到第4步。
* LendingPool.sol的第366行注释应该被删除。
* LendingPool.sol的第**1行注释应该是“greater or equal”而不是“greater of equal”。另外,注意下面的require语句只是检查绝对相等。
* burnOnRedeemInternal函数的文档字符串表示“只有出借池才能调用这个函数”。但是该函数被标记为内部函数,不能从其他合约调用。
* repay函数的文档字符串状态为“repays a borrow […] for the specified amount”。这并不总是正确的——如果调用者指定的数额等于UINT_MAX_VALUE,那么整个借贷就会被偿还。
* 在ILendingRateOracle接口中,文档字符串中指定的所有单位都应该用Ray而不是Wei。
* IPriceOracle.sol的第18行和第23行应该是“ETH price”而不是“asset price”。
* LendingPoolLiquidationManager.sol的第23行应该是“LendingPoolLiquidationManager”而不是“LiquidationManager”。
* LendingPoolDataProvider.sol的第33行应该是“the loan can be liquidated”而不是“the loan gets liquidated”。
* LendingPoolDataProvider.sol的第144行引用AToken合约中不存在的transferInternal()函数。
更新:已在MR#90和MR#98修复。

[L17]误导性的错误消息

* LendingPoolCore合约的回退函数只允许从合约中转账ETH。但是,当前给出的实现,无论何时从合约的构造函数执行,ETH转账都会失败,并会出现错误消息。可以考虑在行内注释中澄清这样的行为,以防止未预期的行为。
* 在LendingPool.sol的463行,错误消息与实际检查的条件不匹配,因此应该将其修改为“[…]等于或小于[…]”。
* 虽然错误消息在LendingPool.sol的第610行声明“The caller of this function must be a contract”,flashLoan函数可以都被合约和外部帐户调用。
更新:未修复,问题在#98跟踪。

[L18]白皮书问题

提交审计版本包含了Aave协议白皮书放在docs文件夹中。下面我们列出了与白皮书相关的一些必须解决的问题:
* 在整个白皮书中,“ray”和“ray”可以互换使用。
* 健康因子的定义与代码不匹配,应该进行更新。
* 在第1.2节中,“Block height at which […]”应该是“Timestamp at which […]”。另外,应该用Tl代替Bl。
* 第1.9节中有一个未完成的句子,其中是:“Check paragraph X where […]”。
* 有两个编号为1.15的部分。更重要的是,它们符合“当前流动性利率”与“借款/流动性利率增量”之间的循环定义。
* 在第1.16节中,“Ci ls”应该说“Ci is”(缺少空格)。此外,最后一句不连贯,应该是“The formula to calculate Ci at a specific point in time […]”。
* 在第1.17节中,“储备规范化收入”的定义是不正确的,因为它是从第1.16节中复制过来的。
* 在1.21节的最后一句话,BΔu应该由TΔu所取代。
* 第2.3节的最后一段提到了白皮书中不存在的“第4章”。
* 在3.1节中,“市场平均贷款利率”的公式定义了权重和。要得到平均值,需要除以平台的总容量。
* 错误地使用符号Bf代替Rf来表示“当前固定借款利率”。
* 在“闪电贷”部分,外部合约上实际执行的方法是executeOperation,而不是前面所说的executeAction。
* 在“当前固定贷款利率”部分,公式没有指定在在利用率U等于阈值Tr时会什么发生。此外,白皮书指出,利用率阈值设置为0.25 * 1e18,但在代码中设置为1e27 / 2。
* 在“固定利率再平衡”部分,向上再平衡的不等式是在错误的方向(应该是Bfu >Lr)。
* 在“固定利率再平衡”一节中,向下再平衡的公式应该是(1 + delta)而不是(1 - delta)。
更新:部分已修复。白皮书得到了显著的改进。新版本的白皮书仍有一些小问题需要解决。特别是:
* 新版本的白皮书声明利用率阈值现在被设置为1e27 / 2,但是在代码中它被设置为1e27 / 4。
* 在“再平衡过程”一节中,rebalanceFixedBorrowRate函数应该重命名为rebalanceStableBorrowRate。
* 第1.2节提到了LendingPoolLibrary.sol文件不存在。
* 参照decreaseTotalBorrowsFixedAndUpdateAverageRate,increaseTotalBorrowsFixedAndUpdateAverageRate函数应该更新“fixed”重命名为“stable”。
* 在第4节的导言中,使用了“固定汇率”一词。
* 在第4.4节中,将“fixed borrow rate of user x”改为“stable borrow rate of user x”。
* 图14在第二个条件块中缺少一个“Yes”标签。
* 考虑使用下标s(而不是f)引入到稳定利率参数。因为s已经用于引用缩放变量,所以可以考虑使用一个同义词,例如下标m的multiplier。
* 在第2.6节中:“all votes are biding”。

注意事项&额外信息


[N01]市场可能会资不抵债

当所有抵押品的价值都低于所有借入资产的价值时,我们就说市场已经资不抵债了。降低市场资不抵债风险的方法包括:谨慎选择抵押比率、激励第三方抵押品清算、谨慎选择在平台上上市的代币等。然而,资不抵债的风险不能完全消除,市场破产的方式有很多种。例如:
* 在网络高度拥堵的时候,标的(或借出)资产的价格会迅速大幅波动,导致市场在被足够多的清算交易挖掘出来之前就已资不抵债。
* 在市场高度波动的时候,预言机的价格会暂时离线。这可能导致预言机在市场资不抵债之前不会更新资产价格。在这种情况下,永远不会有清算的机会。
* 管理员上线ERC20的代币,其中包含一个最新发现的缺陷,允许生成任意多的代币。这种糟糕的代币被用作抵押来借出从未打算偿还的资金。
无论如何,资不抵债的市场影响可能是灾难性的。这将意味着aToken合约实际上是在运行部分储备。这可能会导致“银行挤兑”,最后的供应者会赔钱。
这种风险并不是Aave协议所特有的,在我们的Compound审计中也强调了这一点。所有的抵押贷款(即使是非区块链贷款)都有资不抵债的风险。然而,重要的是要提高人们对这一风险的认识,而且从破产中恢复过来可能非常困难。
更新:Aave团队承认了这一点,认为这是基本经济模型的固有风险。

[N02]固定利率贷款可能永远不会复利

Aave协议允许借款人以固定或可变利率获得贷款。
可变利率贷款每次复利,任何用户更新借出贷款的储备。相反,当储备更新时,固定利率贷款不会复利。它们只在特定贷款更新时计算复利。换句话说,除非已取出固定利率贷款的借款人再次借款,偿还部分贷款,置换利率模式,再平衡固定利率,或清算,否则他们的贷款利息将永远不会复利。
尽管这个问题不构成安全风险,但明确说明这两种贷款之间的本质区别是很重要的,以便提高终端用户的认识。
更新:Aave团队承认该注意事项,并将在文档中更好地阐明可变利率贷款和稳定利率贷款之间的这一特殊差异。

[N03]固定利率贷款的特点是松散封装

在整个基础代码中,固定利率贷款和可变利率贷款之间的界限和相互作用没有很好地定义,这可能会导致令人惊讶或不受欢迎的行为。为了研究这一想法,有必要研究几个不同的例子,其中一些已经在本报告中提到,同时将它们视为底层架构的结果。
由于每个用户只能从每个储备中获得一个活跃贷款,因此borrow函数必须关闭现有贷款才能创建一个新的。这可以隐式地在固定利率贷款和可变利率贷款之间转换。但是,这允许用户绕过对新固定利率贷款的规模和抵押限制,或者使用swapBorrowRateMode函数显式置换的贷款。
实际上,规模限制的首要原因是可能*控利用率(通过充值和借款)。这进而影响应用于新固定利率贷款的利率,当*作被撤销时,该利率不会对随后的利用率变化作出响应。Aave团队承认,规模和抵押要求增加了一个屏障,但在小范围内使用单个帐户,或在大范围内使用多个帐户,不排除有这种*控发生的可能性。最终,这种攻击是可能的,因为固定利率和可变利率贷款共享相同的资产池和利用率,但对市场变化的响应不同。
这也导致了另一个安全要求,其中固定利率必须始终高于流动池可变利率,以避免用户从同一个池中借贷。当这种情况发生时,再平衡贷款的解决方案会产生一些不良后果。首先,它意味着固定利率贷款的利率实际上是可以变化的。其次,它引入了一种间断性,在视窗底部有贷款的人可以突然发现他们的利率上升到当前市场利率。最后,这个条件可以通过利用率来控制,这意味着用户可以故意地使自己或其他用户容易受到再平衡的影响(在任何一个方向)。
最后,可变利率贷款在任何用户更新储备时复利,而固定利率贷款只在特定贷款更新时复利。考虑到支持数据结构和更新函数之间的相似性,这可能会让用户甚至会让熟悉代码的开发人员感到惊讶。
虽然这些问题可以单独解决,但我们认为它们是固定利率贷款的松散封装的症状,这里实现与可变利率贷款和出借人共享数据结构和储备。可以考虑围绕固定利率贷款特性定义明确的边界(通过文档和代码分离),以简化系统并使其更易于理解。
更新:Aave团队理解意思是当前功能的行为是有意的,并对其设计感到满意。虽然我们仍然对稳定利率特性有所保留,但将其从“固定”重命名为“稳定”,更好地阐明协议的实际行为是向前迈进的一步。即将发布的N02文档更有助于描述这两种类型的费率的特性。

[N04]升级应该更新缓存的地址

Aave协议将在未来要通过将要实现的治理系统进行升级。当在LendingPoolAddressesProvider合约(参见“[H01]缺乏访问控制”)中通过特权帐户设置新地址时,这些升级就会发生。系统的几个组件不是孤立的,而是相互依赖的。为提高效率,它们在存储中缓存地址(以避免过于频繁地查询LendingPoolAddressesProvider合约)。因此,在LendingPoolAddressesProvider中设置新地址必须始终被视为升级过程中潜在的多个步骤之一。合约中依赖升级的所有缓存地址也必须更新。
接下来我们列出一个详细列表,说明哪些地址被缓存在系统的合约中。
* LendingPool将以下地址保存在存储中:LendingPoolCore、LendingPoolParametersProvider、LendingPoolAddressesProvider和LendingPoolDataProvider。
* LendingPoolLiquidationManager存储了以下地址:LendingPoolCore, LendingPoolParametersProvider, LendingPoolAddressesProvider和LendingPoolDataProvider。
* DefaultReserveInterestRateStrategy存储了地址:LendingRateOracle和LendingPoolCore。
* LendingPoolDataProvider将保存地址在存储中:LendingPoolCore和LendingPoolAddressesProvider。
* LendingPoolConfigurator在存储中保存地址:LendingPoolAddressesProvider。
* LendingPoolCore保存地址在存储中:LendingPool和LendingPoolAddressesProvider。
更新:Aave团队承认这个问题:
"未来的Aave治理框架将负责控制和更新缓存地址,一旦任何影响到合约的更新就会实施。"

[N05]在事件中丢失相关数据

一些发送的事件可能会从记录的其他相关数据中获益。特别是:
* Borrow事件应该记录已支付的借款手续费。
* Repay事件应该记录实际还款人的地址,考虑到可以代由其他账户进行还款。
* LiquidationCall事件应该记录清算人的地址。
更新:已在MR#47修复。

[N06]未使用的LiquidationCompleted事件

LendingPoolLiquidationManager.sol的第51行声明一个LiquidationCompleted事件。由于它永远不会发送,可以考虑删除声明或适当地发送事件。
更新:已在MR#57修复。

[N07]当金额等于可用流动性时闪电贷会回滚

LendingPool合约的flashLoan功能严格要求贷款金额低于储备的可用流动性。然而,当两个金额相等时,此限制将回滚交易,并会出现误导性的错误消息,通知没有足够的流动性。
更新:已在MR#55修复。

[N08]函数赎回可以以数额为零来调用

AToken合约的redeem函数可以被参数_amount为零进行调用。为了防止不必要的事件发送,并遵循“尽早尽大失败”模式,可以考虑添加一条require语句来验证_amount是否大于零。
更新:已在MR#60修复。

[N09]重构getReserveUtilizationRate函数

要计算储备的总借款额,CoreLibrary的getReserveUtilizationRate函数应该重用可用的getter gettotalborrow,而不是重复代码_self. totalborrow fix.add (_self. totalsvariable)。
更新:已在MR#62修复。

[N10]重用修饰符onlyReserveWithEnabledBorrowingOrCollateral

LendingPoolCore.sol的第677至682行地require语句和reserve的分派与onlyReserveWithEnabledBorrowingOrCollateral修饰符实现了相同的功能。
可以考虑删除这些行并将修饰符添加到transferToReserve函数声明中。
更新:已在MR#63实现。

[N11]冗余地getters

* LendingPool合约中的getters getUserReserveData和LendingPoolDataProvider中的getUserReserveData是两个当前返回相同数据的外部函数。注意的是前一个getter在内部调用后一个getter。
* 有三个公共getter来读取LendingPoolCore合约的reservesList状态变量。即:LendingPoolCore中的getReserves,LendingPool中的reservesList(Solidity自动生成)和getReserves。
为了简化和封装,可以考虑删除所有这些冗余函数,确保最多只有一个可公开访问的getter去暴露数据。
更新:不是问题,冗余就是目的。

[N12]unit40变量的隐式向上转换

为了支持明确性和代码可读性,可以考虑在CoreLibrary.sol的第314行显式地将_lastUpdateTimestamp变量从uint40转换为uint256。
更新:已在MR#65修复。

[N13]对函数局部变量结构体的使用不清晰

LendingPool合约的borrow函数使用名为BorrowLocalVars的结构体,而不是声明本地变量。类似的情况出现在LendingPoolDataProvider的calculateUserGlobalData函数中,该函数使用UserGlobalDataLocalVars结构体。
由于这些实现的目的还不清楚,可以考虑包括一个行内注释来清楚地解释为什么这些函数更喜欢使用struct而不是局部变量。这将增加代码的可读性,防止开发人员在将来对基础代码引入不希望的更改。
更新:已在MR#66修复。

[N14]未使用的状态变量

在LendingPoolLiquidationManager合约中,状态变量parametersProvider从未使用过,因此应该删掉。
更新:不是问题。即使没有使用状态变量parametersProvider,它也有助于使存储的布局与LendingPool合约(它将给LendingPoolLiquidationManager进行delegatecall)的存储保持一致。

[N15]状态变量缺乏明确的可见性

以下状态变量和常量使用默认的可见性:
* 在WadRayMath库中:
WAD_RAY_RATIO
* 在CoreLibrary库中:
SECONDS_PER_YEAR
* 在DefaultReserveInterestRateStrategy合约中:
FIXED_RATE_INCREASE_THRESHOLD,
core,
lendingRateOracle,
baseVariableBorrowRate,
variableBorrowRateScaling,
fixedBorrowRateScaling,
borrowToLiquidityRateDelta,
reserve
* 在LendingPool合约中:
addressesProvider,
core,
dataProvider,
parametersProvider,
UINT_MAX_VALUE
* 在LendingPoolConfigurator合约中:
poolAddressesProvider
* 在LendingPoolCore合约中:
ethereumAddress,
lendingPoolAddress,
addressesProvider,
reserves,
usersReserveData
* 在LendingPoolDataProvider合约中:
core,
addressesProvider,
HEALTH_FACTOR_LIQUIDATION_THRESHOLD
* 在FlashLoanReceiverBase中:
addressesProvider
* 在FeeProvider中:
originationFeePercentage,
feesCollectionAddress
为了提高可读性,可以考虑显式地声明所有状态变量和常量的可见性。
更新:已在MR#68修复。

[N16]命名返回变量

在整个基础代码中对命名返回变量的使用不一致。可以考虑删除所有命名返回变量,显式地将它们声明为局部变量,并在适当的地方添加必要的返回语句。这应该有利于项目的明确性和可读性。
更新:Aave承认了这种不一致性,并计划在将来以某种方式一致地统一返回语句声明。

[N17]命名问题

基础代码中的多个变量、参数和函数可能会受益于更好的命名,从而提高可读性和避免混淆。
特别是:
* 修饰符whenTranferAllowed应该叫做transferallowed。
* AToken合约的状态变量initialExchangeRate解释说明不足。它应该表示交换利率的“方向”。也就是说,要么[aToken / asset],要么[asset / aToken]。
* collateralBalancefterDecrease结构字段应叫做collateralBalanceAfterDecrease。
* 所有形式的liquidationDiscount应改为liquidationBonus,因为它们涉及的价值大于1。
* 在LendingPoolDataProvider合约的calculateUserGlobalData函数中使用的currentLtv变量并不是实际的“当前贷款价值”,因为它还没有在ETH的总抵押余额上取平均值。这一步是通过在第119行重新分配这个变量来完成的。对于currentLiquidationThreshold变量也可以这样说。
* 同一个名称usageAsCollateralEnabled用于表示一个储备的两个不同状态。一方面,它可能意味着用户已经启用了某个储备作为抵押品(请参阅LendingPoolDataProvider.sol的第369行)。另一方面,它表示管理员是否已经建立了一个储备来用作系统范围的抵押品(请参见LendingPoolDataProvider.sol的第273行)。
更新:Aave团队已承认:
"由于这个问题暴露了更多基础工作,影响到其他的链下服务,我们承认这个问题并在以后的修订版中为这些变量提供更好的命名。"

[N18]错别字

* LendingPool.sol的第199行应该说是“need”而不是“needs”。
* LendingPool.sol的382行应该说是“cumulate”而不是“comulate”。
* LendingPool.sol的3**行应该说是“payback”而不是“payaback”。
* LendingPool.sol的第3**和392行应该说是“subtracting”而不是“substracting”。
* LendingPool.sol的**6行应该说是“is”而不是“in”。
* AToken.sol的第71行应该说是“underlying”而不是“undelying”。
* AToken.sol的第128和154行应该是“Optimization”而不是“Optmization”。
* CoreLibrary.sol的第281行应该说是“subtract”而不是“substract”。
* CoreLibrary.sol的第293行应该说是“subtracted”而不是“substracted”。
* LendingPoolLiquidationManager.sol的第97行应该说是“liquidated”而不是“liuquidated”。
* 在README文件中,“accross”应该是“across”。
* 在README文件中,“docker compose”的所有实例都应该为“docker-compose”。
更新:已在MR#69修复。

[N19]不必要地使用的Ownable合约

LendingPoolCore、LendingPoolDataProvider和LendingPoolConfigurator合约都不必要地继承自OpenZeppelin的Ownable合约。鉴于Ownable的特性从未在这些合约中使用过,可以考虑将其从其继承链中移除。
更新:已在MR#70修复。

[N20]不必要的import

有几个导入语句没有使用,可以删除。即:
* 在LendingPoolLiquidationManager.sol的第7、11、15和16行中的import。
* AToken.sol的第3行的import。
* 在LendingPool.sol的第6行和第10行中import。
* 在LendingPoolConfigurator.sol的第6行和第9行的import。
* 在LendingPoolCore.sol的第10行的import。
更新:已在MR#71修复。

[N21]代码中的TODO

在基础代码中有一些“TODO”注释应该被删除,而应该在项目的问题待办事项列表中进行跟踪。参见示例LendingPool.sol的第627行。
更新:已在MR#55,MR#52以及MR#97修复。

[N22]编码风格

在整个基础中都可以看到与Solidity风格指南的有细微差异。为提高代码的可读性,请始终遵循Solidity的编码风格指南,该指南可以通过linter工具(如Solhint)在整个项目中强制执行。
更新:Aave承认代码风格的偏差,并将在基础代码的下一个迭代中对其进行调整。

[N23]声明uint作为uint256

为了更明确,应该将uint的所有实例声明为uint256。
修复:已在MR#72修复。

总结

最初,发现了6个严重风险和8个高风险问题。为了遵循最佳实践并减少潜在的攻击面,提出了几个更改。在审查了与Aave团队报告的所有问题后,我们将1个严重风险问题降级,并将2个高风险问题视为不是问题。识别到严重的攻击途径和损坏的功能,一起的还有缺乏测试套件和说明文档,反映了基础代码还正在开发阶段。正如审计更新部分所述,对于每个特定的问题,发现的所有严重问题都得到了正确的修复。
第一轮审计是Aave迈向成熟的第一步,是旨在处理大量金融资产的项目所需要达到一定的成熟度。为了进一步帮助项目达到生产准备状态,我们强烈建议进行更多的安全审查。

原文链接:https://blog.openzeppelin.com/aave-protocol-audit

最后提醒一下,市场有风险,本文只是个研究,不作为投资建议,请合理控制风险。

点赞就是对传教士最大的鼓励,谢谢支持。

—-

编译者/作者:DeFi传教士

玩币族申明:玩币族作为开放的资讯翻译/分享平台,所提供的所有资讯仅代表作者个人观点,与玩币族平台立场无关,且不构成任何投资理财建议。文章版权归原作者所有。

LOADING...
LOADING...