由 BlockSec 团队 (@BlockSecTeam)

在过去的一周,Compound协议出现了一个漏洞,该漏洞会“意外”地向用户发送大量 COMP 代币。该漏洞(本文中的 bug 2)的根本原因是之前发现的另一个漏洞(本文中的 bug 1)被错误地修复了。
在本文中,我们将详细阐述第一个 bug 的根本原因以及第一个 bug 的修复为何会导致第二个 bug。
背景
Compound协议基于 Compound 白皮书。通过 cToken 合约,链上账户向协议提供资金(Ether 或 ERC-20 代币)以接收 cTokens,或者通过持有其他资产作为抵押品来借入资产。 Compound cToken 合约跟踪这些余额并以算法方式设定借款人的利率。
为了激励用户,向 Compound 提供流动性(提供资金)的用户可以获得利息。具体来说,用户向 Compound 提供资产(例如 Ether 或其他 ERC20 代币),然后获得相应的 cTokens。当 cToken 被退还给 Compound 时,如果用户在 Compound 中没有任何债务,则底层资产(Ether 或 ERC20 代币)和利息将被退还给用户。例如,如果一个用户拥有 1000 Ether,那么他/她可以通过 cEth.mint(1000) 将资产存入 Compound 来获得 cToken。

cToken 代表已锁定在 Compound 中的底层资产。用户可以进一步将 cToken 用作抵押品来借入其他资产。例如,用户可以通过 ceth.mint(1000) 存入 1000 Ether,然后使用获得的 cTokens 来借入价值 75 Ether 的 x Dai(超额抵押——此数量取决于抵押因子),具体通过 cDai.borrow(x) 进行。
核心逻辑实现在 Comptroller 合约中。它维护用户的状态,例如用户向 Compound 存入了多少代币,用户借入了多少代币,以及用户是否可以借入更多代币。此过程中调用的函数包括 getHypotheticalAccountLiquidityInternal()、borrowAllowed()、mintAllowed() 等。
Compound 还有一个名为 COMP 的治理代币。 COMP 代币可用于投票赞成提案。此外,COMP 代币可以在交易所交易。目前,COMP 的价格约为 300 美元。
Bug 1
2021 年 9 月 31 日,Compound DAO 中出现了一个新提案(提案 62),旨在修复 Comptroller 中的一个 bug。

该 bug 与 CompSpeed 相关,它代表每个区块可以分配给用户的 COMP 代币数量。
mint 函数的流程
接下来,我们将使用 mint 函数来描述此 bug 的原因。 mint 函数的调用链是:mint → mintInternal → mintFresh。

在 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 代币数量。
Bug 1 的原因
还有一个函数 setCompSpeed 用于调整市场的 supplySpeed(compSpeeds[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) 来初始化新市场的 index 和 block。然而,由于我们是在现有市场(而非新市场)重新启用 COMP 代币的分配,因此第 1090 和 1091 行的语句不会被执行来初始化 index 和 block(因为 compSupplyState[address(cToken)].block 不为零)。
总之,对于当前已禁用的市场,index 为零。但是,block 不为零。这意味着当我们在调用 setCompSpeed 将 CompSpeed[cToken] 设置为非零值来重新启用已禁用的市场时,index 值不会重新初始化为 CompInitialIndex(1e36)(第 1090 和 1091 行未执行)。
Bug 1 的影响
我们进一步深入研究负责分发 COMP 代币的 distributeSupplierComp 函数。

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

Bug 2:由 Bug 1 的修复引入
为了修复 bug,项目方更改了代码逻辑。具体来说,它在初始化新市场时立即将 index 初始化为 compInitialIndex。

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

即使 supplierIndex == 0,第 1234 行的 if 条件也无法满足,因为 supplyIndex 等于(而非大于)compInitialIndex(1e36)。这导致 supplierIndex 未正确初始化为 compInitialIndex(其值为 0)。然后 deltaIndex(supplyIndex - supplierIndex)将是 compInitialIndex,而不是零。如果用户的 cToken 余额不为零,supplierTokens 将变成一个很大的值。
总之,如果用户在 bug 1 被修复之前恰好执行了 mint 操作,那么他/她拥有 cTokens,并且 supplierIndex 将变为零(因为 COMP 代币已被分配)。然后,在 bug 1 被修复之后(引入了 bug 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)。

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



Bug 2 的修复
Bug 2 的修复很简单。它改变了 distributeSupplierComp 函数中的 if 条件。

经验教训
- 这是一个由另一个 bug 的修复所引起的 bug。如何彻底审查高知名度项目的代码更改仍然是一个悬而未决的问题。
- 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 等知名投资者那里获得了数千万美元的投资。
官方 Twitter 账号:https://twitter.com/BlockSecTeam



