Back to Blog

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

Code Auditing
October 10, 2021
8 min read

由 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 函数的调用链是: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 代币数量。

Bug 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 行未执行)。

Bug 1 的影响

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

supplierIndexcompInitialIndex。然而,由于 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 等于(而非大于)compInitialIndex1e36)。这导致 supplierIndex 未正确初始化为 compInitialIndex(其值为 0)。然后 deltaIndexsupplyIndex - 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 等知名投资者那里获得了数千万美元的投资。

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

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

Sign up for the latest updates
~$15.9M Lost: Trusted Volumes & More | BlockSec Weekly
Security Insights

~$15.9M Lost: Trusted Volumes & More | BlockSec Weekly

This BlockSec bi-weekly security report covers 11 notable attack incidents identified between April 27 and May 10, 2026, across Sui, Ethereum, BNB Chain, Base, Blast, and Berachain, with total estimated losses of approximately $15.9M. Three incidents are analyzed in detail: the highlighted $1.14M Aftermath Finance exploit on Sui, where a signed/unsigned semantic mismatch in the builder-fee validation allowed an attacker to inject a negative fee that was converted into positive collateral during settlement; the $5.87M Trusted Volumes RFQ authorization mismatch on Ethereum; and the $5.7M Wasabi Protocol infrastructure-to-contract-control compromise across multiple EVM chains.

Newsletter - April 2026
Security Insights

Newsletter - April 2026

In April 2026, the DeFi ecosystem experienced three major security incidents. KelpDAO lost ~$290M due to an insecure 1-of-1 DVN bridge configuration exploited via RPC infrastructure compromise, Drift Protocol suffered ~$285M from a multisig governance takeover leveraging Solana's durable nonce mechanism, and Rhea Finance incurred ~$18.4M following a business logic flaw in its margin-trading module that allowed circular swap path manipulatio

~$7.04M Lost: GiddyDefi, Volo Vault & More | BlockSec Weekly
Security Insights

~$7.04M Lost: GiddyDefi, Volo Vault & More | BlockSec Weekly

This BlockSec weekly security report covers eight attack incidents detected between April 20 and April 26, 2026, across Ethereum, Avalanche, Sui, Base, HyperLiquid, and MegaETH, with total estimated losses of approximately $7.04M. The highlighted incident is the $1.3M GiddyDefi exploit, where the attacker did not break any cryptography or use a flash loan but simply replayed an existing on-chain EIP-712 signature with the unsigned `aggregator` and `fromToken` fields swapped out for a malicious contract, demonstrating how partial signature coverage turns any historical signature into a generic permit. Other incidents include a $3.5M Volo Vault operator key compromise on Sui, a $1.5M Purrlend privileged-role takeover, a $413K SingularityFinance oracle misconfiguration, a $142.7K Scallop cross-pool index injection, a $72.35K Kipseli Router decimal mismatch, a $50.7K REVLoans (Juicebox) accounting pollution, and a $64K Custom Rebalancer arbitrary-call exploit.

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit