天天看點

智能合約安全——随機數

本次我們将帶大家了解智能合約中一個經常被用到的東西——随機數。

智能合約的開發中常常會用到随機數,例如 Lottery 和現在流行的 NFT 數字藏品的屬性等都需要用到随機數。目前來說常見的随機數擷取有兩種:使用區塊變量生成随機數,使用預言機來生成随機數。下面我們了解一下這兩者的特點:

1)使用區塊變量生成随機數

我們先了解一下常見的區塊變量有哪些:

block.basefee(uint):目前區塊的基本費用

block.chainid(uint):目前鍊 id

block.coinbase():目前區塊礦工位址 address payable

block.difficulty(uint):目前區塊難度

block.gaslimit(uint):目前區塊 gaslimit

block.number(uint):目前區塊号

block.timestamp(uint):自 Unix 紀元以來的目前區塊時間戳(以秒為機關)

blockhash(uint blockNumber) returns (bytes32):給定區塊的哈希,僅适用于 256 個最近的區塊

其中 block.difficulty, blockhash, block.number 和 block.timestamp 這四個是用得比較多的。由區塊資料生成的随機數可能會限制普通使用者預測随機數的可能性,但是并不能限制礦工作惡,礦工可以決定一個區塊是否被廣播,他們挖出了一個區塊不是一定要廣播出去也可以直接扔掉,這個就叫礦工的選擇性打包。他們可以持續嘗試生成随機數,直至得到想要的結果再廣播出去。當然,礦工會這樣做的前提是有足夠的的利益誘惑,例如可以獲得一個很大的獎勵池中的獎勵,是以使用區塊變量擷取随機數的方法更适合于一些随機數不屬于核心業務的應用。

2)使用預言機生成随機數

預言機是專門為生成随機數種子而搭建的鍊上或者鍊下的服務。除了使用第三方服務,也可以由 DApp 開發商自己搭建一個鍊下服務提供随機數,這種在鍊上擷取鍊下資料的場景通常是通過鍊上預言機的方式來實作。

當然這種方法也會有一些安全風險,例如依賴第三方給出的随機數種子的話同樣會存在第三方作弊或者受賄的情形,即使是自己搭建的随機數服務也可能因為故障等原因無法使用,項目方也有可能操控随機數對 DApp 的運作和使用者造成重大的損失。是以使用鍊下服務擷取随機數的方法依賴于是否有一個可信又穩定的第三方服務,如果有,那麼這個方法相較于使用區塊鍊變量生成随機數的方法,随機數的不可預測性會更強一些。

接下來我們還是用合約代碼來給大家示範弱随機數可能帶來的危害。

漏洞示例

智能合約安全——随機數

漏洞分析

首先我們先來了解一下代碼中的兩個函數,abi.encodePacked 和 keccak256:

l abi.encodePacked 對參數進行編碼,solidity 提供兩種編碼方法 encode 和 encodePacked,前者對每一個參數進行 32 位元組補齊,後者不進行補齊而是直接将待編碼參數連接配接起來。

l keccak256 雜湊演算法,可以将任意長度的輸入壓縮成 64 位的 16 進制的數,且哈希碰撞的機率近乎為 0。

接下來我們來看合約代碼,這個合約是一個猜數字赢以太的遊戲,我們可以看到,部署者使用上個區塊的區塊哈希和區塊時間作為随機數種子生成随機數,我們隻需要模拟他的随機數生成方法就可以得到獎勵。下面我們來看攻擊合約:

攻擊合約

智能合約安全——随機數

下面我們先來分析攻擊流程:

攻擊者調用Attack.attack()函數,它模拟了 GuessTheRandomNumber 合約中随機數的生成方式生成随機數後調用 guessTheRandomNumber.guess() 并将生成的随機數傳入,由于從 Attack.attack() 生成随機數到調用 guessTheRandomNumber.guess() 都是在同一區塊中完成的,且在同一區塊中 block.number 和 block.timestamp 這兩個參數是不變的,是以,Attack.attack() 和 guessTheRandomNumber.guess() 這兩個函數生成的随機數的結果是相同的,進而攻擊者可以順利通過 if(_guess == answer) 判斷得到獎勵。

修複建議

如果随機數屬于非核心業務的話可以使用未來區塊哈希來生成随機數也就是将猜數和領獎分開做異步處理。針對這次的漏洞合約寫一個優化版本,大家可以看下:

智能合約安全——随機數

添加了deadline 參數将 guess 和 claim 做異步處理後,在部署合約後的 72 小時内可以調用 guess() 猜随機數,在 72 小時後 guess() 關閉 claim() 開啟,玩家可以通過 claim() 來驗證自己是否猜中。當然,這個修複合約并不是完美的解決方案,正如前置知識中提到的,如果礦工來玩的話他可以在打包的時候知道自己是否猜中,如果猜中打包上鍊,如果沒有猜中放棄打包(相信沒有任何一個礦工願意為了得到一個以太而付出這麼大的代價)。是以最優的解決辦法還是接入知名預言機來擷取随機數。

如果想了解更多的智能合約和區塊鍊知識,歡迎到區塊鍊交流社群CHAINPIP社群,一起交流學習~

社群位址:https://www.chainpip.com/