去中心化金融 (DeFi) 作为区块链生态当红项目形态,其安全尤为重要。从去年至今,发生了几十起安全事件
BlockSec 作为长期关注 DeFi 安全的研究团队 (
https://blocksecteam.com),独立发现了多起 DeFi 安全事件,研究成果发布在顶级安全会议中(包括 USENIX Security, CCS 和 Blackhat)。在接下来的一段时间里,我们将系统性分析 DeFi 安全事件,剖析安全事件背后的根本原因
往期回顾
今天带来第二期:本文简述了一个老实的日本寿司店收银员,因为过于相信他人,被骗走攒了很久的血汗钱的真实故事
时间:Nov-28-2020 05:41:10 AM +UTC #11345179
阅读建议:
- 如果您刚刚接触 DeFi,可以从头开始看起,但是文章较长,看不下去记得点个关注再走
- 如果您对 Sushiswap、AMM、DEX 等比较了解,可以直接从「0 x1 攻击分析」开始
0 x0. 背景介绍
Uniswap 走过最长的路就是 Sushiswap 的套路(不是
Sushiswap 由 Uniswap 分叉而来 [注 1],自诩为社区版 Uniswap[注 2]
【注 1】"Taking Uniswap‘s elegant core design, we‘ve added community-oriented features that we believe help improve the design of the protocol, as well as provide further benefits to the actors involved."
【注 2】"It‘s not just a fork. LPs are take care of. It‘s not just a 0.3% fee going to them anymore."
为什么叫社区版呢?因为 Sushiswap 在 Uniswap 的基础上添加了 SUSHI 这一平台代币
【注】SUSHI 是 Sushiswap 的平台代币,可以和 USDC、ETH 一样在二级市场交易,同时也具有治理的功能(拥有 SUSHI 可以参与 Sushiswap 一些决策的投票)
原本 Uniswap 的交易手续费是 0.3% ,也就是在 Uniswap 上每次 swap 代币,都需要预先扣除卖出代币的 0.3% 流入交易池,使得流动性提供者所能换取的底层代币(Underlying token)更多
而 Sushiswap 将此交易费划分为两部分:其中 0.25% 和原来一样,作为交易的手续费流入交易池,另外的 0.05% 用来回购 SUSHI (这里注意,一会要考的)
① 在一切开始之前,我们先简单回顾一下 AMM 的原理:
AMM 是实现去中心化交易所 (DEX) 的一种技术
作为交易所,最核心的功能当然就是交换(swap)代币,而代币本质上就是存储在区块链上的数字,不同的代币是不同的变元。只要寻找到一条合适的曲线,可以表示出这种此消彼长的关系(比如 Uniswap 的恒定乘积),就可以通过程序来实现代币的 " 自动交换 "
从合约层面来看,我们可以通过 Uniswap 的 Factory 合约创建出不同交易对合约(Pari, 交易池)。一个交易池对应着一对 token,人们可以找到合适的交易池来交换代币(swap),交易池中的底层代币是流动性提供者(Liquidity provider)存入的。作为凭证,交易池会给流动性提供者发送 " 交易池代币 "(LP token),LP token 同时也代表占有池中资产的份额
这里要注意的是,每个交易对合约都会继承一个 ERC20 代币合约,所以交易对合约本身就是 LP token,或者我们可以从 OOP 的角度来看,交易池只是一种扩展版的 ERC20,这种 ERC20 代币我们统称为 LP token,它不仅具有普通的 mint, send, burn 功能,还有 : LP.addLiquidity(token0, token1), LP.swap(token0) → token1 这些额外的接口
【注】Sushiswap 的 LP token 一般被称作 SLP
交易池中两种 token 的价格如何确定?有一种简单的方法,就是看比值。比如一个池中 ETH:USDT = 1:2000,那我们就可以简单的认为一个 ETH 能换出 2000 个 USDT,池中 ETH 更少,更值钱。有个比较专业的词叫做 spot price,2000 USDT/ETH 就是 ETH 在该池中的 spot price
但是真的如此吗?对于 AMM 类型的 DEX,比如 Uniswap 中有一个 DAI/USDT 的交易池,池中的储备量为:100DAI + 100USDT,DAI 在池中的 spot price 是 1 USDT/DAI ,但是,这时我们用 100 DAI 只能从池中换出 50 个 USDT,实际价格(effective price)为 0.5 USDT/DAI。我们把这部分的差就叫做滑点(slippage)
通常会做归一化处理即:slippage = (effective price - spot price) / spot price,上面例子中 slippage = ( 1 - 0.5) / 1 = 0.5 = 50%
那有什么办法可以使滑点小一些吗?毕竟没有人愿意吃亏。我们再看一个例子:还是 USDT/DAI 池,池中的储备量为:1000DAI + 1000USDT,可以发现:这时用 100DAI 能够换出的 USDT 数量为:90.90 个,明显可以看出这次 effective price 就更接近 spot price,而此时的滑点为:(1 - 0.909) / 1 = 0.091 = 9.1%。
原因就在于池子的储备量大了,这便是交易池的深度因此我们可以得出结论:底层代币的储备量越大(深度越大),价格差异越小(滑点越低)
看到这里,聪明的同学可能就要问了 :
②为什么 Sushiswap 给的手续费少了,还有人愿意给 Sushiswap 提供流动性呢(流动性挖矿介绍)?
向 Sushiswap 提供流动性虽然获得的手续费减少了 0.5% ,但是 Sushiswap 额外提供了一个功能:流动性挖矿 (Yield Farming)
这个词听起来时髦的很,其实本质上就是 " 存币生币 "(种币得币 [注 1]),和现实中把钱存银行吃利息类似
Sushiswap 允许流动性提供者将手里的 LP token 存到 Sushiswap (SushiChef 的 deposit 方法),而 Sushiswap 每个区块会铸出一定数量的 SUSHI (最初的 100,000 个区块,每个区块释放 100 x 10 = 1,000 枚 SUSHI,往后每个块释放 100 个)这些将 LP token 存到 Sushiswap 的人会获得相应数量的 SUSHI (利息)
【注 1】:流动性挖矿的英文名 Yield Farming,直译过来就是收益耕种,还是很生动展示了 " 币农 " 这一形象
【思考题】同样是 3%,一个是全给你,另一个先扣你点,我统一收上来再分配,为啥后面这个就更香?
但是,问题又来了:利息得是真的钱啊,如果有人和你说你把钱存我这里,我每个月会给你发一张白纸,你会存吗?应该不会吧。所以现在 Sushiswap 要做的就是把 SUSHI 的价格 " 炒高 ":
③SUSHI 代币价值的来源?
还记得之前我们提到的回购 SUSHI 吗?SUSHI 之所以具有价值,就在于这个回购 SUSHI。我们知道一个东西,没人买的话,就算喊到天价也一分钱不值。问题是谁来买 SUSHI?Sushiswap 说「没人买,我来买啊」
那 Sushiswap 用什么买,怎么买?
④ Sushiswap 如何回购 SUSHI (convert 函数的原理)?
还记得 Sushiswap 将 0.3% 的手续费拆成 0.25%+0.05% 吧?收的这 0.05% 就用来买 SUSHI
交易的手续费是预先扣除的,最终反应在 LP token 能从池中换出的底层代币数量(交易者手续费在池中的囤积越多,用一个 LP token 能换出的底层代币就越多)
而每当有人调用 mint (添加流动性)或者 burn (撤出流动性)时,相应的 LP token (0.05%)就会发送给 feeTo,对于 Uniswap 这个 feeTo 尚未设置,而Sushiswap 将这个 feeTo 设置为了 SushiMaker 合约
SushiMaker 这个合约既负责管理从各个池子上收来的 LP token,同时也负责将这些 LP token 换成 SUSHI,以实现回购 SUSHI
而实现将这些 LP token 转换为 SUSHI 的方法,就在于合约中的一个 " 按钮 ",convert(token0, token1) 函数,任何人都可以按这个按钮
按下后,它会先通过 token0 和 token1 获取到相应的 LP token (也就是交易对的地址),然后将自己拥有的 LP token 烧掉换成底层的两种 token
最后将这两种 token 分别换成 SUSHI:先是分别将两种 token 换成 wETH (通过:token0/wETH,token1/wETH 交易对)[注 4],再将所有的 wETH 换成 SUSHI (通过:wETH/SUSHI 交易对)[注 5](最终换得的 SUSHI 转给 SushiBar 合约,这部分超纲了,和本次攻击无关,就不展开了)
【注 4】因为会先都换成 wETH,所以对于 token 是 wETH 本身,就可以跳过这一步
【注 5】因为最终的目的是换成 SUSHI,所以对于 token 是 SUSHI 本身,就可以跳过这一步
0 x1. 攻击分析
本次事件中,攻击者盯上的便是 Sushiswap管理手续费的合约 SushiMaker,下面我们来看看这小子到底干了什么吧:
1.1 基本原理:
上面已经解释了 convert(token0, token1) 函数的原理,并且提到,这个按钮任何人(仅限 EOA)都可以调用。看起来 convert 函数和调用它的人半点关系都没有(只是替 Sushiswap 将 SushiMaker 中存的手续费换成 SUSHI),Sushiswap 的管理员可以定期去调用,闲着没事的好心人也可以帮忙调用
但我们要硬扯上关系的话,其实还是可以沾上点的!
回忆 convert 函数执行过程,有一步是 SushiMaker 将 token0、token1 分别到 token0/wETH、token1/wETH 中去换 wETH,如果这两个交易池是健康的(比如 : USDT、USDC…),确实沾不到什么关系
但这两个交易池有没有可能是恶意的呢(比如:被攻击者操控或劫持),那就是另一个故事了
比方说,token0/wETH 池子是攻击者创建的,滑点奇高。其中的价格非常离谱,token0:wETH 本来是 1:1,但是池中是 300:1,或者是深度极低。我们知道 convert 是要用 token0 来买 wETH,这一步中 SushiMaker 就会血亏,用远低于市场的价格卖出了 token0,但是合约自己是不知道的(没有做相应的检查)
接下来,攻击者只要在这个池中在做一笔反向的交易(用 wETH 买 token),就可以把 SushiMaker 亏的钱弄到自己口袋里了
既然要攻击 SushiMaker 合约,首先要分析能偷到哪些钱?
SushiMaker 中存放着从各个池中收来的 SLP,而这些SLP 可以直接到相应的交易池去提取底层代币:
1.3 代码漏洞
convert 函数:
![]()
_toWETH 函数:
![]()
看出什么亮点了吗?SushiMaker里的转账逻辑都是:transfer(balanceOf(this))
我们可以分两个阶段来看 convert 的调用过程:
第一个阶段:在convert函数中 SushiMaker 拥有的是 SLP,它通过 burn balanceOf(this) 实现将全部 SLP 换成两个底层代币
第二个阶段:SushiMaker 获得了 burn 来的 底层代币,再拿 burn 得到的 token 去不同的交易池换 wETH(提示:这句话是错的!)
你发现哪里有问题了吗?我们来看第二阶段的代码 _toWETH:注意兑换的逻辑为:
![]()
所以这个 balanceOf(address(this)) 真的只有在第一阶段 burn 来的 底层代币 吗?
Nononono~
想一想,如果这里的 token 并不是底层代币,而是一个 SLP,会发生什么?
IERC20(SLP).balanceOf(address(this))是 SushiMaker 拥有的所有 SLP,既包含刚刚 burn 出来的,也包含原本 SushiMaker 就有的(从相应的交易池中收取,积攒的手续费)
如果底层代币是 SLP,那第一阶段 burn 的是什么?答案是SLP 的 SLP!
还记得吗?攻击者的目标是将 SushiMaker 诱骗到其创建的恶意交易池中交易,这个 SLP1/WETH 池不恰好是攻击者创建的,当攻击者调用 convert(SLP1, wETH) 时,SushiMaker 由于上面这一漏洞(我们暂且称为资产隔离问题)会将其全部的 SLP1 都到 SLP1/WETH 池中换取 WETH —— 正中下怀
【注】SLP/WETH 池一般都是不存在的,或是深度很浅的。攻击者其实也可以通过建立 SLP1/SLP2 来实现攻击,但是这样的话,还需要单独为 SLP1、SLP2 建立 SLP1/WETH,SLP2/WETH,不如直接一步到位
1.4 Real World
管你看没看懂,继续看就完了
下面我们来看看攻击者在真实世界中到底做了什么吧?(要记得,攻击者的目的是将 SushiMaker 骗到他劫持的恶意交易对中)
一组攻击包括很多笔交易,这里以对MKR/WETH池的攻击为例:
- Swap Exact ETH for Tokens: 0 xa8c4edd85727d
- Approve:0 xf1fdd4cf4d8aa
- Add Liquidity ETH: 0 x7340edca1a17f
- Approve: 0 x41a33f0c91b7c
- Add Liquidity ETH: 0 x896f412f15a7a
- Transfer: 0 x0e8a76bf7295d
- Convert: 0 x4947b4f075f8e
- Swap Exact ETH For Tokens: 0 x5f37bb3b97341
- Remote Liquidity ETH: 0 xdff10159275e0
- Swap Exact Tokens For ETH: 0 xb8889bbdeb478
攻击者的一个完整的攻击周期:
序章:攻击准备
Step 1:通过 swapExactETHForTokens 用一些 ETH 换取 MKR ,作为攻击的启动资金
Step 2:将刚刚换到的 MKR 和 ETH 一起,向 Sushi 的 MKR/wETH 交易池中添加流动性,获得该池的 SLP(MKR/wETH)(后简称为 SLP)
Step 3:将刚刚换到的 SLP 和 ETH 一起,向 Sushi 的 SLP/wETH 交易池中添加流动性(如果不存在该池,自动创建一个),获得新池的 SLP(SLP/wETH) (后简称 SLP‘)
Step 4:将新生成的 SLP‘ 转给 SushiMaker
【解释】这步有什么意义呢?因为攻击者后续会调用 convert(SLP, wETH),将 SushiMaker.sol 合约中存放的 SLP‘ 燃烧掉,换出 SLP, wETH。如果 SushiMaker.sol 中没有 SLP‘ 就没法进行下去了
但是,实际复现过程中,我发现有没有这步其实都影响不大。因为上一步 addLiquidity(SLP, wETH) 是,一方面会 mint 出 SLP‘ 给攻击者(0.25% 的手续费),另一方面还会 mint 出一些 SLP‘ 给 SushiMaker.sol (0.05% 的手续费),所以 SushiMaker.sol 中其实还是有一点 SLP‘ 的
→ 现在攻击者手里有 SLP‘,其实他有两个不同的决策:
1. 将 SLP‘ 转给 SushiMaker,这样 SushiMaker 可以换出更多的 SLP 和 wETH,下一步回到这个交易池将自己所有的 SLP (主要是从 MKR/wETH 池中收的手续费,一小部分是 convert 第一阶段中用 SLP‘ 换出来的)换成 wETH
2. 不转 SLP‘ 给 SushiMaker,因为之前创建 SLP/wETH 这个池的时候会 mint 一点点 SLP‘ 给 SushiMaker,所以还是可以通过调用 convert 将 SushiMaker 存的 SLP 都换成 wETH
第一种方式中,使 SushiMaker 拥有的 SLP‘ 更多,会换出更多的 SLP 和 wETH,使得池中的滑点更大,SushiMaker 能亏的 SLP 就更多,但是如果 SushiMaker 中本身存的 SLP 就不多,SLP/wETH 深度也不大时,其实结果差不太多,都是几乎将 SushiMaker 搬空。
总而言之,还是第一种方式更优,可以看出攻击者其实是有精心考虑过的
高潮:攻击 SushiMaker
Step 5:调用 convert(SLP, wETH),这一步中,SushiMaker 会先将 SLP‘ burn 掉,换成 SLP 和 wETH,再将自己所有的 SLP 都转成 wETH (包括之前从 MKR/wETH 池中收的 SLP 手续费),问题在于SLP/wETH 这个池是由攻击者创建的(已被劫持),池中的深度非常小,滑点极高,SushiMaker 通过swap 将 SLP 换成 wETH 这步会血亏(大量的 SLP 只换出了一点点 wETH )
尾声:获利
Step 6:上一步中,SushiMaker 在 SLP/wETH 这个池中血亏了大量的 SLP,池中 wETH 巨值钱,此时攻击者只需要调用 swap 用一点点 wETH 就可以几乎将整个池子搬空(换出全部的 SLP,SLP 是真的钱,可以到 SLP(MKR/WETH) 池中提现的)
0 x2 附录
2.1 本次事件相关的攻击交易:
后续攻击者又尝试攻击 Sushiswap 的其他相关仿盘项目,如 LuaSwap:
![]()
但可能获利不多,只成功几笔就不了了之了
2.2 本次事件后续结果:
在 #11351530 块,Sushi 的管理员向攻击者喊话:
可以看到,好像有些延迟,攻击者的攻击依然持续了一段时间:
最后一条 Convert 中:
SushiMaker 地址从 0 x6684977bBED67 变成了 0 x280ac711bb99d:
![]()
可以看到区别在于:代码第 26 行_toWETH 限制了 amount,这样修改以后就不会将 SushiMaker 中存的全部 SLP 都去池中换成 wETH,而只是换取 burn 出的一部分
0 x3. 参考
BlockSec 团队以核心安全技术驱动,长期关注 DeFi 安全、数字货币反洗钱和基于隐私计算的数字资产存管,为 DApp 项目方提供合约安全和数字资产安全服务。团队发表 20 多篇顶级安全学术论文 (CCS, USENIX Security, S&P),合伙人获得 AMiner 全球最具影响力的安全和隐私学者称号 (2011-2020 排名全球第六). 研究成果获得中央电视台、新华社和海外媒体的报道。独立发现数十个 DeFi 安全漏洞和威胁,获得 2019 年美国美国国立卫生研究院隐私计算比赛 (SGX 赛道) 全球第一名。团队以技术驱动,秉持开放共赢理念,与社区伙伴携手共建安全 DeFi 生态。