12月3日,着名DeFi借贷协议Aave部署了V2版本,只管我们并没有被雇佣来查看其代码,但在越日,我们照样对其举行了简朴审查。很快,我们就发现了一个影响Aave V1和V2版本合约的破绽,并讲述了该问题。在将我们的剖析发送给Aave的一小时内,他们的团队修复了该破绽,以减轻潜在影响。若是该破绽被行使,这一问题将损坏Aave,并影响外部DeFi合约中的资金。
据悉,有5家差别的平安公司审查了Aave代码库,其中有一些使用了形式化验证。然而,这个破绽并没有被这些公司注意到。这篇文章形貌了这一问题,以及“该破绽是若何逃过检测”等其它的一些经验教训。此外,我们也在开发一种新的Slither检测器,它可以识别这一破绽,从而为以太坊社区提高平安性。
破绽
Aave使用了delegatecall
署理模式,这一点我们在已往的文章中已经详细讨论过了。简朴来看,每个组件被分成了两个合约:(1)包罗实现的逻辑合约,(2)包罗数据并使用delegatecall与逻辑合约举行交互的署理。在逻辑合约上执行代码时,用户与署理合约举行交互。这是delegatecall署理模式的简化示意:
在Aave中,LendingPool(LendingPool.sol)
是使用delegatecall署理的可升级组件。
而我们发现的破绽依赖于这些合约中的两个功效:
借贷池具有其自己的delegatecall功效;
初始化可升级合约
这种可升级模式的一个限制是,署理不能依赖逻辑合约的组织函数(Constructor)举行初始化。因此,状态变量和初始设置必须在公共初始化函数中执行。
在LendingPool中,初始化函数设置提供者地址(_addressesProvider
):
initializer调节器防止多次挪用initialize,它要求知足以下条件为true:
以下:
初始化允许在相同买卖中多次挪用调节器(因此有多个initialize函数);
isConstructor()
是署理执行代码所需的;revision > lastInitializedRevision
允许在合约升级时再次挪用初始化函数;
虽然它通过署理,预期可正常事情,然则(3)也允许任何人直接在逻辑合约上挪用initialize
函数。一旦逻辑合约被部署:
revision将为0x2(LendingPool.sol#L56);
lastInitializedRevision将为0x0;
而破绽是:任何人都可以在LendingPool逻辑合约中设置_addressesProvider
。
随便delegatecall
LendingPool.liquidationCall
直接委托挪用(delegatecall)由_addressProvider
返回的地址:
这允许任何人启动LendingPool逻辑合约,设置受控地址提供者,并执行随便代码,包罗selfdestruct
。
行使破绽的场景:任何人都可以损坏借贷池逻辑合约。下面是一个简化的视觉示意:
缺乏存在检查
就问题自己而言,已经是很严重了,由于任何人都可以损坏逻辑合约,并阻止署理执行借贷池代码。
,,菜包钱包(caibao.it)是使用TRC-20协议的Usdt第三方支付平台,Usdt收款平台、Usdt自动充提平台、usdt跑分平台。免费提供入金通道、Usdt钱包支付接口、Usdt自动充值接口、Usdt无需实名寄售回收。菜包Usdt钱包一键生成Usdt钱包、一键调用API接口、一键无实名出售Usdt。
然而,在署理合约中使用OpenZeppelin会加剧这一问题的严重性。我们在2018年撰写的一篇博客文章中强调,没有代码的合约委托挪用(delegatecall)能在不执行任何代码的情况下返回乐成。只管我们最初发出忠告,但OpenZeppelin并未在其署理合约中修复回退函数:
若是署理委托挪用(delegatecall)了一个已损坏的借贷池逻辑合约,则署理将返回乐成,而不会执行任何代码。
由于Aave可以更新署理以指向另一个逻辑合约,因此这种破绽行使不会持久。但在可行使此破绽的时间局限内,任何挪用该借贷池的第三方合约,都将表现为某些代码已被执行,但现实却并未执行。这将打破许多外部合约的基本逻辑。
受影响的合约
所有AToken(Aave代币):
AToken.redeem
挪用pool.redeemUnderlying
(AToken.sol#L255-L260)。由于挪用什么也不做,用户将烧掉他们的AToken,而不会收到他们的底层资产;WETHGateway(WETHGateway.sol#L103-L111):存款会存储在网关中,然后任何人都可以窃取存款资产;
任何基于Aave信用委托v2(MyV2)的代码库(MyV2CreditDelegation.sol);
若是我们发现的问题被行使,则Aave之外的许多合约都市受到种种方式的影响。确定一份完整的名单是难题的,我们没有试图这样做。这一事宜凸显了DeFi可组合性的潜在风险,以下是我们找到的一些受影响的合约:
DefiSaver v1 (AaveSaverProxy.sol)
DefiSaver v2 (AaveSaverProxyV2.sol)
PieDao – pie oven (InterestingRecipe.sol#L66)
修复及建议
幸运的是,在我们讲述这个破绽之前,还没有人行使它。Aave对其两个版本的借贷池挪用了initialize函数,从而保证了合约的平安:
LendingPool V1: 0x017788dded30fdd859d295b90d4e41a19393f423 修复时间: 2020年12月4日 07:34:26 PM +UTC
LendingPool V2: 0x987115c38fd9fd2aa2c6f1718451d167c13a3186 修复时间: 2020年12月4日 07:53:00 PM +UTC
历久而言,合约部署者应:
在所有逻辑合约中添加一个组织函数(constructor )以使initialize函数无效;
检查delegatecall署理fallback函数中是否存在合约;
仔细检查delegatecall陷阱,并使用slither-check-upgradeability;
形式化验证合约并不是防弹的
Aave的代码库经过了形式化验证,区块链领域的一个趋势是,人们会以为平安特征是圣杯。用户可能会实验凭据这些特征的存在与否,对种种合约的平安性举行排序。我们以为这是危险的,它会导致错误的平安感。
Aave形式化验证讲述列出了 LendingPool 视图函数(例如,它们没有副作用)以及池操作(例如,操作乐成后返回true且不还原)的属性。例如,已验证的属性之一是:
然而,若是逻辑合约遭到损坏,则该属性可能会被损坏。那若何才气对此举行验证?虽然我们无法访问定理证实或所使用的设置,但很可能证实proof没有思量可升级性,或者prover不支持庞大的合约交互。
这在代码验证中是很常见的。你可以通过对整体行为的假设来证实目的组件中的行为,然则在多合约设置中证实属性是具有挑战性和耗时的,因此必须举行权衡。
形式化验证手艺很棒,然则用户必须意识到它们笼罩局限很小,而且可能会错过攻击前言。另一方面,自动化工具和人工审查可辅助开发人员以较少的资源来提升代码库的平安性。领会每种解决方案的优点和局限性,对开发人员和用户而言都至关重要。当前的问题就是一个很好的例子,Slither可以在几秒钟内发现这个问题,受过训练的专家可能会很快指出它,而要用平安特征来检测,则需要支出很大的精神。
总结
Aave做出了努力反映,并在发现问题后迅速修复了该破绽。危急避免了,但最近遭受黑客攻击的其他受害者却没有那么幸运。在部署代码并将其露出于对抗性环境之前,我们建议开发者:
查看这里的检查表和训练;
将Slither添加到你的连续集成管道中并观察其所有讲述;
给平安公司适当的时间来审查你的系统;
请注意可升级性,至少请审查合约升级反模式,合约迁徙的事情方式,以及使用OpenZeppelin的可升级性;
网友评论