7

Uniswap重入事件详尽解析

 3 years ago
source link: https://www.jinse.com/blockchain/1149712.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Uniswap重入事件详尽解析

BlockSec Team 刚刚

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

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

如果能重来,你会做什么?

本期简述了一个意外发现时空暗道的毛贼,如何戏弄守护在秘宝洞口的独角兽,将财宝窃于囊中的魔幻故事

阅读建议:

  1. 如果您初识 Defi,又有耐心的话,可以从头开始阅读,酌情跳过废话

  2. 如果您对AMM、ERC777、Uniswap等非常了解,可以直接从0x1中Uniswap重入部分开始

  3. 文章较长,看不下去,记得点个关注再走喔~

正文

时间:2020-4-18. 8:58. #9893295

【注】imBTC 是 tokenLon 发行的与 BTC 价值 1:1 锚定的 ERC777 标准代币

5550349_image3.png

  • 相关代币的价值情况:

5550350_image3.png

imBTC ($7029.38) : ETH ($178.81) = 39.31

背景介绍

  • AMM

交易(Trade)是什么

交易就是卖家和买家,俩人你情我愿,大家都觉得不亏,可以达成这次的交换

交易所(Exchange)是什么

交易所是这个游戏的组织者,它就像一个红娘,男男女女来到她这里,提出自己的要求,它便开始牵线,还要保证双方都满意

放在现实中,这些要求,就是买家卖家的出价(ask price & bid price),这些全都记录在交易所的服务器中。服务器中,买卖的交易请求不断更新跳动,交易所的机器要做的就是在尚未达成请求中,找到一对可以匹配的,然后促成这笔交易(撮合)。比如:张三想不低于50块卖茅台的股票,李四想不超过60块买茅台的股票,机器看到后「刚好,那你俩就凑合过吧」。这种便是中心化交易所通过记账簿(Limit Order Book)的交易处理方式

但是,这有什么弊端呢?对于健康运行的交易所,市场很热,不断有大量的买单和卖单,机器很快就可以找到匹配的交易对。如果对于低迷的市场,你想卖,但是没人买,这会发生什么?找不到接盘的人!这很影响效率(time is money),所以这时市场上出现了做市商

什么是做市商(Market Maker)呢?

刚才提到,买家找不到卖家,或者卖家找不到买家。怎么解决这一问题呢?

中间商!无论是买家还是卖家,都可以直接找他,他会大量回购资产,再卖出(只赚个辛苦钱)。这其实类似于一种缓存的机制。他要求做市商必须有足够的资金,大家才相信他不会乱要价(这样对于持有资产的他来说是更大的损失,杀鸡取卵)

去中心化交易所(DEX)是什么?

DEX无非就是将上述的过程放到区块链上。它可以直接把上面的程序改写成智能合约照搬到一条区块链上,同样用这种记账簿的方式去撮合交易。但是要知道区块链上的存储是相当昂贵的(也有一些链下存储,链上验证的方式来解决这一问题)

于是人们就开始寻找一种方案,可以通过智能合约实现代币的有效交换,什么叫有效交换呢,就是无论的买的人还是卖的人都觉得不亏(以市场价达成)

既然问题出在,记账簿方式一方面可能存在找不到匹配对手,另一方面链上存储比较昂贵。那我们可不可以把做市商这一机制也搬到链上来?简单来说,就是有一段智能合约它可以吸收大量的资金,每当有人想交换代币时,直接调用这个合约就可以以市场价获取另一种代币,这就是自动化做市商(AMM)

自动化做市商是什么

上面提到AMM需要解决两个问题:

1) 如何吸收大量的资金(需要有不同种类的代币,这样才可以换来换去)?

传统做市商需要先买资产,但是如果AMM先去买币,它去哪里买呢?记账簿类型的DEX吗?这并没有解决根本矛盾。

链上混的,大家谁没几个币(可能是从中心化交易所用法币买入或交易得到的,也可能是参与某些DeFi项目的Rewards),所以它只要骗大家过来把币放在自己这里,资金不就来了。

不过如果没有经济激励,没人会愿意将自己的钱放在别人口袋里的。这个激励便是从交易的手续费中获取,当AMM运作起来,只要有人做交易,就需要交一定的手续费,这个手续费会分配给那些给池子提供流动性的人(流动性=钱)

