往期回顾: (1) [BlockSec DeFi攻击分析系列之一] 我为自己代言:ChainSwap攻击事件分析 (2)[BlockSec DeFi攻击分析系列之二] 倾囊相送:Sushiswap手续费被盗 (3)[BlockSec DeFi攻击分析系列之三] 偷天换日:深度剖析Akropolis攻击事件 (4)[BlockSec DeFi攻击分析系列之四] 表里不一:Sanshu lnu 的 Memestake 合约遭袭事件分析 (5) [BlockSec DeFi攻击系列之五] 以假乱真:DODO V2 众筹池遭袭事件分析 如果能重来,你会做什么? 本期简述了一个意外发现时空暗道的毛贼,如何戏弄守护在秘宝洞口的独角兽,将财宝窃于囊中的魔幻故事 正文
相关代币的价值情况:
0.0 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 对于上面这条曲线,曲线上的每一点,就代表交易池中两种代币的一种状态,比如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),想知道吗?哎,我就不说,想知道就自己继续看下去! 0.1 ERC777
我们都知道 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])
但是 ERC20 就完美了吗?其实还没有,其中为人所诟病有: 每次都需要先 approve 再进行其他操作(至少2笔 Tx,当然也有一些线下签名的方式,来避免这一问题) ERC20 中的授权没有权限的概念,只是简单的授权余额,这在很多情况下还是存在危险的 每次转账无法携带信息,这限制了很多应用的想象力 代币误转后锁死在合约中(如果合约没有实现相应的处理逻辑) 可以看到 ERC20 的功能是非常单一且基础的,为了对此进行改进提出了 ERC777 标准(ERC777 标准兼容 ERC20[注2])
了解了 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]
值得注意的是,ERC777 标准中提到,token实现应满足sender回调 → 更新状态 → receiver回调的顺序,以防止发生重入事件(伏笔2),代码中的表现为: 还有一些其他的特性,如:操作员概念、Mint与Burn完善了token的生命周期等等,与本次攻击关系不大,暂且不展开 ? 总结 ERC777是对ERC20的\"升级\" 它会在代币转移 (balance加减) 之前回调TokensToSend函数,转移之后回调TokensReceived函数 TokensToSend函数由转移代币的持有者 (可以是合约) 实现,TokensReceived由转移代币的接收者实现,这给了用户很大的自由,但也带来了一些问题,比如本次的攻击 0x1. 攻击分析1.0经典重入攻击 我们先不急着去看攻击过程,先复习下最简单的重入攻击(例如: The DAO,LendfMe等事件) 在这些\"经典\"攻击中,攻击者通过重入可以不断的使合约对其转账,直到退出\"递归\"时才更新一次的状态,他可能转账了1000个 Token (50个 * 20次),但是 balance 却只减少了50 如果攻击者可以在 transfer 的过程中重新调用 withdraw 函数,就可以实现重入。主要原因在于:合约中转账等操作先于余额状态的更新 ? 总结 简单来说,重入攻击就是打断施法,重点在于: ①在哪里打断施法 ②打断以后又做了些什么可以影响后续的结果 重入攻击有一个重要的特征,就是:先转账,后更新状态 对于上面这种传统的重入攻击,打断的便是「转账+记账」这一组合技,做的事情就是不断重新转账,以影响后续的记账结果 1.1 重入Uniswap (前方重点!) 那 Uniswap 如何重入呢?我们知道,Uniswap 是一个去中心化交易所(DEX),用户可以在上面交换代币
这并不像传统重入攻击的「转账+记账」模式。那它可以在哪里打断施法,又可以做哪些事情影响后续的结果呢? 代码分析 Uniswap 交易对合约中的交换函数(例如: ethToToken, TokenToToken...),原理基本一致,即保证交易池(交易对合约,后简称交易池)内两种币数量的乘积恒定(不考虑 Fee 的情况下[注1]),这些函数会先调用 getInputPrice 方法获取可以购买的另一种代币数量: 对应的公式为: 这里公式表示:池中原来储备量为ether : token,现在alice手里有token(put)个 token,ether(get)代表她能从池中买到多少个ETH 我们现在直接挑其中一个开锤,比如 tokenToETH (这个函数的功能是用 token 换 ETH): 我们可以看到这个函数先将 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 还未更新),攻击者正是利用这一点,每次薅一点羊毛,直到把人家羊给薅秃了: 从公式中可以看到,本来在一次 swap 后,token 和 ETH 的状态会同时变化(以维持乘积恒定),但是由于重入发生在发送ETH和更新 token 余额之间,直接被打断施法了,从而造成了悲剧 很简单的道理:如果正常的两次调用,第二次是 token↑ 使得 etherget↓,但是由于重入后状态没有更新(token 没变),所以相比\"正常情况\"下可以获得更多的 ETH
? 总结 总结来说,这次的攻击是由于: ① UniswapV1不兼容ERC777代币 → ② 从而导致合约代码可重入 → ③ 从而导致恒定乘积中变量状态不一致 → ④ 从而导致交易池资金被薅走 1.2 Real World 原理大概就是这样,管你听没听懂,继续看就完了,下面我们来看看real world中攻击者到底做了什么? 其实说到现在,更更聪明的读者,都可以跑去自己攻击了(友情提示:小心警察叔叔找上门哦 我们随便找一笔攻击者的Tx:0x32c83905db61047834f29385ff8ce8cb6f3d24f97e24e6101d8301619efee96e 可以看到攻击主要分为两个部分: 首先是一堆的自毁合约,看起来比较迷惑,但是查看这些自毁合约的调用者(GST[注1])就可以知道这是为了节省攻击的Gas(与攻击本身关系不大) 攻击过程: 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.3 Misc 这次的攻击事件,攻击者\"或许\"不是第一个发现漏洞的人 Uniswap 交易对合约中的重入漏洞,早在2019年1月12日ConsenSys 的审计报告中就被提及,而且在#14 commit中提到:合约中可能存在多种方式的重入攻击(包括利用 ERC-777 标准代币),并给出了简单的攻击过程 审计报告中提出:对于 UniswapV1 交易对合约中的 exchange 类型函数,无论 transfer 是发生在 token 余额状态变更前,还是 token 余额状态变更后,如果 transfer函数 可以重入,都会造成损失,并给出了后一种情况的简单攻击过程模拟
1.4 深入分析 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 < Δy 接着我们再考虑一个问题:同样先不考虑手续费,如果我们先用 2*Δy个 ETH 换出2*Δx个 imBTC,接着分两次,每次用Δx个 imBTC 去池中换 ETH,两次换出的 ETH 数量相等吗? 答案肯定是不相等的。原因就在于上面提到的凹函数这一性质(如图: 图中C是AB的中点)!
这两次交换,第一次换出的数量要大于第二次的数量。这就意味着,总共能换出2*Δy个 ETH,但是第一次能换出的 ETH 数量是大于Δy的! 如果能重来,那有没有可能,用 imBTC 换回 ETH 的过程中,两次交换都用第一次的结果? 没错,只要我们在第一笔转帐前打断施法 (打断点①),重新调用交换函数! 这样用 2x 个 ETH 换出 2y 个 imBTC,接着分两次每次都可以用 y 个 imBTC 换出 >x 个 ETH,最终换出比投入更多 (>2x) 的 ETH(在不考虑手续费的情况下 由于 Uniswap 是先计算可以换出代币的数量,再进行转账。这样就可以:重复使用第一段的价格(可以换出 >x 数量的 imBTC)
对于Uniswap是否可以实现这种,在第一笔转帐前重入呢? 很不幸的是,Uniswap的逻辑是先操作 ETH 再操作 代币,这意味着无论是用ETH买代币,还是用代币买ETH,都是先将ETH转出给用户,或是先将ETH转入给交易池,这便不符合我们上面提到的第一笔转账需要是ERC777代币 (这样我们才可以回调) 但是! Uniswap 还存在着 TokenToToken 这种方式,因为 V1 只支持 Token / ETH 交易池,所以这一函数的实现原理,就是: 先在第一个池中用 Token 换出 ETH,再在第二个池中用 ETH 换出 Token 可以看到 这个函数的实现逻辑是: 比如我们使用imBTC换DAI,它先将imBTC转给第一个交易池,然后将换出的ETH转给第二个池获取相应的DAI 太好了,这样不就有了ERC777代币作为第一笔转账的条件了嘛! 但是,我们要怎么把钱取走呢,方法是: 我们自己来创建第二个交易池,因为我们是这个交易池中代币的 owner, 所以我们可以mint出无限多的代币,来将池中的 ETH 拿空,而池中的 ETH 便是第一个我们在第一个交易池中的输出,也就是重入攻击的获利 实验结果: 这其实就是ConsenSys审计报告中提出的攻击方式 (但是并未实现 |回收 伏笔2 隐藏性质2: k值越小,曲线凹的程度越大,相等间距x的变化 (Δx),引起y的变化 (Δy?) 会越来越小!(如下图中,ΔAC >ΔA'C') 上面在第一个代币转账前打断我们已经验证过是可行的了(在没有手续费的情况下一定能获利 >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 回调函数重入获利(这种方式获利更简单易懂,也就是攻击者使用的方式) 0x2. 附录a. 攻击者是否获利最大化,如何获利更多? 这是一个比较困难的问题 从直觉上来看:攻击者每笔攻击交易重入的次数越多,使用的Ether数额越大,获利就越多,但是还要考虑实际交易对中真实的情况 因此小编只是做一些简单的尝试与统计: 优化的维度有:初始时攻击者投入的Ether数量,投入Token占比,重入的深度、攻击次数
实验1:获利与投入ETH数量及投入Token占比的关系
结论:投入ETH的数量越大,获利越大,并且增长的幅度也会有所加大。投入token的占比在0.5时接近最大值 实验2:获利与攻击次数的关系
我们知道随着攻击次数的增加,池中状态会一直向曲线的左侧移动,也就是说随着攻击次数的增加,获利会逐渐增大 上面两图是两种不同的方式,上图每次使用固定的100ETH进行攻击,下图初始用100ETH攻击,后续每次使用的ETH会累积上之前的获利。很显然累积上获利使池子更快的被掏空(40 次 / 175次) 结论:随着攻击次数的增加,获利会以指数趋势增加 实验3:重入次数与获利的关系
结论:随着重入次数的增加,理论上获利是会更多的,但是增长的幅度逐渐趋于平缓
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]关停合约(禁止转账等)
在#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 $ETH$UNI#defi##区块链安全# —- 编译者/作者:BlockSec 玩币族申明:玩币族作为开放的资讯翻译/分享平台,所提供的所有资讯仅代表作者个人观点,与玩币族平台立场无关,且不构成任何投资理财建议。文章版权归原作者所有。 |
[BlockSec DeFi攻击系列之六] 终而复始:Uniswap重入事件
2021-08-27 BlockSec 来源:区块链网络
LOADING...
相关阅读:
- Messari:NFT是如何保存在链上的?2021-08-27
- 一文详解Terra金融帝国的崛起2021-08-27
- DeFi 收益率和储蓄协议 Gro Protocol 将推出治理代币 GRO2021-08-27
- COTI 和 CFX 在新交易所上市后涨幅不错。长期看LINK有上涨到40美金的潜力2021-08-27
- DAOventures 在 Immunefi 上推出漏洞赏金计划2021-08-27