2021年8月4日,Popsicle Finance 在一次攻击中遭受了巨额财务损失(超过2000万美元)[1]。 经过手动分析,我们确认这是一次“重复领取”攻击,即其奖励系统中的一个漏洞允许攻击者反复领取奖励。 在下文中,我们将使用一次攻击交易来阐述攻击过程和漏洞的根本原因。
背景
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借入大量流动性;
- 发动了存款-提款-领取奖励循环来执行攻击(总共有8个循环,大量流动性已从Popsicle Finance的多个金库中提取);
- 将闪电贷归还给AAVE,并通过Tornado.Cash洗钱。

具体来说,存款-提款-领取奖励循环由几个步骤组成,使用我们的在线工具 [2]可以轻松标记和清晰总结:

利润分析
总计,攻击者从Popsicle Finance中攫取了2000万美元,包括2.56K WETH、96.2 WBTC、160K DAI、5.39M USDC、4.98M 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