2)如何以市场价交易?

现在DEX把大家的币都骗过来了,这时有人来了,想用一种代币来买走池中的另一种代币。他能买多少呢?

其实抽象来看,每个人拥有的数字货币不过是区块链上存的数字,而不同的代币就是不同的变元。交易这一过程,对于交易池来说,就是一个变元增加,另一个变元减少

回忆一下我们小学学到的数学知识:一条曲线的斜率k = Δy / Δx,上面能买多少的问题,就变成了如何找到和市场一致的这个 k

5550351_image3.png

对于上面这条曲线,曲线上的每一点,就代表交易池中两种代币的一种状态,比如P点:y代币有B个,x代币有A个。这时有人来池中做交易他花了 BD 个 y(Δy)可以换出 AC 个 x (Δx),这时交易池的状态就从P点转移到了Q点,斜率 k 值随之 "变小"

因为这个曲线是无限延展的,k值可以取遍 0 - ∞,所以肯定存在一个点与市场的状态一致 (斜率 k 相等)

那问题来了,谁来推动当前的交易池状态向着市场状态逼近?

答案是套利者,每当交易池中状态与市场状态不一致时,就会有套利者发现机会,比如当前池中 1 ETH : 5 USDT,市场上 1 ETH : 10 USDT,这时明显交易池中 ETH 的价格虚低,就会有人来交易池中用 5个USDT 买走1个 ETH,再去市场上卖掉获得 10个USDT,净赚5个 USDT(低买高卖),而此时交易池的状态就向市场的状态趋近了一步,就这样不停的有人做套利,最终交易池的状态一定会和市场的状态相差无几

总结

AMM类型的交易所解决的痛点是:区块链上代币的有效交换

俗话说的好:「哪里有痛点,哪里就有钱赚」。有很多人愿意掏钱(手续费)来使用代币交换这个服务

AMM一方面用这些手续费吸引玩家向资金池投钱,资金池有了钱就可以通过AMM实现代币交换;另一方面,由于套利者的存在,池子代币交换的价格与市场价格一致

这样,提供流动性的玩家赚到了手续费,套利者赚到了差价,用户得到了代币有效交换这一服务。三个角色缺一不可,构成这一系统。一拍即合,各自欢喜

其中AMM有几种性质,最广为人知的就是:交易池中底层代币(Underlying Token)的储备量满足一定的不变式,比如Uniswap的恒定乘积 (reserve0 * reserve1 = k)

但其实还有很多隐藏的性质 (伏笔1),想知道吗?哎,我就不说,想知道就自己继续看下去!

  • ERC777

提出时间:2017-11-20

我们都知道 ERC20 中代币转账函数的基础款是 transfer,它的功能只是简单的 balance 加减,比如 alice 调用 transfer(bob, 100) ,bob 是不知道谁给自己转了100个 token

当然对于我们来说,可以通过查看 Ethscan 或者查找区块数据得知(但也要等到区块上链)。如果 bob 是一个合约,他是没办法在转账 balance[bob] += 100; 发生的当下得知。这产生了诸多不便,比如用户想使用合约的一项服务,但是支付了服务费(token)后,合约并不知道谁付钱给了它

因此ERC20中同时存在另一套组合技 approve + transferFrom,这样用户就可以通过先授权给第三方,第三方再通过查看 allowance(授权额度的映射表)来代替委托人转账,这无疑带来的很多的便利(很多合约都需要用户先授权,再调用其方法。Uniswap 也是如此,比如调用 Uniswap 的 swap 函数需要用户先对 Uniswap进行一定额度的 approve [注1])

【注1】有时候为了方便,同时省去每次approve的gas开销,用户选择直接approve最大值 0xffff...,这种行为是不安全的,如果第三方合约受到攻击,您的资产也会处于危险中

