BlockSec DeFi攻击分析系列之二倾囊相送:Sushiswap手续费被盗

avatar
BlockSec
3年前
本文约6844字,阅读全文需要约9分钟
一个老实的日本寿司店收银员,因为过于相信他人,被骗走攒了很久的血汗钱的真实故事

去中心化金融 (DeFi) 作为区块链生态当红项目形态,其安全尤为重要。从去年至今,发生了几十起安全事件

BlockSec 作为长期关注 DeFi 安全的研究团队 (https://blocksecteam.com),独立发现了多起 DeFi 安全事件,研究成果发布在顶级安全会议中(包括 USENIX Security, CCS 和 Blackhat)。在接下来的一段时间里,我们将系统性分析 DeFi 安全事件,剖析安全事件背后的根本原因

往期回顾:
(1) [BlockSec DeFi 攻击分析系列之一] 我为自己代言:ChainSwap 攻击事件分析

今天给大家带来第二期:
本文简述了一个老实的日本寿司店收银员,因为过于相信他人,被骗走攒了很久的血汗钱的真实故事

时间:Nov-28-2020 05:41:10 AM +UTC #11345179

BlockSec DeFi攻击分析系列之二倾囊相送:Sushiswap手续费被盗

阅读建议:

  • 如果您刚刚接触 DeFi,可以从头开始看起,但是文章较长,看不下去记得点个关注再走

  • 如果您对 Sushiswap、AMM、DEX 等比较了解,可以直接从「0x1 攻击分析」开始

0x0. 背景介绍

Uniswap 走过最长的路就是 Sushiswap 的套路(不是

Sushiswap 由 Uniswap 分叉而来 [注 1],自诩为社区版 Uniswap[注 2]

【注 1】Taking Uniswaps elegant core design, weve 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】Its not just a fork. LPs are take care of. Its not just a 0.3% fee going to them anymore.

为什么叫社区版呢?因为 Sushiswap 在 Uniswap 的基础上添加了 SUSHI 这一平台代币

【注】SUSHI 是 Sushiswap 的平台代币,可以和 USDCETH 一样在二级市场交易,同时也具有治理的功能(拥有 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 能换出的底层代币就越多)

BlockSec DeFi攻击分析系列之二倾囊相送:Sushiswap手续费被盗

而每当有人调用 mint (添加流动性)或者 burn (撤出流动性)时,相应的 LP token (0.05%)就会发送给 feeTo,对于 Uniswap 这个 feeTo 尚未设置,而 Sushiswap 将这个 feeTo 设置为了 SushiMaker 合约

BlockSec DeFi攻击分析系列之二倾囊相送:Sushiswap手续费被盗

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 本身,就可以跳过这一步

BlockSec DeFi攻击分析系列之二倾囊相送:Sushiswap手续费被盗

0x1. 攻击分析

本次事件中,攻击者盯上的便是 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 亏的钱弄到自己口袋里了

1.2 资产分析:

既然要攻击 SushiMaker 合约,首先要分析能偷到哪些钱?

SushiMaker 中存放着从各个池中收来的 SLP,而这些 SLP 可以直接到相应的交易池去提取底层代币

BlockSec DeFi攻击分析系列之二倾囊相送:Sushiswap手续费被盗

1.3 代码漏洞:

convert 函数:

function convert(address token0, address token1) public {
   // At least we try to make front-running harder to do.
   // 限制只能 EOA 来调用该函数
   require(msg.sender == tx.origin, do not convert from contract);
   // 获取相应 SLP token 合约的地址(即:token0/token1 交易对合约地址)
   IUniswapV2Pair pair = IUniswapV2Pair(factory.getPair(token0, token1));
   // 将 SushiMaker 中存有的全部 SLP,转给交易池
   pair.transfer(address(pair), pair.balanceOf(address(this)));
   // Burn 掉这些 SLP 换成两种 token
   pair.burn(address(this));
   // 分别将两种 token 找到相应的和 wETH 的交易对,全部转换为 wETH
   uint256 wethAmount =_toWETH(token0) +_toWETH(token1);
   // 将转换的到的全部 wETH 找 wETH/SUSHI 交易对换成 SUSHI_toSUSHI(wethAmount);}

_toWETH 函数:

function_toWETH(address token) internal returns (uint256) {
   // 对于 SUSHI,直接 Sushimaker 全部 SUSHI 转给 Bar
   if (token == sushi) {
       uint amount = IERC20(token).balanceOf(address(this));_safeTransfer(token, bar, amount);
       return 0;
   }
   // 对于 WETH,直接将全部 WETH 转给 WETH/SUSHI 交易对,swap 出相应的 SUSHI
   if (token == weth) {
       uint amount = IERC20(token).balanceOf(address(this));_safeTransfer(token, factory.getPair(weth, sushi), amount);
       return amount;
   }
   // 获取 token/WETH 的交易对地址
   IUniswapV2Pair pair = IUniswapV2Pair(factory.getPair(token, weth));
   if (address(pair) == address(0)) {
       return 0;
   }
   // 计算可以换出的 WETH 数量:amount0out,amount1Out (其中一个是可以换出的 WETH 数量,
   // 一个是 0)
   // 获得交易对中两种资产的数量(reserve)
   (uint reserve0, uint reserve1,) = pair.getReserves();
   // 判断哪个是 token0 (字母序小的那个)
   address token0 = pair.token0();
   // 通过 token 排序获得 reserveIn -> token=token0?reserve0:reserve1, reserveOut -> token->token0?reserve1:reserve1...
   (uint reserveIn, uint reserveOut) = token0 == token ? (reserve0, reserve1) : (reserve1, reserve0);
   // 计算 amountOut = (997 * amountIn * reverseOut) / (1000 * reverseIn + 997 * amountIn)
   uint amountIn = IERC20(token).balanceOf(address(this));
   uint amountInWithFee = amountIn.mul(997);
   uint numerator = amountInWithFee.mul(reserveOut);
   uint denominator = reserveIn.mul(1000).add(amountInWithFee);
   uint amountOut = numerator / denominator;
   // 根据排序获得 amout0Out,amount1Out (一个是 amountOut,一个是 0)
   (uint amount0Out, uint amount1Out) = token0 == token ? (uint(0), amountOut) : (amountOut, uint(0));_safeTransfer(token, address(pair), amountIn);
   // 执行 swap,用 token 换出 wETH
   pair.swap(amount0Out, amount1Out, factory.getPair(weth, sushi), new bytes(0));
   return amountOut;}

看出什么亮点了吗?

SushiMaker 里的转账逻辑都是:transfer(balanceOf(this))

我们可以分两个阶段来看 convert 的调用过程:
第一个阶段:在 convert 函数中 SushiMaker 拥有的是 SLP,它通过 burn balanceOf(this) 实现将全部 SLP 换成两个底层代币
第二个阶段:SushiMaker 获得了 burn 来的底层代币,再拿 burn 得到的 token 去不同的交易池换 wETH (提示:这句话是错的 !

你发现哪里有问题了吗?我们来看第二阶段的代码_toWETH:注意兑换的逻辑为:

uint amount = IERC20(token).balanceOf(address(this));

所以这个 balanceOf(address(this)) 真的只有在第一阶段 burn 来的 底层代币 吗?
Nononono~
想一想,如果这里的 token 并不是底层代币,而是一个 SLP,会发生什么?

IERC20(SLP).balanceOf(address(this))是 SushiMaker 拥有的所有 SLP,既包含刚刚 burn 出来的,也包含原本 SushiMaker 就有的(从相应的交易池中收取,积攒的手续费)

如果底层代币是 SLP,那第一阶段 burn 的是什么?答案是 SLP 的 SLP

BlockSec DeFi攻击分析系列之二倾囊相送:Sushiswap手续费被盗

还记得吗?攻击者的目标是将 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 骗到他劫持的恶意交易对中)

攻击者地址:0x1925e832C22522E0d9947eE4677120b2f28E4cD4 (https://etherscan.io/address/0x1925e832c22522e0d9947ee4677120b2f28e4cd4)

一组攻击包括很多笔交易,这里以对 MKR/WETH 池的攻击为例:
Swap Exact ETH for Tokens:  0xa8c4edd85727d
Approve:0xf1fdd4cf4d8aa
Add Liquidity ETH: 0x7340edca1a17f
Approve: 0x41a33f0c91b7c
Add Liquidity ETH: 0x896f412f15a7a
Transfer: 0x0e8a76bf7295d
Convert: 0x4947b4f075f8e
Swap Exact ETH For Tokens: 0x5f37bb3b97341
Remote Liquidity ETH: 0xdff10159275e0
Swap Exact Tokens For ETH: 0xb8889bbdeb478

BlockSec DeFi攻击分析系列之二倾囊相送:Sushiswap手续费被盗

攻击者的一个完整的攻击周期:

序章:攻击准备

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 搬空。总而言之,还是第一种方式更优,可以看出攻击者其实是有精心考虑过的

BlockSec DeFi攻击分析系列之二倾囊相送:Sushiswap手续费被盗

高潮:攻击 SushiMaker

Step 5:调用 convert(SLP, wETH),这一步中,SushiMaker 会先将 SLP burn 掉,换成 SLP 和 wETH,再将自己所有的 SLP 都转成 wETH (包括之前从 MKR/wETH 池中收的 SLP 手续费),问题在于 SLP/wETH 这个池是由攻击者创建的(已被劫持),池中的深度非常小,滑点极高,SushiMaker 通过 swap 将 SLP 换成 wETH 这步会血亏(大量的 SLP 只换出了一点点 wETH )

BlockSec DeFi攻击分析系列之二倾囊相送:Sushiswap手续费被盗

尾声:获利

Step 6:上一步中,SushiMaker 在 SLP/wETH 这个池中血亏了大量的 SLP,池中 wETH 巨值钱,此时攻击者只需要调用 swap 用一点点 wETH 就可以几乎将整个池子搬空(换出全部的 SLP,SLP 是真的钱,可以到 SLP(MKR/WETH) 池中提现的)

BlockSec DeFi攻击分析系列之二倾囊相送:Sushiswap手续费被盗

0x2. 附录

2.1 本次事件相关的攻击交易:[
   {
       YFI/WETH: {
           0x18be4ac177c5ada3522e3cafd6c40d750d4288a3a7b22e140521124074fed2ae: Swap Exact ETH For Tokens,
           0xc8653ef0c4658acf3d44059babea07c62578769cac10176e61310f7af04e1597: Approve,
           0x18ed3e5efeec3e475501786541a4f7602340e5aa6bc6cbf24b9c9aa1062449ac: Add Liquidity ETH,
           0x42d553b614d0a64ea2cfe95bf2a99d337c07f1b53ac4d7987966fc39817975e6: Approve,
           0x041463e755e91548b9d6dd1e61519b08f101ff96a44df969b33ee57996619a88: Add Liquidity ETH,
           0x8a2d7926e50123459b7814c0e19b5ac59f7ce221cb5fa82f79d2c0c37ef7ef1f: Transfer,
           0xaa001ac10841c784954b0028192f0ea232bf84e390b964af98f1c49074ec4beb: Convert,
           0x08bbbeaf7cbd2649738812cf720042eb7ebe1411be2f7cf3ede41411dcbf8bc0: Swap Exact ETH For Tokens,
           0x3952f24ef1e5dce37463edcf0f3902316131fb823d79946b96f2a2397a9cd25d: Remove Liquidity ETH,
           0x47c200fefd8c007d972b65b97dadf429c3e6e91359b7b49aaa66ee0fc4a555f4: Swap Exact Tokens For ETH
       },
       profit: 0.2558139 ether
   },
   {
       USDT/WETH: {
           0x123ec6c44fa3baa70c66f583f8ee6bc6b6d2b4d39a1119ab8490d2c1120d4647: Convert,
           0x08bbbeaf7cbd2649738812cf720042eb7ebe1411be2f7cf3ede41411dcbf8bc0: Swap Exact ETH For Tokens,
           0x42973ea279d0bda4222e4689709f86bdb12117dce14420f84f3e9cc6fd42ede2: Remote Liquidity ETH,
           0x30eab0a184f9a1ed6cee309754bb6536bd443fea567776a72b72f3ab911e0113: Swap Exact Tokens For ETH
       },
       profit: 0.1835604 ether
   },
   {
       MKR/WETH: {
           0xa8c4edd85727d3d25401f0cbca1136982edb833f77ff0a93178b222063eb57a4: Swap Exact ETH for Tokens,
           0xf1fdd4cf4d8aa073ff01876333cfeadeab2b87423664d1daa4cf36ad5e415587: Approve,
           0x7340edca1a17fc9e5a6587d6a89c0ed01c5d1f1d20cb19738ab993f6ef7b8b4d: Add Liquidity ETH,
           0x41a33f0c91b7cd17edf888356dfb110d7744f593e432b54ec2f21aae19076aa0: Approve,
           0x896f412f15a7abb959c3c43b8cd9206720fc37f4eabeae336ba101670003e9db: Add Liquidity ETH,
           0x0e8a76bf7295d9316704fde7f953eced612daa9ca7a70322248eeb8a7f508656: Transfer,
           0x4947b4f075f8e9f89f6364158b3d05a92cd72b7920c44c2150a0d12ba009a7a0: Convert,
           0x5f37bb3b9734175b605f6e02700c4b710e5e01622f30d822bcca227d91363d77: Swap Exact ETH For Tokens,
           0xdff10159275e064dad2c7c79585d1f3c4fd60b73d843715e0cff1a2292730e99: Remote Liquidity ETH,
           0xb8889bbdeb478809c3781ccb1c8a16f9fa90b6844d180818b6e73a16963a5ddc: Swap Exact Tokens For ETH
       },
       profit: 1.9761631 ether
   },
   {
       KP3R/WETH: {
           0xe5a025658cd28a688442a57a9b441980feeed8f5e7892064efc68966eb4253eb: Swap Exact ETH For Tokens,
           0xe77e7e9d636c2b733d226693d95d06005ae2b5aa8532957240310225aa73e051: Approve,
           0x5ce8451f9f39f2d32ffbd943d38723a8da6fff59ece8f62caa84c4c2a311ede3: Add Liquidity ETH,
           0x171b741bb7cdcafebdbed1c503eb33babc70c2e2e45b2b55330df3bc6d5c9eda: Approve,
           0x0ad658ebab282a26d67da0a97083a41c83d0eb1ab6d19a4adb3676b57fc3851c: Add Liquidity ETH,
           0x676dd29fade2b93e5ca4b0da9d2b5e4ede86f69d26a6201440e359892360b096: Transfer,
           0x3b37a792edbb785b223a2b4f0971834083acd52503e54d00989a365bbc533627: Convert,
           0x4df5506fdc7616ff65b4ae3b93360e180c7b6f5e976914c4af04648ad6559817: Swap Exact ETH For Tokens,
           0x07e56eaa7935fb341654ac20223b0d32fab006b3f34e372aecf6bce05c3f903f: Remove Liquidity ETH,
           0x8f4d9d676e9a12d07dcd0c8225e207df48f8cb575cdaa6d81c4b7124c3500510: Approve,
           0xac3c837c249fb68ceebd896a72e1e4e8cf355caf3f5fcd0bdc7df7c101ea0596: Swap Exact Tokens For ETH
       },
       profit: 1.8226875 ether
   },
   {
       YFI/WETH: {
           0x1b0eb70bc4d2407745df416acf6e2c312d25ee63c7e8e1291545d66e23fbf704: Pre Attack,
           0x8f6caabd4ecfad30793db76f60a7e28832a18e2bcf006c0ee3652063c7386778: Convert,
           0xe6d28052b55ab520a211daebbc67ee3aece6ae356044e9352fc82910214a038a: Swap
       },
       profit: -0.031382 ether
   },
   {
       AXS/WETH: {
           0x375f5befeb1af976ebb09cf5539b3a3b9746b87153cfa96ba862deb8828970f1: Pre Attack,
           0xed8508a18c347c3829024f7acb94056a78cc63152e2246ebd42b59cd86d0c8f9: Convert,
           0xd08eb264e39cd5dca8edb3684dfd36afb905aea9c47a43110b6c5c45bdb2f7d1: Swap
       },
       profit: 1.349482 ether
   },
   {
       CRD/WETH: {
           0xc6c86de784359a3813b31b4be5806a23f82fdd8d5cd9bd3a7379d33a413eb920: Pre Attack,
           0x7d1b36019fb287867419dffb281b5ad73b37d8dc21abca47f7b618875842ef0a: Convert,
           0x8c14595b205cd04d1e5062a3efe47d3e4dae946eabe744e2d8350d6ba951495a: Swap
       },
       profit: 1.325357 ether
   },
   {
       USDC/WETH: Failed
   },
   {
       COVER/WETH: Failed
   },
   {
       YAX/WETH: {
           0x063bc5df97c0ec87d6c309b54fbc4e3ed13b22f5a115fb1170015e4e9d23cd3d: Pre Attack,
           0x9537551d0db7ed6ae372c050c73cae1272f4d7e7847ea908d6e4d440809a871f: Convert,
           0x9f6bdf1c9065f5f5e5c71f32058093b7ac06e140c3a33db2f21cc8a5ff8a5d21: Swap Exact ETH for Tokens,
           0x9281c35277ea6594924600d70b73077a4307973c540edb5b4ed534c7e25f2423: Approve,
           0xe2f682d8b4801061d504025f11b3032648a3b94019396f7b12749b162f8404cf: Remote Liquidity ETH,
           0xf6ea6c9eb92ca9aed70e8b4b03a6e5565a3a13df71a0aa527fa556077de553bc: Approve,
           0xbf19c9a6a4d879467519299100b4f56c4e86369382ba87d90bebf6c1b1893c14: Swap Exact Tokens For ETH
       },
       profit: 1.153749 ether
   },
   {
       SEEN/WETH: {
           0xc6b39015ab0a32762fb23108e9705cb30d171374c7f9d4b9cf30e640f43d4884: Pre Attack,
           0x3c14c48007147f78ae3c3ac5d56f74413ebc468c6e09f7eb1098c19b9b546185: Convert,
           0x8681b43b4814bb54f49650b19d42133c933bcdbc65dfcd36a6cadf7ef916391c: Swap
       },
       profit: 1.0703319 ether
   },
   {
       AAVE/WETH: {
           0x97c06394d2b9f34606946b438b4fb26ab41f379f8612831c2ec4ed6daac6b4e3: Pre Attack,
           0x7eb4eada4f0700e8a2e8b94d4e4a1879e66ccb36285968500e370420210dde6d: Convert,
           0x40ae49cdb3e9fa61c3309bf35d08ec99494d4a6aa2815791aed1ca654a04d211: Swap
       },
       profit: 0.987368 ether
   },
   {
       REVV/WETH: {
           0x59b5e394caf35f5bbe603b42044707cbb20712d765c260da4434fde0fd3c0e15: Pre Attack,
           0x45660abd04d88f0dae0524b40182d11cb57710316e58ac14413909a9aaf44869: Convert,
           0xcd8749d61d8e96da48de8bd641280f2ce8d93aa5e1e44c9144d322d8b01b1c81: Swap
       },
       profit: 0.888306 ether
   },
   {
       SNX/WETH: {
           0x7eea5790e31176d45b944c0f2624c30a755650b1a353e052191e36d23db2abbc: Pre Attack,
           0x1518424118b519af43657d5de42311f5b5174aab941ff1b7bfe14b0ef5d0874a: Convert,
           0x04cd83c93cf8f37fbafb7d05d5f34cb23bcef8871b2744efad517e6aab38395c: Swap
       },
       profit: 0.908345 ether
   },
   {
       OUSD/WETH: {
           0xc4c196e2383e472edf7dee0c93830f20b3314490d9145dfeac936249688a4cd8: Swap Exact ETH For Tokens,
           0x975e65569e52644fe7b029ce28fef2c3f07dcb818ea9c9d4b7235ab7c4e11c67: Approve,
           0x8c569e7eaa6d7dfc57d5115e628a2b8fc2628837d4e73d6ca85125f639e4f0d0: Add Liquidity ETH,
           0x0f2ed674f497069878c585297812f036a6dd50060e0685e275092320a3797db0: Approve,
           0x483eb12c36b997e9fcb26210e116400f13702db4887b0f4cc3e04fdb8d40c04b: Add Liquidity ETH,
           0xc18eb762dc3fe84afdc7d2642f380c7f56c5ac18eb9a50103766ba0caa5a6bdf: Transfer,
           0x45fd39eef68076c052908c7f8a214cac1cad073f2b5ccf0b919e95670bc801d5: Convert,
           0x32beea84a5b4916cd7107adbb8f609071547e78751c360ceee9bff455d3e7660: Swap Exact ETH For Tokens,
           0x2eb1802c7771695e6dc0835ed2a0e4d029345f35f1d465b8f15ccae11824da2a: Remote Liquidity ETH,
           0x2be981eb82e7577ce43c91718e734cc83e7dd4b384095ea18d62540e53b391bc: Swap Exact Tokens For ETH,
           0x584c6f03a2029bc1b262f31016d87156af3c0c773ebad44f6a2b1e63d623ba5d: Swap Exact Tokens For ETH
       },
       profit: 0.4029899 ether
   },
   {
       CRV/WETH: {
           0xa7b8e476e15ff576878efee81bf200abeec1491941c5507ca846b51cd685d01e: Pre Attack,
           0x19ee64733e4edb44e22086814e7493f30d5bb66582e5963b0db9ea8d05a5bd73: Convert,
           0xe959e2079d1366893cce39212194fd387a0c4594f423c07b9af0d4fa7c2c062c: Swap
       },
       profit: 0.698111 ether
   },
   {
       LINK/WETH: {
           0x62b3ae66524c68ddabd255066807cd7b7f7304e4e8b2da72b9a5e4d8352d35b9: Pre Attack,
           0xb0f7512a12afabb56b3d69cef4df83add82c81338dc92e059d8ba130ef777a1e: Convert,
           0x54570d6f723c3b9f9c6c9e5f7c0357ba50c847b87f17ef5bb517416dcc7f47c3: Swap
       },
       profit: 0.6134929 ether
   },
   {
       RSR/WETH: {
           0xa69b5290fe5dd5580e0c738baa47000cf51ed77cc06b8b54a244c7509f8002ad: Pre Attack,
           0x87a639847c58b6704f28407be39e3ec64b01f005b47753b1a99faf776ea11780: Convert,
           0xa458915881d79debafe5f465843931e572321d35bc98ec25db9ecb9f6a3ea6fd: Swap
       },
       profit: 0.589048 ether
   },
   {
       CORE/WETH: {
           0xa01fd6fcfee90a7a2b0a180e7271b9429e9f23cf502ba7966932e5012c1bc9b7: Pre Attack,
           0x05af95d603cba12d9ca14ce307a973c8875cdcb2e70b07b295440cd2940ea953: Convert,
           0x9bb8b7ca5b41c55a8efcdcd49fdec16747ccee0bf33df8e8f4d62eb116142b21: Swap
       },
       profit: 0.236909 ether
   },
   {
       zLOT/WETH: {
           0x0bb5d792a1d1ea598e45997d5dedb4fcf6acafab47afdb45892417cd128cf37f: Pre Attack,
           0x2550eb33adbe7cfb68cce685363ad2cab90860e5e8a8ed87de1f0a6321e24cf8: Convert,
           0x514bd13f68c8afc1f86ea3e74ca5f51724c2736fa327144b7b6aaf7228bd9124: Swap
       },
       profit: 0.516633 ether
   },
   {
       USDC/WETH: {
           0x98b24059e21c3bdbf7de835f8f30daacaac7a00ba2e9cfdff7e0540b172828aa: Pre Attack,
           0x29fd17d8060f94cd43b45718d0f36df727a27ff4ce67b3ce5a65d6d40ff2d99a: Convert,
           0x24b03cbacf310b5efd2500c3aa8499a356a5ea835426a33d929a38ceef6ea83d: Swap
       },
       profit: 0.688682 ether
   },
   {
       sUSD/WETH: {
           0x8014c37ff678124d5fcf652547264769aa125bba933aadd4a81a4f6115d71ff2: Pre Attack,
           0xc55509dbd81f4da525a540cdbd319d1ffacd2aab2dd3f07abd51cfa2528f28fb: Convert,
           0xc0867b786399a23804f60bb2e974ee5f214a44d418ba5d7a0e51496f9d5f8ff8: Swap
       },
       profit: 0.4547969 ether
   },
   {
       AKRO/WETH: {
           0xc38c30660b9c06e9cecc77c9ff0019020eb9c6fa20887dc15b354ed1a50fec26: Pre Attack,
           0x03b9eb788861e5442c3739738cfa848c73f20871e110c6587609169136c3e772: Convert,
           0x88d3a3e31df87329e87fbfc21b9648907365c1e2d5a7a236eae5655c1ba3ad41: Swap
       },
       profit: 0.43971 ether
   },
   {
       DAI/WETH: {
           0x6964f68672edb916ae170254ee48c537cbb64ca0584651d21fd575550559354f: Pre Attack,
           0x61856228835bfd97dc6b9d7674aaabb577b74fa7d7ff7b7f45454a8d521ff533: Convert,
           0x053ebfe8aedd5975137cfa9bf7d7329a2d6413f6d6e364b252f4386222957eed: Swap
       },
       profit: 0.694006 ether
   },
   {
       YFI/WETH: {
           0xf28c246bf1ce25da1811c7c0eb6fefb8587fa2cdafc84cec4e709600429e6e3f: Pre Attack,
           0xc75a8ca881d4da75774f51006651c9946311d40145ce69d07aee3a85627153d6: Convert,
           0x332c7eff23c9022fe6578550a079034cd3356d9f66507d2ec38462169a4b282c: Swap
       },
       profit: 0.672139 ether
   },
   {
       UBXT/WETH: {
           0x635c79a8dca89a3aa95f545e2243a735906c4ff221cfbc160396c150d58ea036: Pre Attack,
           0x3ffcfc9985622ad7cf0fdc2eb582ad7ce8bf9e9295fd7a4de44354fdd71a688a: Convert,
           0x7c6af5ca27ceb04aad514ddcaee8afc6dd4eb79d0816e24b007e7db205e93ce3: Swap
       },
       profit: 1.1470659
   },
   {
       0xMaki: I see u.
   },
   {
       WBTC/WETH: {
           0xa195c9c23a56ca5fa747677c04a2d5a8c513bdfd141e45c20a8c4e091ca73883: Pre Attack,
           0x136b1d2bf6c51a6ed1fc3f1da7a2783e3835fa78c9149c1c6d2b21e9aad8b05b: Convert,
           0x43cde98b0be5932b8dbf709eebd0cbb4738599c9a4f0795ddb52d7d31f293cf4: Swap
       },
       profit: 2.575794 ether
   },
   {
       USDC/WETH: {
           0xfc8196231bfb22ae6fcc9ceb04f9e5e3d647fc9e023870020048be93958218a5: Pre Attack,
           0xe43fe2eb54c2eefba519a7ff9cf27f84e743961268dfdf9477a47cd2ea467642: Convert,
           0x4b0b3b51150b3a270d10f3388ccf12a196a2cada4203e3a2c39af88d5dc04958: Swap
       },
       profit: 1.157208 ether
   },
   {
       LINK/WETH: {
           0x95eea1cedccffc405d0cb9743360712b69a295a66fe0649b5ab1b067869f05fa: Pre Attack,
           0xf259718ee2f81b543bbfbe2f236cd4235651f8365ec8944741a3c7f3242f06b9: Convert,
           0x525929006fc3f089d67b6596f53c5ebe6b82de1f8d3cdeb397f1f55ff4937c47: Swap
       },
       profit: 0.7395449 ether
   }

后续攻击者又尝试攻击 Sushiswap 的其他相关仿盘项目,如 LuaSwap:

https://etherscan.io/tx/0xeacfed6fb18563c18b9af5cb0d3a5b13e97cf02026ef9a4cb625cc33580d02a7

BlockSec DeFi攻击分析系列之二倾囊相送:Sushiswap手续费被盗

但可能获利不多,只成功几笔就不了了之了

2.2 本次事件后续结果:

在 **#11351530**块,Sushi 的管理员向攻击者喊话:

BlockSec DeFi攻击分析系列之二倾囊相送:Sushiswap手续费被盗

可以看到,好像有些延迟,攻击者的攻击依然持续了一段时间:

BlockSec DeFi攻击分析系列之二倾囊相送:Sushiswap手续费被盗


最后一条 Convert 中:

BlockSec DeFi攻击分析系列之二倾囊相送:Sushiswap手续费被盗


SushiMaker 地址从 0x6684977bBED67 变成了 0x280ac711bb99d:

Old:0x6684977bBED67e101BB80Fc07fCcfba655c0a64F
   function convert(address token0, address token1) public {
       // At least we try to make front-running harder to do.
       // 限制只能 EOA 来调用该函数
       require(msg.sender == tx.origin, do not convert from contract);
       // 获取相应 SLP token 合约的地址(即:token0/token1 交易对合约地址)
       IUniswapV2Pair pair = IUniswapV2Pair(factory.getPair(token0, token1));
       // 将 SushiMaker 中存有的全部 SLP,转给交易池
       pair.transfer(address(pair), pair.balanceOf(address(this)));
       // Burn 掉这些 SLP 换成两种 token
       pair.burn(address(this));
       // 分别将两种 token 找到相应的和 wETH 的交易对,全部转换为 wETH
       uint256 wethAmount =_toWETH(token0) +_toWETH(token1);
       // 将转换的到的全部 wETH 找 wETH/SUSHI 交易对换成 SUSHI_toSUSHI(wethAmount);
   }
   function_toETH(address token) {
       ...
       uint amountIn = IERC20(token).balanceOf(address(this));
   }New:0x280ac711bb99dE7C73FB70fb6DE29846D5e4207F
   function convert(address token0, address token1) public {
       // At least we try to make front-running harder to do.
       require(msg.sender == tx.origin, do not convert from contract);
       IUniswapV2Pair pair = IUniswapV2Pair(factory.getPair(token0, token1));
       pair.transfer(address(pair), pair.balanceOf(address(this)));
       (uint amount0, uint amount1) = pair.burn(address(this));
       uint256 wethAmount =_toWETH(token0, amount0) +_toWETH(token1, amount1);_toSUSHI(wethAmount);
   }
   function_toETH(address token, uint amountIn) {
       ...
   }

可以看到区别在于:代码第 26 行_toWETH 限制了 amount,这样修改以后就不会将 SushiMaker 中存的全部 SLP 都去池中换成 wETH,而只是换取 burn 出的一部分

但是这样问题就解决了吗?其实还没有,时隔两个月,同样的地方,Sushiswap 又中枪了 见:amenda:SushiSwap 攻击事件 (2021 年 1 月 27 号) 分析 (https://zhuanlan.zhihu.com/p/372058217)

0x3. 参考

https://www.blocksecteam.com/

contact@blocksecteam.com

原创文章,作者:BlockSec。转载/内容合作/寻求报道请联系 report@odaily.email;违规转载法律必究。

ODAILY提醒,请广大读者树立正确的货币观念和投资理念,理性看待区块链,切实提高风险意识;对发现的违法犯罪线索,可积极向有关部门举报反映。

推荐阅读
星球精选