2021年8月4日,Popsicle Finance遭受了巨额财务损失(超过2000万美元)的攻击 [1]。 经过人工分析,我们确认这是一起“双重认领”(double-claiming)攻击,即其奖励系统中的一个漏洞允许攻击者反复认领奖励。 在下文中,我们将使用一个攻击交易来演示攻击过程和漏洞的根本原因。
背景
Popsicle Finance 是一个收益优化平台,支持跨链(例如以太坊和BSC)的多个金库。
具体来说,用户首先调用 deposit 函数提供流动性,并获得 Popsicle LP 代币(简称 PLP)。之后,Popsicle Finance 将为用户管理流动性(与 Uniswap 等平台交互)以赚取利润。
用户可以调用 withdraw 函数从 Popsicle Finance 提取流动性,Popsicle Finance 将根据 PLP 代币计算提取金额。
激励奖励来自流动性,会随着时间的推移而累积。
用户可以调用 collectFees 函数认领奖励,这是本次攻击的关键。
漏洞分析
在 collectFees 函数中,会为用户计算 token0Reward 和 token1Reward(相应 LP 代币对的奖励)。整个计算逻辑很简单。但是,该函数使用了一个名为 updateVault 的修饰符,用于相应地更新奖励。


简而言之,updateVault 会:
- 首先调用
_earnFees函数从池中获取累积费用; - 然后调用
_tokenPerShare函数更新token0PerShareStored和token1PerShareStored,它们表示每股代币0和代币1在池中的数量; - 最后调用
_fee0Earned和_fee1Earned函数更新用户的奖励(即token0Rewards和token1Rewards)。

_fee0Earned 和 _fee1Earned 函数共享相同的逻辑,即实现以下公式(以代币0为例):
user.token0Rewards += PLP.balanceOf(account) * (fee0PerShare - user.token0PerSharePaid) / 1e18
请注意,计算是增量的,这意味着即使用户不持有 PLP 代币,计算出的奖励仍然是 token0Rewards 中存储的值。
因此,我们可以得出以下两个观察结果:
- 用户的奖励存储在
token0Rewards和token1Rewards中,这些与任何 PLP 代币无关; collectFees函数仅依赖于token0Rewards和token1Rewards的状态,这意味着在不持有 PLP 代币的情况下也可以提取奖励。
在现实场景中,这意味着用户将钱存入银行,银行给她一张存款凭证。不幸的是,这张凭证既不防伪,也与用户无关。 在这种情况下,有可能制造副本并将其分发给他人,从而从银行获利。
攻击流程
简而言之,攻击者采取了以下步骤发起攻击:
- 创建了三个合约。其中一个用于发起攻击,另外两个用于调用
collectFees函数以获取奖励; - 利用闪电贷,即从 AAVE 借入大量流动性;
- 发起了存入-提取-收取费用(Deposit-Withdraw-CollectFees)循环来执行攻击(总共有8个循环,并从 Popsicle Finance 的多个金库中提取了大量流动性);
- 将闪电贷返还给 AAVE,并通过 Tornado.Cash 洗钱。

具体来说,存入-提取-收取费用循环包含几个步骤,可以使用我们的在线工具 [2]轻松标记和清晰总结:

利润分析
总共,攻击者从 Popsicle Finance 窃取了 2000 万美元,其中包括 2.56K WETH、96.2 WBTC、16 万 DAI、539 万 USDC、498 万 USDT、10.5K UNI。 在这次利用之后,攻击者首先通过 Uniswap 和 WETH 将所有其他代币兑换成 ETH,然后通过 Tornado.Cash 进行洗钱。
致谢
Yufeng Hu, Ziling Lin, Junjie Fei, Lei Wu, Yajin Zhou @BlockSec
(按姓氏字母顺序排列)
Medium: https://blocksecteam.medium.com/
Twitter: https://twitter.com/BlockSecTeam
联系方式: [email protected]
参考
[1] https://twitter.com/defiprime/status/1422708265423556611