更多细节了解,可以关注我们的相关工作: Towards understanding the unlimited approval in Ethereum (https://www.youtube.com/watch?v=ijgYfdOADVI)

但是 ERC20 就完美了吗?其实还没有,其中为人所诟病有:

  1. 每次都需要先 approve 再进行其他操作(至少2笔 Tx,当然也有一些线下签名的方式,来避免这一问题)

  2. ERC20 中的授权没有权限的概念,只是简单的授权余额,这在很多情况下还是存在危险的

  3. 每次转账无法携带信息,这限制了很多应用的想象力

  4. 代币误转后锁死在合约中(如果合约没有实现相应的处理逻辑)

可以看到 ERC20 的功能是非常单一且基础的,为了对此进行改进提出了 ERC777 标准(ERC777 标准兼容 ERC20 [注2])

【注2】实现的方式无非是在ERC777标准中实现ERC20同样的函数 (如:transfer, transferFrom ...),但是在这些接口内部调用ERC777的逻辑(如:_move方法)

了解了 ERC777 的来历以后,我们看看具体 ERC777 做了哪些改进:

1. 在转账的过程中可以携带数据,相当于在 ERC20 的 transfer 函数上加了一些参数(calldata),这个数据有什么用呢,作为 hook 函数的参数,便于 hook 函数据此来作出不同的决策

2. 代币的转移不仅仅是 balance 的加减:ERC777 引入了两个 hook 函数 tokensToSend 和 tokensReceived,这两个函数是干什么用的呢?过程很简单:在一笔转账交易过程中,balance 减少的地址(token holder)如果实现了 tokensToSend 接口函数,就先去执行 holder 的这个接口函数;同样的,balance增加的地址(token receiver)如果实现了 tokensReceived ,收到转账后会去执行receiver的这个接口函数[注3]

【注3】这里利用的是ERC1820注册机制:这里不需要详细了解细节,只要知道任何地址都可以实现接口函数,对于EOA来说,可以通过部署一个合约,在其中实现接口函数,并将注册信息发给ERC1820合约,此后当EOA触发相关的接口时,就会先通过ERC1820查找接口实现的合约地址,再去调用相关的接口函数

5550352_image3.png

值得注意的是,ERC777 标准中提到,token实现应满足 sender回调 → 更新状态 → receiver回调 的顺序,以防止发生重入事件(伏笔2),代码中的表现为:

5550353_image3.png

还有一些其他的特性,如:操作员概念、Mint与Burn完善了token的生命周期等等,与本次攻击关系不大,暂且不展开

总结

ERC777是对ERC20的"升级"

它会在代币转移 (balance加减) 之前回调TokensToSend函数,转移之后回调TokensReceived函数

TokensToSend函数由转移代币的持有者 (可以是合约) 实现,TokensReceived由转移代币的接收者实现,这给了用户很大的自由,但也带来了一些问题,比如本次的攻击 

攻击分析

  • 经典重入攻击

我们先不急着去看攻击过程,先复习下最简单的重入攻击(例如: The DAO,LendfMe等事件)

在这些"经典"攻击中,攻击者通过重入可以不断的使合约对其转账,直到退出"递归"时才更新一次的状态,他可能转账了1000个 Token (50个 * 20次),但是 balance 却只减少了50

5550354_image3.png

5550355_image3.png

如果攻击者可以在 transfer 的过程中重新调用 withdraw 函数,就可以实现重入。主要原因在于:合约中转账等操作先于余额状态的更新

总结

简单来说,重入攻击就是打断施法,重点在于:

在哪里打断施法

打断以后又做了些什么可以影响后续的结果

重入攻击有一个重要的特征,就是:先转账,后更新状态

对于上面这种传统的重入攻击,打断的便是「转账+记账」这一组合技做的事情就是不断重新转账,以影响后续的记账结果

  • 重入Uniswap

(前方重点!)

那 Uniswap 如何重入呢?我们知道,Uniswap 是一个去中心化交易所(DEX),用户可以在上面交换代币

【补充】UniswapV1 只实现了ETH和任意 token 之间的交换,对于 token 与 token 的交换,可以借助ETH中转来实现

这并不像传统重入攻击的「转账+记账」模式。那它可以在哪里打断施法,又可以做哪些事情影响后续的结果呢?

Uniswap 交易对合约中的交换函数(例如: ethToToken, TokenToToken...),原理基本一致,即保证交易池(交易对合约,后简称交易池)内两种币数量的乘积恒定(不考虑 Fee 的情况下[注1]),这些函数会先调用 getInputPrice 方法获取可以购买的另一种代币数量:

5550356_image3.png

对应的公式为:

5550357_image3.png

这里公式表示:池中原来储备量为 ether : token ,现在alice手里有 token(put) 个 token,ether(get) 代表她能从池中买到多少个ETH

我们现在直接挑其中一个开锤,比如 tokenToETH (这个函数的功能是用 token 换 ETH):

5550358_image3.png

我们可以看到这个函数先将 ETH 转给用户,再调用 transferFrom 收取用户的代币(代码第8行和第10行)

我们是否可以打断这两笔转账呢? 对于普通的 ERC20 代币,确实是没有办法打断转账的过程,但是还记得吗?我们提到的 ERC777 代币,这种复杂的代币,恰恰提供了这样的暗道,使不怀好意之人,有了可乘之机

现在想法很简单了,如果 Uniswap 存在一个 ETH-ERC777 的池,我们就可以利用 ERC-777 的回调功能,在 transferFrom 的过程中,重入这个函数,继续发送 (send) 一笔 ETH 给自己

这时可能有聪明的读者要问了:「即使重入后又转了一笔 ETH 给自己,后面"递归"返回后,不是还要为每轮重入所购买的 ETH 付相应的 token 吗?」没错,是这样的,如果只是简单的重入这个函数,只是把一次购买(token → ETH),变成了多次购买,毛都赚不到

更聪明的读者可能现在已经想起来,之前我们提到的 Uniswap 的计价公式,由 ERC-777 的特点,我们可以知道重入是发生在 ETH 之后,token 余额变更之前,这就意味着,在重入过程中计价公式的变量状态其实是不一致的(ETH 的 reserve 更新了,但是 token 的 reserve 还未更新),攻击者正是利用这一点,每次薅一点羊毛,直到把人家羊给薅秃了:

5550359_image3.png

从公式中可以看到,本来在一次 swap 后,token 和 ETH 的状态会同时变化(以维持乘积恒定),但是由于重入发生在发送 ETH 和更新 token 余额之间,直接被打断施法了,从而造成了悲剧

很简单的道理:如果正常的两次调用,第二次是 token↑ 使得 etherget ↓,但是由于重入后状态没有更新(token 没变),所以相比"正常情况"下可以获得更多的 ETH

【注1】公式相关推导过程(基本原理就是:保证交易池中两种代币一直满足恒定的乘积

5550360_image3.png

【注2】可能读到这里, 你还是感觉哪里不对, 这是正常的, 如果有兴趣, 你可以思考这样几个问题:

1) 这样一定能获利吗, 需要满足什么条件吗?

2) 攻击者获利是最优的吗, 还可以怎样优化?

