2020 年夏天, SushiSwap 创始人 Chef Nomi 所发布的 MasterChef 智能合约可能是最早一波 DeFi 狂潮中被重新部署最多次的合约。很多的 DeFi 项目会略为修改 MasterChef 合约来实现他们流动性挖矿的功能。然而,过去一年多的时间内,有几个项目在修改 Nomi 主厨「食谱」的过程中犯了错误,造成无法挽救的损失 。2021 年 10 月 6 日,我们也发现了一个失误的项目,所幸在还没造成损失之前,该项目就成功修复了漏洞,并且完成升级。

0x00: Dinosaur Eggs

这次事件的主角Dinosaur Eggs 项目的「流动性资金池」(Liquidity Pool)智能合约也是一个「加强版」的 MasterChef 合约,其主要修改的部分是新增了「addtionalRate」功能。这个功能是为了让特定的NFT 持有者在存入「流动性提供者代币」(LP Tokens)时可以获得额外的奖励,最高可达10%,条件是存入LP Tokens 之前必须「销毁」特定的NFT 资产。

从上面的程序代码片段327-328 行可以看到(_amount*user.addtionalRate) 会被加到user.addtionalAmount 里头,而这个addtionalAmount 会在计算奖励时被加入计算。

0x01: 漏洞细节

漏洞出现在从MasterChef 继承而来的紧急逃生出口— emergencyWithdraw() 函数。这个函数是用来让user 可以在紧急情况把所有存入的LP tokens 一次取出来,不考虑奖励计算。然而,前面提到的user.addtionalAmount 在这个函数里没有被reset,也就是说下一次的harvest() call 会出现user 没有存入任何LP tokens 的状态下仍然可以领取奖励的情况。

从harvest() 函数的第342 行可以看到pendingAmount 的计算是[(user.amount+user.additionalAmount)*pool.accRewardPerShare – user.rewardDebt]。由于之前的emergencyWithdraw() 已经reset 了user.amount 以及user.rewardDebt,pendingAmount 就变成了(user.addtionalAmount*pool.accRewardPerShare)。因此,攻击者可以在没有任何LP tokens 存入的状态下,不停的来回利用harvest() 跟emergencyWithdraw() 把所有的reward tokens 取出。

0x02: 漏洞利用

上面的攻击合约程序代码验证了上述的想法,在prepare() 函数里,我们刻意铸造了一个NFT(第36 行)并且通过第40 行的additionalNft() 函数启动了前面提到的addtionalRate 机制,随后我们deposit() 了一部分LP tokens 到LiquidityPool。为了获得额外的奖励,我们在trigger() 函数里利用回圈多次调用emergencyWithdraw() 与harvest() 函数(第48-51 行)。

从上面的eth-brownie 截图可以看到,我们只用了30 个LP tokens (DsgLP) 就能获取上千的reward tokens (DSG)。在同样的情况如果只是单纯的harvest() 没有刻意emergencyWithdraw(),只有不到一个的DSG 奖励。假设攻击者利用闪电贷款生成大量LP tokens,还能更进一步扩大获利。

0x04: 后续发展

在我们向DSG 团队通报这个漏洞之后,他们很快的确认问题并且展开了修补工作。新版的LiquidityPool 合约部署上线后,DSG 团队通知了用户将资产从旧版取出后迁移到新版,同时也将旧版的流动性挖矿暂停。幸运的是,在完成迁移之前,并没有真正的攻击发生,DSG 团队也根据漏洞赏金计画给了我们$10k 等值的DSG tokens奖励。这部分奖金随后被用来捐助开放文化基金会,支持开源软件发展。