[蝴蝶效应] Bug修复引发的复合安全事件

文章描述了Compound协议中的两个bug及其对COMP代币向用户分配的影响。

[蝴蝶效应] Bug修复引发的复合安全事件

由 BlockSec 团队(@BlockSecTeam)

在上周,Compound 协议出现了一个会“意外”向用户发送大量 COMP 代币的漏洞。这个漏洞(本博客中的漏洞 2)的根本原因是由于另一个之前发现的漏洞(本博客中的漏洞 1)的修复不当。

在本博客中,我们将详细阐述第一个漏洞的根本原因,以及第一个漏洞的修复如何导致第二个漏洞的产生。

背景

Compound 协议基于 Compound 白皮书。通过 cToken 合约,链上账户向协议提供资金(Ether 或 ERC-20 代币)以获得 cToken,或者通过持有其他资产作为抵押品向协议借入资产。Compound cToken 合约跟踪这些余额并算法化地设定借款人的利率

为了激励用户,为 Compound 提供流动性(供应资金)的用户可以获得利息。具体来说,用户将资产(例如 Ether 或其他 ERC20 代币)存入 Compound,并获得相应的 cToken。当用户将 cToken 退还给 Compound 时,如果没有债务,底层资产(Ether 或 ERC20 代币)和利息将退还给用户。例如,如果一个用户有 1000 Ether,他/她可以通过 cEth.mint(1000) 将资产放入 Compound 以获得 cToken。

cToken 代表了已锁定在 Compound 中的底层资产。用户可以进一步使用 cToken 作为抵押品来借入其他资产。例如,用户可以通过 ceth.mint(1000) 存入 1000 Ether,然后使用获得的 cToken 以 75 Ether 的价值(超额抵押——此数值取决于抵押因子)借入 x Dai,通过 cDai.borrow(x) 实现。

核心逻辑实现在 Comptroller 合约中。它维护着用户的状态,例如用户向 Compound 存入了多少代币,借入了多少代币,以及用户是否可以借入更多代币。此过程中调用的函数包括 getHypotheticalAccountLiquidityInternal()borrowAllowed()mintAllowed() 等。

Compound 还有一个称为 COMP 的治理代币。COMP 代币可用于对提案进行投票。此外,COMP 代币可以在交易所进行交易。目前,COMP 的价格约为 300 美元。

漏洞 1

2021 年 9 月 31 日,Compound DAO 中出现了一个新的提案(提案 62),旨在修复 Comptroller 中的一个漏洞。

该漏洞与 CompSpeed 相关,它表示每个区块可以分配给用户的 COMP 代币数量。

`mint` 函数的流程

接下来,我们将使用 mint 函数来描述此漏洞的原因。mint 函数的调用链是:mintmintInternalmintFresh

mintFresh 函数中,它调用 mintAllowed,然后更新用户的 cToken 余额。

mintAllowed 函数中,它首先调用 updateCompSupplyIndex,然后调用 distributeSupplierComp 来 1)更新市场的 compSupplyState,以及 2)向用户分配 COMP 代币。

updateCompSupplyIndex

updateCompSupplyIndex 函数将更新每个市场的状态,主要是 compSupplyState[cToken]

CompMarketState 结构中,它记录了本次更新的区块号(block)以及影响应分配给用户(持有 cToken 的用户)的 COMP 代币数量的奖金指数(index)。

每个代币的奖金指数(index)是什么? 这是随时间累积的值(如下公式所示)。

这显示了应分配给用户的 COMP 代币数量(对于用户持有的每个 cToken)。

distributeSupplierComp

另一个函数 distributeSupplierComp 负责记录应分配给用户(供应商)的 COMP 代币数量到 compAccrued[supplier]

具体来说,它在 updateCompSupplyIndex 函数中更新全局奖金指数 compSupplyState。然后在 distributeSupplierComp 函数中,supplyIndex 记录当前奖金指数,supplierIndex 显示用户的(供应商)上次奖金指数。差值 (supplyIndex - supplierIndex) 乘以 用户的 cToken 余额,即为应分配给用户的 COMP 代币数量。

漏洞 1 的原因

还有一个函数 setCompSpeed 用于调整市场的 supplySpeedcompSpeeds[address[cToken]])。

这是因为,如果我们为某个市场将 CompSpeed 设置为零,则意味着 COMP 代币不会分配给该市场的用户。因此,如果我们想先禁用某个市场的 COMP 分配,然后再重新启用它,可以按照以下步骤操作:

  • 步骤 I:将 CompSpeed[cToken] 设置为零,以禁用 COMP 代币的分配。
  • 步骤 II:调用 setCompSpeed 函数将 CompSpeed[cToken] 设置为非零值。

步骤 I:对于在步骤 I 中已禁用 COMP 代币分配的市场(supplySpeed == 0),区块号不为零,因为在 updateCompSupplyIndex 中区块号是持续更新的(else if (deltaBlocks > 0))。