深入分析部分, 小编对这些问题做了一些简单的尝试, 如果有兴趣, 不妨继续看下去

(这已经是小编第4次复盘这次攻击, 但还觉得很多问题没有真正的搞清楚, 所以如果你没看懂, 那也没什么大不了的)

总结

总结来说,这次的攻击是由于:

① UniswapV1不兼容ERC777代币 → ② 从而导致合约代码可重入 → ③ 从而导致恒定乘积中变量状态不一致 → ④ 从而导致交易池资金被薅走

  • Real World

原理大概就是这样,管你听没听懂,继续看就完了,下面我们来看看real world中攻击者到底做了什么?

其实说到现在,更更聪明的读者,都可以跑去自己攻击了(友情提示:小心警察叔叔找上门哦

我们随便找一笔攻击者的Tx:0x32c83905db61047834f29385ff8ce8cb6f3d24f97e24e6101d8301619efee96e

5550361_image3.png

可以看到攻击主要分为两个部分:

  1. 首先是一堆的自毁合约,看起来比较迷惑,但是查看这些自毁合约的调用者(GST[注1])就可以知道这是为了节省攻击的Gas(与攻击本身关系不大)

5550362_image3.png

  1. 攻击过程:

step 1: 使用 1 ETH 向 Uniswap(imBTC) 换取 imBTC

step 2: 将换得的 imBTC 分两次(一次一半),向 Uniswap(imBTC) 换回 ETH(其中第二笔是重入所得),通过简单的计算我们可以知道:0.611341052127704463 + 0.472375805535296596 = 1.0837168576630012 > 1 通过这种方式来薅羊毛

step 3: 最后将收益从攻击合约转给攻击者自己(1.0837168576630012 - 1 = 0.08371685766300119(一笔攻击的获利)

【注1】GST (GasToken):是一个旨在节省Gas的代币,我们知道Ethereum有一个特性就是销毁合约时会返回大量的Gas,所以GST的原理就是:在Gas Price便宜的时候,用户可以通过这个合约生成一系列子合约,来"存储Gas"(同时Mint出相应的GST代币,代币用户存储了多少单位的Gas),当需要时再用GST调用合约销毁当时创建的子合约换取相应的Gas

[GST2]: 0x0000000000b3f879cb30fe243b4dfee438691c04 (https://gastoken.io/)

这次的攻击事件,攻击者"或许"不是第一个发现漏洞的人

Uniswap 交易对合约中的重入漏洞,早在 2019年1月12日 ConsenSys 的审计报告中就被提及,而且在 #14 commit 中提到:合约中可能存在多种方式的重入攻击(包括利用 ERC-777 标准代币),并给出了简单的攻击过程

审计报告中提出:对于 UniswapV1 交易对合约中的 exchange 类型函数,无论 transfer 是发生在 token 余额状态变更前,还是 token 余额状态变更后,如果 transfer函数 可以重入,都会造成损失,并给出了后一种情况的简单攻击过程模拟

【补充】利用 ERC-777 重入属于前一种,重入发生在状态变化前(还记得上面我们提到的,ERC-777 代币转移的过程吗,_callTokensToSend 是发生在 _move 之前的 | 回收伏笔1),审计报告中还指出,相比第二种情况,利用 ERC-777 来攻击会更简单

"If token balances are updated after the reentrancy (e.g. ERC-777), the algorithm is even easier and requires fewer funds to steal liquidity pool."

5550363_image3.png

https://github.com/ConsenSys/Uniswap-audit-report-2018-12#31-liquidity-pool-can-be-stolen-in-some-tokens-eg-erc-777-29

Uniswap的过程可以简化为:两笔转账,一笔向交易池转入,一笔从交易池转出。有三个位置可以切入,① 第一笔转账前,② 两笔转账中间,③ 第二笔转账后。显然在第二笔后是没有意义的

注意!!! 要记住:用户从Uniswap买币时,Uniswap是先将钱转给用户,再将用户的钱转来。所以这两笔转账是 先转出,再转入

现在,可以揭秘上面提到的AMM的隐藏性质(回收 | 伏笔1)了,那就是:

隐藏性质1:

AMM恒定乘积的曲线 x * y = k,是一个"凹函数",凹函数意味着,他不像一次函数那样,相等间距的x变化,带来的y变化是相同的。而是:沿着一个方向,相等间距x的变化 (Δx),引起y的变化 (Δy⇣) 会越来越小或越来越大!

你可能有点懵。下面我们一点点来看:

最最简单的情况下,我们不考虑交易的手续费,在 ETH/imBTC 池中,用 Δy 个 ETH 换出 Δx 个 imBTC,紧接着再用 Δx 个 imBTC 换回 ETH 可以换出多少呢?答案是 Δy,这很简单

接着我们引入手续费(0.3%),有了手续费的摩擦,这一结论就不成立了,两次交易都会损失一部分手续费,导致最后换出的ETH

5550364_image3.png

接着我们再考虑一个问题:同样先不考虑手续费,如果我们先用 2*Δy 个 ETH 换出 2*Δx 个 imBTC,接着分两次,每次用 Δx 个 imBTC 去池中换 ETH,两次换出的 ETH 数量相等吗?

答案肯定是不相等的。原因就在于上面提到的凹函数这一性质(如图: 图中C是AB的中点)!

5550365_image3.png

[注] y轴代表ETH, x轴代表imBTC; 从B到A代表: 先用ETH买imBTC, 再从A回到B代表: 用imBTC买回ETH

这两次交换,第一次换出的数量要大于第二次的数量。这就意味着,总共能换出 2*Δy 个 ETH,但是第一次能换出的 ETH 数量是大于 Δy 的!

如果能重来,那有没有可能,用 imBTC 换回 ETH 的过程中,两次交换都用第一次的结果?

没错,只要我们在第一笔转帐前打断施法 (打断点①),重新调用交换函数!

这样用 2x 个 ETH 换出 2y 个 imBTC,接着分两次每次都可以用 y 个 imBTC 换出 >x 个 ETH,最终换出比投入更多 (>2x) 的 ETH(在不考虑手续费的情况下

5550366_image3.png

由于 Uniswap 是先计算可以换出代币的数量,再进行转账。这样就可以:重复使用第一段的价格(可以换出 >x 数量的 imBTC)

QUIZE:不考虑手续费这是稳赚不赔的买卖,但是如果引入了手续费,事情会怎么样呢?这就有一定的条件了,要看到底薅的更多,还是亏得更多

> 有兴趣可以自行推导

对于Uniswap是否可以实现这种,在第一笔转帐前重入呢?

很不幸的是,Uniswap的逻辑是先操作 ETH 再操作 代币,这意味着无论是用ETH买代币,还是用代币买ETH,都是先将ETH转出给用户,或是先将ETH转入给交易池,这便不符合我们上面提到的第一笔转账需要是ERC777代币 (这样我们才可以回调)

但是! Uniswap 还存在着 TokenToToken 这种方式,因为 V1 只支持 Token / ETH 交易池,所以这一函数的实现原理,就是: 先在第一个池中用 Token 换出 ETH,再在第二个池中用 ETH 换出 Token

5550367_image3.png

可以看到 这个函数的实现逻辑是: 比如我们使用imBTC换DAI,它先将imBTC转给第一个交易池,然后将换出的ETH转给第二个池获取相应的DAI

太好了,这样不就有了ERC777代币作为第一笔转账的条件了嘛!

但是,我们要怎么把钱取走呢,方法是: 我们自己来创建第二个交易池,因为我们是这个交易池中代币的 owner, 所以我们可以mint出无限多的代币,来将池中的 ETH 拿空,而池中的 ETH 便是第一个我们在第一个交易池中的输出,也就是重入攻击的获利 

实验结果: 

5550368_image3.png

这其实就是 ConsenSys  审计报告中提出的攻击方式 (但是并未实现 | 回收 伏笔2

隐藏性质2:

k值越小,曲线凹的程度越大,相等间距x的变化 (Δx),引起y的变化 (Δy⇣) 会越来越小! (如下图中,ΔAC > ΔA'C')

5550369_image3.png

上面在第一个代币转账前打断我们已经验证过是可行的了(在没有手续费的情况下一定能获利 >2*Δy),那在两个代币转账之间打断呢(打断点②)?

攻击者采用的便是这种方式!

事实也是可行的,第一笔转账是从Uniswap转出 (交易池先将钱转给用户),交易池中一种代币的存量增加 (y⇣) 这使得 k 变小,曲线由上面一条跃迁到下面那条 (A → A')

从图中可以明显的看到A'状态下的价虽然次与A点,但是还是优于C点的 (p = y / x),所以如果不考虑手续费,继续使用 Δx 的 imBTC 换出的 ETH: ΔA'C' > ΔCB

这意味着,相比正常情况下 (正常情况下: 2*Δx imBTC 可以换出 ΔAC + ΔCB = 2*Δy),重入可以换出 ΔAC + ΔA'C' > 2*Δy

如果考虑手续费,情况可能就更复杂一些了,理论上还是可以获利的 (但是是否一定可以获利呢? 小编对此也没有证明出来

总结

由于 ERC777 的引入使得 Uniswap 的转账过程可以被重入

Uniswap swap的过程可以分为两部分: 从交易池转出, 向交易池转入

我们可以从两个地方重入:

打断点①

通过 TokenToTokenSwap 函数,如果输入 Token 是 ERC777 标准。可以利用TokensToSend 回调函数实现在两次转账前重入获利 (比较复杂, 也就是审计报告中提到的攻击)

打断点②

通过 TokenToEth 函数,在 ETH 转账后,Token 转账前,利用 TokensToSends 回调函数重入获利(这种方式获利更简单易懂,也就是攻击者使用的方式

  • 附录

a. 攻击者是否获利最大化,如何获利更多?

这是一个比较困难的问题

从直觉上来看:攻击者每笔攻击交易重入的次数越多,使用的Ether数额越大,获利就越多,但是还要考虑实际交易对中真实的情况

因此小编只是做一些简单的尝试与统计:

优化的维度有:初始时攻击者投入的Ether数量,投入Token占比,重入的深度、攻击次数

这些都可以在数学上求解,但是小编懒(bu)得(hui)搞,有兴趣的大佬可以尝试

实验条件:区块号 #9893295, 工具 brownie

实验1:获利与投入ETH数量及投入Token占比的关系

实验参数:使用ETH的数量 [1, 3, 5, ... ,19],投入Token的占比 [1/20, 3/20, 5/20, ... 19/20]

【注】这里的token占比指的是:还记得凹函数这一性质吗,前半段下降快于后半段,这里实验的是前半段与后半段的比例对获利的影响,其中占比指的是前半段占全部的比例

5550370_image3.png

结论:投入ETH的数量越大,获利越大,并且增长的幅度也会有所加大。投入token的占比在0.5时接近最大值

实验2:获利与攻击次数的关系

实验参数:分别使用100 ETH / 累计 ETH两种方式,尝试增加攻击次数

我们知道随着攻击次数的增加,池中状态会一直向曲线的左侧移动,也就是说随着攻击次数的增加,获利会逐渐增大

5550371_image3.png

5550372_image3.png

上面两图是两种不同的方式,上图每次使用固定的100ETH进行攻击,下图初始用100ETH攻击,后续每次使用的ETH会累积上之前的获利。很显然累积上获利使池子更快的被掏空(40 次 / 175次)

结论:随着攻击次数的增加,获利会以指数趋势增加

实验3:重入次数与获利的关系

实验参数:重入次数取 [2, 40],使用100 ETH

5550373_image3.png

结论:随着重入次数的增加,理论上获利是会更多的,但是增长的幅度逐渐趋于平缓

【注】重入次数与token占比是关联的,比如重入2次,token占比为0.5 ...

同时还需要考虑gas limit等条件,所以攻击者选择重入2次,token占比0.5,还是有道理的

b. 本次事件涉及的攻击Tx有哪些 (时间范围)? 

通过使用我们的内部工具与数据集,得到结果如下:

对于Attacker (0x60f3fdb85b2f7) 来说,攻击Txs涉及的区块范围为:9893295 - 9894249 共954块

c. 攻击机会何时开始存在?

攻击者是否发现的足够早(攻击者之前是否存在攻击机会)?

 UniswapV1的 imBTC 池在 #9059910 被创建出来,攻击开始于 #9893295

d. 本次事件后续结局如何?

通过使用我们的内部工具与数据集,得到结果如下:

在 #9894379 块 (2020-4-18 12:49:50):0xb9e29984fe506 向 imBTC 合约发送一笔Tx (0x7ce097c5149),调用其 pause 方法[注1]关停合约(禁止转账等)

5550374_image3.png

【注1】pause的实现方式很简单,利用一个全局标志变量 _pause,对每个转账函数加一个modifier来修饰,当这个标志为true时,revert掉

在 #9895526 块 (2020-4-18 16:57:55):0xb9e29984fe506 向 imBTC 合约发送一笔Tx (0xced24b64665b9),调用 unpause 方法,解冻 imBTC 合约,恢复正常交易

安全建议

道路千万条,安全第一条,这里小编给出一些安全建议,各位大佬权当参考:

1.  对于重要函数(修改一些重要Storage变量),建议使用一些防止重入的方法,如lock(比如Openzeppelin中提供的ReentrancyGuard等方法)

2. 合约代码尽量满足:Checks-Effects-Interaction 模型

3. 项目上线前应做好审计工作,并不断迭代修改。审计方和项目方,是相互促进的关系。像本次事件中,审计中指出的错误,时隔一年被攻击,岂不是很尴尬

4. 应提前考虑好兼容问题,保证合约代码的完备性。比如 通缩/通胀 代币、ERC777代币等比较特殊的代币模型,都应尽可能的考虑与规避风险

  • imBTC Uniswap Pool Drained for ~$300k in ETH: https://defirate.com/imbtc-uniswap-hack/

  • Openzeppelin PoC:https://github.com/OpenZeppelin/exploit-uniswap#exploit-details

  • https://medium.com/imtoken/about-recent-uniswap-and-lendf-me-reentrancy-attacks-7cebe834cb3

  • 详解 Uniswap 的 ERC777 重入风险:https://paper.seebug.org/1182/

  • https://medium.com/imtoken/about-recent-uniswap-and-lendf-me-reentrancy-attacks-7cebe834cb3


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK