Uniswap重入事件詳盡解析

2021-08-27 18:08:02

去中心化金融(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 標准代幣

Uniswap重入事件詳盡解析

  • 相關代幣的價值情況:

Uniswap重入事件詳盡解析

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

Uniswap重入事件詳盡解析

對於上面這條曲线,曲线上的每一點,就代表交易池中兩種代幣的一種狀態,比如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查找接口實現的合約地址,再去調用相關的接口函數

Uniswap重入事件詳盡解析

值得注意的是,ERC777 標准中提到,token實現應滿足 sender回調 → 更新狀態 → receiver回調 的順序,以防止發生重入事件(伏筆2),代碼中的表現為:

Uniswap重入事件詳盡解析

還有一些其他的特性,如:操作員概念、Mint與Burn完善了token的生命周期等等,與本次攻擊關系不大,暫且不展开

總結

ERC777是對ERC20的"升級"

它會在代幣轉移 (balance加減) 之前回調TokensToSend函數,轉移之後回調TokensReceived函數

TokensToSend函數由轉移代幣的持有者 (可以是合約) 實現,TokensReceived由轉移代幣的接收者實現,這給了用戶很大的自由,但也帶來了一些問題,比如本次的攻擊 

攻擊分析

  • 經典重入攻擊

我們先不急着去看攻擊過程,先復習下最簡單的重入攻擊(例如: The DAO,LendfMe等事件)

在這些"經典"攻擊中,攻擊者通過重入可以不斷的使合約對其轉账,直到退出"遞歸"時才更新一次的狀態,他可能轉账了1000個 Token (50個 * 20次),但是 balance 卻只減少了50

Uniswap重入事件詳盡解析

Uniswap重入事件詳盡解析

如果攻擊者可以在 transfer 的過程中重新調用 withdraw 函數,就可以實現重入。主要原因在於:合約中轉账等操作先於余額狀態的更新

總結

簡單來說,重入攻擊就是打斷施法,重點在於:

在哪裏打斷施法

打斷以後又做了些什么可以影響後續的結果

重入攻擊有一個重要的特徵,就是:先轉账,後更新狀態

對於上面這種傳統的重入攻擊,打斷的便是「轉账+記账」這一組合技做的事情就是不斷重新轉账,以影響後續的記账結果

  • 重入Uniswap

(前方重點!)

那 Uniswap 如何重入呢?我們知道,Uniswap 是一個去中心化交易所(DEX),用戶可以在上面交換代幣

【補充】UniswapV1 只實現了ETH和任意 token 之間的交換,對於 token 與 token 的交換,可以借助ETH中轉來實現

這並不像傳統重入攻擊的「轉账+記账」模式。那它可以在哪裏打斷施法,又可以做哪些事情影響後續的結果呢?

代碼分析

Uniswap 交易對合約中的交換函數(例如: ethToToken, TokenToToken...),原理基本一致,即保證交易池(交易對合約,後簡稱交易池)內兩種幣數量的乘積恆定(不考慮 Fee 的情況下[注1],這些函數會先調用 getInputPrice 方法獲取可以購买的另一種代幣數量:

Uniswap重入事件詳盡解析

對應的公式為:

Uniswap重入事件詳盡解析

這裏公式表示:池中原來儲備量為 ether : token ,現在alice手裏有 token(put) 個 token,ether(get) 代表她能從池中买到多少個ETH

我們現在直接挑其中一個开錘,比如 tokenToETH (這個函數的功能是用 token 換 ETH):

Uniswap重入事件詳盡解析

我們可以看到這個函數先將 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 還未更新),攻擊者正是利用這一點,每次薅一點羊毛,直到把人家羊給薅禿了:

Uniswap重入事件詳盡解析

從公式中可以看到,本來在一次 swap 後,token 和 ETH 的狀態會同時變化(以維持乘積恆定),但是由於重入發生在發送 ETH 和更新 token 余額之間,直接被打斷施法了,從而造成了悲劇

很簡單的道理:如果正常的兩次調用,第二次是 token↑ 使得 etherget ↓,但是由於重入後狀態沒有更新(token 沒變),所以相比"正常情況"下可以獲得更多的 ETH

【注1】公式相關推導過程(基本原理就是:保證交易池中兩種代幣一直滿足恆定的乘積

Uniswap重入事件詳盡解析

【注2】可能讀到這裏, 你還是感覺哪裏不對, 這是正常的, 如果有興趣, 你可以思考這樣幾個問題:

1) 這樣一定能獲利嗎, 需要滿足什么條件嗎?

2) 攻擊者獲利是最優的嗎, 還可以怎樣優化?

深入分析部分, 小編對這些問題做了一些簡單的嘗試, 如果有興趣, 不妨繼續看下去

(這已經是小編第4次復盤這次攻擊, 但還覺得很多問題沒有真正的搞清楚, 所以如果你沒看懂, 那也沒什么大不了的)

總結

總結來說,這次的攻擊是由於:

① UniswapV1不兼容ERC777代幣 → ② 從而導致合約代碼可重入 → ③ 從而導致恆定乘積中變量狀態不一致 → ④ 從而導致交易池資金被薅走

  • Real World

原理大概就是這樣,管你聽沒聽懂,繼續看就完了,下面我們來看看real world中攻擊者到底做了什么?

其實說到現在,更更聰明的讀者,都可以跑去自己攻擊了(友情提示:小心警察叔叔找上門哦

我們隨便找一筆攻擊者的Tx:0x32c83905db61047834f29385ff8ce8cb6f3d24f97e24e6101d8301619efee96e

Uniswap重入事件詳盡解析

可以看到攻擊主要分為兩個部分:

  1. 首先是一堆的自毀合約,看起來比較迷惑,但是查看這些自毀合約的調用者(GST[注1])就可以知道這是為了節省攻擊的Gas(與攻擊本身關系不大)

Uniswap重入事件詳盡解析

  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/)

  • Misc

這次的攻擊事件,攻擊者"或許"不是第一個發現漏洞的人

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."

Uniswap重入事件詳盡解析

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

Uniswap重入事件詳盡解析

接着我們再考慮一個問題:同樣先不考慮手續費,如果我們先用 2*Δy 個 ETH 換出 2*Δx 個 imBTC,接着分兩次,每次用 Δx 個 imBTC 去池中換 ETH,兩次換出的 ETH 數量相等嗎?

答案肯定是不相等的。原因就在於上面提到的凹函數這一性質(如圖: 圖中C是AB的中點)!

Uniswap重入事件詳盡解析

[注] 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(在不考慮手續費的情況下

Uniswap重入事件詳盡解析

由於 Uniswap 是先計算可以換出代幣的數量,再進行轉账。這樣就可以:重復使用第一段的價格(可以換出 >x 數量的 imBTC)

QUIZE:不考慮手續費這是穩賺不賠的买賣,但是如果引入了手續費,事情會怎么樣呢?這就有一定的條件了,要看到底薅的更多,還是虧得更多

> 有興趣可以自行推導

對於Uniswap是否可以實現這種,在第一筆轉帳前重入呢?

很不幸的是,Uniswap的邏輯是先操作 ETH 再操作 代幣,這意味着無論是用ETH买代幣,還是用代幣买ETH,都是先將ETH轉出給用戶,或是先將ETH轉入給交易池,這便不符合我們上面提到的第一筆轉账需要是ERC777代幣 (這樣我們才可以回調)

但是! Uniswap 還存在着 TokenToToken 這種方式,因為 V1 只支持 Token / ETH 交易池,所以這一函數的實現原理,就是: 先在第一個池中用 Token 換出 ETH,再在第二個池中用 ETH 換出 Token

Uniswap重入事件詳盡解析

可以看到 這個函數的實現邏輯是: 比如我們使用imBTC換DAI,它先將imBTC轉給第一個交易池,然後將換出的ETH轉給第二個池獲取相應的DAI

太好了,這樣不就有了ERC777代幣作為第一筆轉账的條件了嘛!

但是,我們要怎么把錢取走呢,方法是: 我們自己來創建第二個交易池,因為我們是這個交易池中代幣的 owner, 所以我們可以mint出無限多的代幣,來將池中的 ETH 拿空,而池中的 ETH 便是第一個我們在第一個交易池中的輸出,也就是重入攻擊的獲利 

實驗結果: 

Uniswap重入事件詳盡解析

這其實就是 ConsenSys  審計報告中提出的攻擊方式 (但是並未實現 | 回收 伏筆2

隱藏性質2:

k值越小,曲线凹的程度越大,相等間距x的變化 (Δx),引起y的變化 (Δy⇣) 會越來越小! (如下圖中,ΔAC > ΔA'C')

Uniswap重入事件詳盡解析

上面在第一個代幣轉账前打斷我們已經驗證過是可行的了(在沒有手續費的情況下一定能獲利 >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佔比指的是:還記得凹函數這一性質嗎,前半段下降快於後半段,這裏實驗的是前半段與後半段的比例對獲利的影響,其中佔比指的是前半段佔全部的比例

Uniswap重入事件詳盡解析

結論:投入ETH的數量越大,獲利越大,並且增長的幅度也會有所加大。投入token的佔比在0.5時接近最大值

實驗2:獲利與攻擊次數的關系

實驗參數:分別使用100 ETH / 累計 ETH兩種方式,嘗試增加攻擊次數

我們知道隨着攻擊次數的增加,池中狀態會一直向曲线的左側移動,也就是說隨着攻擊次數的增加,獲利會逐漸增大

Uniswap重入事件詳盡解析

Uniswap重入事件詳盡解析

上面兩圖是兩種不同的方式,上圖每次使用固定的100ETH進行攻擊,下圖初始用100ETH攻擊,後續每次使用的ETH會累積上之前的獲利。很顯然累積上獲利使池子更快的被掏空(40 次 / 175次)

結論:隨着攻擊次數的增加,獲利會以指數趨勢增加

實驗3:重入次數與獲利的關系

實驗參數:重入次數取 [2, 40],使用100 ETH

Uniswap重入事件詳盡解析

結論:隨着重入次數的增加,理論上獲利是會更多的,但是增長的幅度逐漸趨於平緩

【注】重入次數與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]關停合約(禁止轉账等)

Uniswap重入事件詳盡解析

【注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

鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播信息之目的,不構成任何投資建議,如有侵權行為,請第一時間聯絡我們修改或刪除,多謝。

推薦文章

Hyperliquid:從史上最有價值空投到高估值警鐘

毫無疑問,Hyperliquid 是近期加密市場炙手可熱的焦點,它通過一場震撼行業的空投,不僅引發...

coincaso
12 2天前

Arthur Hayes 新文聚焦 | 全球貨幣政策的真相,比特幣接下來何去何從?

作為一名宏觀經濟預測者,我試圖基於公开數據和當前事件,作出能夠指導投資組合資產配置的預測。我喜歡“...

coincaso
18 3天前

Ouroboros DeFi:為什么 Usual Money 被低估了?

前言:Ouroboros DeFi 方法論在Ouroboros DeFi收益基金,我們的投資策略始...

coincaso
14 3天前

WEEX 唯客交易所贊助臺北區塊鏈周 支持更多全球用戶Onboard Web3

第三屆臺北區塊鏈周(Taipei Blockchain Week, TBW)於 12 月 12-1...

coincaso
14 3天前

DeFi 3.0正在到來:下一代去中心化金融的演進

DeFi 3.0正逐步成形,多個前沿項目正在引領這一波技術浪潮。$HYPE、$PENDLE、$IN...

coincaso
16 6天前

生息穩定幣協議分析:安全要點與監管難題

穩定幣在加密行業的交易、支付以及儲蓄中佔據至關重要的地位。截至目前,穩定幣市值約 2000 億美元...

coincaso
24 6天前