去中心化金融 (DeFi) 作为区块链生态当红项目形态,其安全尤为重要。从去年至今,发生了几十起安全事件。
BlockSec 作为长期关注 DeFi 安全的研究团队 (https://blocksecteam.com),独立发现了多起 DeFi 安全事件,研究成果发布在顶级安全会议中(包括 USENIX Security, CCS 和 Blackhat)。在接下来的一段时间里,我们将系统性分析 DeFi 安全事件,剖析安全事件背后的根本原因。
往期回顾:
(1)[BlockSec DeFi 攻击分析系列之一] 我为自己代言:ChainSwap 攻击事件分析
(2)[BlockSec DeFi 攻击分析系列之二] 倾囊相送:Sushiswap 手续费被盗
(3)[BlockSec DeFi 攻击分析系列之三] 偷天换日:深度剖析 Akropolis 攻击事件
0xffffffff. 前言
北京时间 2021 年 07 月 21 日 03:40,我们的攻击检测系统检测到某个交易异常。通过对该交易进行扩展分析,我们发现这是一起利用通缩代币(deflation token) KEANU 的机制对 Sanshu Inu 部署的 Memestake 合约的奖励计算机制的漏洞进行攻击的事件,攻击者最后获利 ETH 约 56 个。下面详细分析如下:
阅读建议:
如果您刚刚接触 DeFi (Ethereum),可以从头看器,但是文章比较长,看不下去记得点个关注再走。
如果您对 Akropolis 等 DeFi 聚合器项目比较了解,可以直接从「0x2 攻击分析」开始。
0x0. 背景介绍
今年以来爆火的狗狗币(DOGE)、柴犬币(SHIB)引发了广泛的关注,同时带火了其他相关的 meme 币,更引发了大量的项目方开发自己的 meme 币及围绕 meme 币提供服务,其中 Sanshu Inu 就是其中一员。Sanshu Inu 不仅发行 meme 币 SANSHU,还创建了合约 Memestake 作为 meme 币的耕种池。用户只要往 Memestake 中质押 meme 币,就可以获得代币 Mfund 作为奖励。
另一方面,大量的 meme 币都是通缩代币,即该种代币的发行量会逐渐减少。其中部分 meme 币的通缩实现形式是在用户每次进行交易转账(transfer)的时候扣取一定比例的币用于销毁和再分配,这将导致接收方实际收到的 token 数量小于支出方实际支付的数量。本次涉及的通缩代币 KEANU 就是采用这种实现。
该攻击的大致原理就是通过控制 Memestake 进行 KEANU 的多次转入转出减少其持有的 KEANU 数量,从而利用其奖励计算函数的漏洞致使 Memestake 给攻击者发送大量 Mfund。
0x1. 代码分析
为了便于理解,我们首先简要地介绍一下和此次攻击相关的两个实体合约:KEANU 代币的 KeanuInu 合约和 Memestake 合约。
KeanuInu 合约
正如前面所说,KeanuInu 在实现代币 KEANU 的转账时,会扣取一定比例的币用于销毁和再分配,其中用于销毁的比例设置为定值——2%。如图,在调用 KeanuInu 的 transfer() 及 transferFrom() 函数的时候,函数调用中显示的转账数量跟 emit 的事件 log 中记录的数量并不一致。
其由于其实际代码实现调用较为复杂,此处不再展示,感兴趣的朋友可以根据后面附录给出的合约地址在 etherscan.io 自行查看合约实现。另外,上面两张截图来源于我们自行开发的交易解析工具,目前开放公测中。欢迎各位点击 http://tx.blocksecteam.com:8080/ 试用。我们的工具将函数调用与过程中产生事件 log 结合展示的方式,对于分析通缩代币等问题更有帮助。
Memestake 合约
下图是 MemeStake 的 deposit 函数。函数首先调用 updatePool 更新资金池状态,然后将用户的 token 转账给自己。当传入的_amount 大于 0 时会在代码的 1295 行进行转账。
然而,由于 KEANU token 的通缩特性,虽然调用 safeTransferFrom 函数时传入的金额是_amount,但是实际上转入资金池的金额小于_amount。并且在代码分析中我们注意到,transfer 的目的地是自己,也就是说对于 MemeStake 来说,所有用户的某个币种(如 KEANU token)的存款都属于 MemeStake。
在转账后的 1296 行,MemeStake 会对用户的存款进行登记,但这里登记采用的仍然是_amount (而真实的转账量小于_amount),因此用户真正的存款量比登记的 user.amount 更小。
最后在 1299 行,可以看出 user.rewardDebt 参数也是根据(比真实值要大的) user.amount 来计算的。
下图是 MemeStake 的 withdraw 函数。该函数首先会检查 user.amount 是否还有足够的余额,但由于 user.amount 本身比真实值大,因此这里的检查是不准确的。接下来,同样会调用 updatePool 函数更新资金池状态。
在 1321 行,withdraw 函数会先扣除在 user.amount 中登记的余额,然后调用 transfer 函数把 token 转回用户。和 deposit 函数一样,这里的逻辑同样存在问题,由于每次转账都会造成通缩,因此转给用户的数量会小于实际的转账量。
最后来看 MemeStake 的 updatePool 函数。首先从 1255 行可以看出,每次调用会记录上一次更新的 blockNumber,如果此次调用的区块和上次更新时相同,则会直接返回,也就是说 updatePool 对每个区块只会更新一次资金池状态。
接下来在 1259 行,会获取 MemeStake 自身在 token 合约中的余额(上文提到,每次用户 deposit 都会将 token 转给 MemeStake)。最后在 1275 行,会利用这个余额作为分母,计算该资金池每一次 deposit 和 withdraw 的奖励(也就是 pool.accMfundPerShare 参数)。计算方式如下:
pool.accMfundPerShare += mFundReward / token.balanceOf(MemeStake)
回到 withdraw,我们来看存取款奖励代币 Mfund 是怎样转账的。首先在上图 withdraw 函数的第 1325 行,计算用户是否有 pending 的 Mfund token 没有发放,计算公式为:
rewardMfund = user.amont * pool.accMfundPerShare / 1e18 - user.rewardDebt。
而 rewardDebt 是这样计算的(图中第 1325 行):
user.rewardDebt = user.amount * pool.accMfundPerShare / 1e18
因此,从代码中我们不难构造出一种可能的攻击:
首先,在一个交易内,通过反复调用 deposit 和 withdraw 函数,榨干 MemeStake 的资金池。这个操作利用了三个代码问题:
首先,user.amount 的记账比真实值多,因此每次 withdraw 都可以成功。
第二,MemeStake 中所有用户的资金都在一个池子中,因此每一笔转账实际上 Burn 掉的是池子中其他用户存入的 KEANU token。
第三,由于 updatePool 在同一个块中不会进行状态更新,因此不会影响 pool.accMfundPerShare 参数,也不会产生 Mfund token 的 reward。
接下来,在下一个区块时,直接调用 withdraw 函数。
通过对 updatePool 函数的分析可知,此时会产生池子状态的更新,且由于前一步操作榨干了 MemeStake 的资金池,token.balanceOf(MemeStake) 极低,产生了巨大的 pool.accMfundPerShare。
随后在 withdraw 函数的第 1315 行,计算出的 Mfund reward 量非常大,导致巨额的 Mfund 回报。
0x2. 攻击分析
前面介绍了漏洞成因及漏洞的利用方式,我们接下来介绍攻击者实际是如何进行攻击的。
如图所示,攻击可以分为 4 步,其中关键攻击步骤为第 2 步,利用通缩代币的特性操纵 Memestake 的奖励计算。
第 1 步(准备),首先攻击者创建了两个合约并进行初始化,其中 合约一 为表现正常的投资合约,攻击者通过合约一往 Memestake 存入约 2,049B KEANU ,为步骤 3 获利大量 MFUND 奖励做好铺垫。合约二 为操纵 Memestake 的奖励计算的合约,先进行了相关 token 的 approve 操作。
第 2 步(操纵),攻击者先从 uniswapV2 中 flash loan 大量的 KEANU 代币,然后通过合约二往 Memestake 中多次 deposit 和 withdraw 大量 KEANU,导致 Memestake 被迫大量交易 KEANU。 由于 KEANU 是一种通缩代币,每次交易会烧掉 2% 的交易额,导致用户真正存入 Memestake 的量比登记的 user.amount 更小,取出时又是按照 user.amount 转给用户(详见代码分析),导致 Memestake 池子中 KEANU 的代币持有量不断减少,最终为 1e-07。如下图所示,涉及交易为 0x00ed,交易截图未完全,请自行点击链接查看。
第 3 步(获利),攻击者首先通过合约二调用了 Memestake.updatePool() 函数,修改了 KEANU 所在池子的 accMfundPerShare,由于该值依赖于池子所持有的 KEANU 的代币量,而这在第二步中被操纵了(具体公式见下方代码分析)。这使得合约二在接下来 withdraw 的时候可以获得远超正常值的 Mfund(约 61M)这种 token 作为奖励。第 3 步发生于交易 0xa945 中,同时攻击者开始将部分获得的 MFund 换成 WETH 等代币。
第 4 步(收尾),攻击者将获得的 MFund、KEANU 等代币换成 ETH,并通过 Tornado.Cash 转移走,至此攻击结束,攻击者从中获利 ETH 55.9484578158357 个(攻击者的 EOA 地址及部署的攻击合约还残留有部分 SANSHU 和 KEANU 代币未计入),约 10 万美元。
下图为攻击地址 0x0333 的交易截图,交易截图未完全,请点击地址链接查看详情。
攻击相关
有趣的是,攻击的第 2、3 步都与 flashbots 交易有关。
其中第 2 步涉及的交易 0x00ed 由于采用了 UniswapV2 flashloan,且交易前后相当于用约 38ETH 去购买了 KEANU,由此产生了很大的套利空间。因此该笔交易受到另一名攻击者的三明治攻击(sandwich attack),即本事件的攻击者也是另一个三明治事件的受害者。该三明治攻击者获利 3.2769697165652474ETH,但是给了矿工 2.405295771958891249ETH,净获利 0.8716739446063562ETH。
而第 3 步攻击涉及的交易 0xa945 则由于在 uniswap 池子中大量售出 MFund,创造了套利空间,所以被 back-running 而成为 flashbots 交易。该 searcher 获利 0.13858054192600666ETH,其中交给矿工 0.099085087477094764ETH,净获利 0.03949545444891189ETH。
有关 flashbots 和三明治攻击的详细介绍可参阅我们的另一篇攻击介绍 由 xSNXa 被攻击事件引发的对 FlashBot 的思考 。由于 UniswapV2 中将 flash loan 的实现与普通的 Swap 结合在一起,具体的实现原理及为什么由此导致第 2 步存在套利空间可以参阅我们的 paper Towards A First Step to Understand Flash Loan and Its Applications in DeFi Ecosystem (SBC 2021).
0x3. 总结及安全建议
攻击者利用通缩代币的特性控制了平台持有代币的数量,影响了奖励代币的计算发放,由此获利 ETH 55.9484578158357 个。而这原因在于,Sanshu Inu 平台在引入通缩代币时缺乏一定的安全考量,导致攻击者有空可趁。
由此,我们给有关项目方的安全建议有:
对项目引入的代币应当有足够的认识,或者通过建立白名单的机制对交互的 token 进行限制。近两年来,已经有多起安全事件与未加限制的 token 或者交互的 token 有问题有关,如最近的 BSC 链上的 Impossible Finance 事件,我们这个系列上一篇 Akropolis 攻击事件,2020-11-17 的 Origin Dollar 事件及 2020-06-28 的 Balancer 事件等。特别是与通缩代币的交互,如在该事件发生不久后,PeckShield 也报告了一起发生在 polygon 链上同样利用通缩代币及奖励计算漏洞的安全事件——PolyYeld 事件。
项目上线前,需要找有资质的安全公司进行安全审计。我们可以看到,由于 defi 的 money lego 属性,很多 defi 项目之间可以随意组合,从而产生了互相影响,而这正是 defi 领域安全事件频发的原因。因此,项目方所需关注的安全问题不仅仅局限于自己项目,也同样需要考虑在与其他项目交互过程中存在的安全漏洞。
BlockSec 团队以核心安全技术驱动,长期关注 DeFi 安全、数字货币反洗钱和基于隐私计算的数字资产存管,为 DApp 项目方提供合约安全和数字资产安全服务。团队发表 20 多篇顶级安全学术论文 (CCS, USENIX Security, SP),合伙人获得 AMiner 全球最具影响力的安全和隐私学者称号 (2011-2020 排名全球第六). 研究成果获得中央电视台、新华社和海外媒体的报道。独立发现数十个 DeFi 安全漏洞和威胁,获得 2019 年美国美国国立卫生研究院隐私计算比赛 (SGX 赛道) 全球第一名。团队以技术驱动,秉持开放共赢理念,与社区伙伴携手共建安全 DeFi 生态。