步骤 II:在执行步骤 II 中的操作时,setCompSpeedInternal 函数会经过 else if (compSpeed != 0) 语句(第 1083 行)。然后,在第 1088 至 1093 行,有一个检查 if (compSupplyState[address(cToken)].index == 0 && compSupplyState[address(cToken)].block == 0) 来初始化一个新市场indexblock。但是,由于我们正在重新启用一个现有市场的 COMP 代币分配(而不是一个新市场),因此第 1090 和 1091 行的语句不会被执行来初始化 indexblock(因为 compSupplyState[address(cToken)].block 不为零)。

总之,对于一个当前已禁用的市场,index 为零。但是 block 不为零。这意味着当我们通过调用 setCompSpeedCompSpeed[cToken] 设置为非零值来重新启用已禁用的市场时,index不会被重新初始化为 CompInitialIndex (1e36)(第 1090 和 1091 行未执行)。

漏洞 1 的影响

我们进一步深入研究负责分配 COMP 代币的 distributeSupplierComp 函数。

supplierIndexcompInitialIndex。然而,由于漏洞,supplyIndex 仍然为零,这将导致 Double memory deltaIndex = sub_(supplyIndex=0, supplierIndex=1e36) 发生下溢。

漏洞 2:由漏洞 1 的修复引入

为了修复这个漏洞,项目方更改了代码逻辑。具体来说,它在新市场初始化时立即将 index 初始化为 compInitialIndex

由于全局奖金指数(index)已初始化为 compInitialIndex,用户奖金指数也应初始化为该值。让我们看一下 distributeSupplierComp 函数。

即使 supplierIndex == 0,第 1234 行的 if 条件也无法满足,因为 supplyIndex 等于(而不是大于)compInitialIndex (1e36)。这导致 supplierIndex 没有正确初始化为 compInitialIndex(其值为 0)。那么 deltaIndex (supplyIndex - supplierIndex) 将等于 compInitialIndex,而不是零。如果用户的 cToken 余额不为零,supplierTokens 将会变成一个很大的值。

总之,如果用户碰巧在漏洞 1 修复之前执行了 mint 操作,那么他/她将拥有 cToken,并且 supplierIndex 将变为零(因为 COMP 代币已被分配)。然后,在漏洞 1 修复之后(引入了漏洞 2),当用户再次调用 mint 函数时,他/她就可以获得大量的 COMP 代币(1e36*ctoken.balanceOf(user))。

实际案例

我们在下文中展示了受影响的市场:

0xF5DCe57282A584D2746FaF1593d3121Fcac444dC: cSAI 0x12392F67bdf24faE0AF363c24aC620a2f67DAd86: cTUSD 0x95b4eF2869eBD94BEb4eEE400a99824BF5DC325b: cMKR 0x4B0181102A0112A2ef11AbEE5563bb4a3176c9d7: cSUSHI 0xe65cdB6479BaC1e22340E4E755fAE7E509EcD06c: cAAVE 0x80a2AE356fc9ef4305676f7a3E2Ed04e12C33946: cYFI

对于用户(0xa7b95d2a2d10028cc4450e453151181cbcac74fc),在此交易中获得了 4,466.542459954989867175 COMP 代币(0x6416ed016c39ffa23694a70d8a386c613f005be18aa0048ded8094f6165e7308)。

对该交易的进一步调试表明,由于漏洞 2,deltaIndex 为 1e36,并且用户当时恰好拥有 cToken。

漏洞 2 的修复

对漏洞 2 的修复很简单。它更改了 distributeSupplierComp 函数中的 if 条件。

教训

  • 这是一个由另一个漏洞的修复所引起的漏洞。如何彻底审查高调项目的代码更改仍然是一个悬而未决的问题。
  • DAO 可以消除中心化的风险。然而,它也使得安全事件的响应过程变得缓慢。
  • 高调的 DeFi 项目可以借鉴传统程序中的良好安全实践,例如部署一个高效的模糊测试系统,并进行持续的测试过程。

关于 BlockSec

BlockSec 是一家开创性的区块链安全公司,由一群享誉全球的安全专家于 2021 年创立。公司致力于为新兴的 Web3 世界增强安全性和可用性,以促进其大规模采用。为此,BlockSec 提供智能合约和 EVM 链安全审计服务,用于安全开发和主动阻止威胁的Phalcon平台,用于资金追踪和调查的MetaSleuth平台,以及供 Web3 构建者在加密世界高效冲浪的MetaSuites扩展。

截至目前,该公司已为 MetaMask、Uniswap Foundation、Compound、Forta 和 PancakeSwap 等 300 多家知名客户提供服务,并在两轮融资中获得了来自 Matrix Partners、Vitalbridge Capital 和 Fenbushi Capital 等杰出投资者的数千万美元投资。

官方网站:https://blocksec.com/

官方 Twitter 账号:https://twitter.com/BlockSecTeam

Sign up for the latest updates