当前位置:主页 > 列表页 > 正文

[BlockSec DeFi攻击系列之六] 终而复始:Uniswap重入事件

2021-08-27 23:50 | 出处: BlockSec

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

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


往期回顾:

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

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

(3) [BlockSec DeFi攻击分析系列之三] 偷天换日:深度剖析Akropolis攻击事件

(4) [BlockSec DeFi攻击分析系列之四] 表里不一:Sanshu lnu 的 Memestake 合约遭袭事件分析

(5) [BlockSec DeFi攻击系列之五] 以假乱真:DODO V2 众筹池遭袭事件分析




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

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


阅读建议:

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

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

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


正文


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


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






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



0 x0. 背景介绍


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

提出时间:2017-11-20


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


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


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

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

更多细节了解,可以关注我们的相关工作: 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 函数 tokensToSendtokensReceived,这两个函数是干什么用的呢?过程很简单:在一笔转账交易过程中,balance 减少的地址(token holder)如果实现了 tokensToSend 接口函数,就先去执行 holder 的这个接口函数;同样的,balance增加的地址(token receiver)如果实现了 tokensReceived ,收到转账后会去执行receiver的这个接口函数[注3]

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



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


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


✓ 总结


ERC777是对ERC20的"升级"


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


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


0 x1. 攻击分析


1.0 经典重入攻击


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


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


相关文章