你可以在我的Github上看到審計的代碼:https://github.com/merlox/casino-ethereum/blob/master/contracts/Casino.sol
以下就是我的合約Casino.sol的審計報告:
序言
在這份智能合約審計報告中將包含以下內容:
免責聲明
審計概覽和優良特性
對合約的攻擊
合約中發現的嚴重漏洞
合約中發現的中等漏洞
低嚴重性的漏洞
逐行評注
審計總結
1、免責聲明
審計不會對代碼的實用性、代碼的安全性、商業模式的適用性、商業模式的監管制度或任何其他有關合約適用性的說明以及合約在無錯狀態的行為作出聲明或擔保。審計文檔僅用于討論目的。
2、概述
該項目只有一個包含142行Solidity代碼的文件 Casino.sol 。所有的函數和狀態變量的注釋都按照標準說明格式(即Ethereum Nature Specification Format,縮寫為natspec,它是以太坊社區官方的代碼注釋格式說明,原文參考github:【https://github.com/ethereum/wiki/wiki/Ethereum-Natural-Specification-Format】,譯者注)進行編寫,這可以幫助我們快速地理解程序是如何工作。
該項目使用了一個中心化的服務實現了Oraclize API,來在區塊鏈上生成真正的隨機數字。
譯者注:
Oraclize是一種為智能合約和區塊鏈應用提供數據的獨立服務,官網:【http://www.oraclize.it】。因為類似于比特幣腳本或者以太坊智能合約這樣的區塊鏈應用無法直接獲取鏈外的數據,所以就需要一種可以提供鏈外數據并可以與區塊鏈進行數據交互的服務。Oraclize可以提供類似于資產/財務應用程序中的價格信息、可用于點對點保險的天氣信息或者對賭合約所需要的隨機數信息。
這里是指在這個項目的源代碼中引入了一個實現了Oraclize API的開源的Solidity代碼庫。
在區塊鏈上生成隨機數字是一個相當困難的課題,因為以太坊的核心價值之一就是可預測性,其目標是確保沒有未定義的值。
譯者注:
這里之所以說在區塊鏈上生成隨機數很困難,是因為,無論采用何種算法,都需要使用時間戳作為生成隨機數的“種子”(因為時間戳是計算機領域內唯一可以理論上保證“不會重復”的數值);而在智能合約中取得時間戳只能依賴某個節點(礦工)來做到。這就是說,合約中取得的時間戳是由運行其代碼的節點(礦工)的計算機本地時間決定的;所以這個節點(礦工)的可信度就成了最大的問題。理論上,這個本地時間是可以由惡意程序偽造的,所以這種方法被認為是“不安全的”。通行的做法是采用一個鏈外(off-chain)的第三方服務,比如這里使用的Oraclize,來獲取隨機數。因為Oraclize是一種公共基礎服務,不會針對特定的合約“作假”,所以這可以認為是“相對安全的”。
因為使用Oraclize可以在鏈外生成隨機數字,所以使用它來產生可信的數字被認為是一種很好的做法。 它實現了修飾符和一個回調函數,用于驗證信息是否來自可信實體。
此智能合約的目的是參與隨機抽獎,人們在1到9之間下注。當有10個人下注時,獎金會自動分配給贏家。每個用戶都有一個最低下注金額。
每個玩家在每局游戲中只能下一次注,并且只有在參與者數量達到要求時才會產生贏家號碼。
優秀特性
這個合約提供了一系列很好的功能性代碼:
3、對合約進行的攻擊
為了檢查合約的安全性,我們測試了多種攻擊,以確保合約是安全的并遵循了最佳實踐。
重入攻擊(Reentrancy attack)
此攻擊通過遞歸地調用ERC20代幣中的 call.value() 方法來提取合約中的以太幣,如果用戶在發送以太幣之后才更新發送者的 balance (即賬戶余額,譯者注)的話,攻擊就會生效。
當你調用一個函數將以太幣發送給合約時,你可以使用fallback函數再次執行該函數,直到以太幣被從合約中提取出來。
由于該合約使用了 transfer() 而不是 call.value() ,因此不存在重入攻擊的風險;因為transfer函數只允許使用2300 gas,這只夠用來產生事件日志數據并在失敗時拋出異常。這樣就無法遞歸調用發送者函數,從而避免了重入攻擊。
因為transfer函數只會在每局游戲結束,向贏家分發獎勵時才會被調用一次,所以重入式攻擊在這里不會導致任何問題。
請注意,調用此函數的條件是投注次數大于或等于10次,但這個投注次數只有在 distributePrizes() 函數結束時才會被重置為0,這是有風險的;因為理論上是可以在投注次數被清零之前調用該函數并執行所有邏輯的。
所以我的建議是在函數開始時就更新條件、將投注次數設置為0,以確保 distributePrizes() 在被超出預期地多次調用時不會產生實際效果。
數值溢出(Over and under flows)
當一個 uint256 類型的變量值超出上限2**256(即2的256次方,譯者注)時會發生溢出。其結果是變量值變為0,而不是更大。
例如,如果你想把一個unit類型的變量賦予大于2**256的值,它會簡單地變為0,這是危險的。
另一方面,當你從0值中減去一個大于0的數字時,則會發生下溢出(underflow)。例如,如果你用0減去1,結果將是2**256,而不是-1。
在處理以太幣的時候,這非常危險;然而在這個合約中并不存在減法操作,所以也不會有下溢出的風險。
唯一可能發生溢出的情況是當你調用 bet() 向某個數字下注時, totalBet 變量的值會相應增加:
有人可能會發送大量的以太幣而導致累加結果超過2**256,這會使totalBet變為0。這當然是不大可能發生的,但風險是有的。
所以我推薦使用類似于[OpenZeppelin’s SafeMath.sol]這樣的庫。它可以使你的計算處理更安全,免去發生溢出(overflow或者underflow)的風險。
可以將其導入來使用,對uint256類型激活它,然后使用 .mul() 、 .add() 、 .sub() 和 .div() 這些函數。例如:
import './SafeMath.sol';
contract Casino {
using SafeMath for uint256; function example(uint256 _value) {
uint number = msg.value.add(_value);
}
}
重放攻擊(Replay attack)
重放攻擊是指在像以太坊這樣的區塊鏈上發起一筆交易,而后在像以太坊經典這樣的另一個鏈上重復這筆交易的攻擊。(就是說在主鏈上創建一個交易之后,在分岔鏈上重復同樣的交易。譯者注。)
以太幣會像普通的交易那樣,從一個鏈轉移到另一個鏈。
基于由Vitalik Buterin提出的EIP 155【https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md】,從Geth的1.5.3版本和Parity的1.4.4版本開始,已經增加了對這個攻擊的防護。
譯者注:
EIP,即Ethereum Improvement Proposal(以太坊改進建議),官方地址【https://github.com/ethereum/EIPs】是由以太坊社區所共同維護的以太坊平臺標準規范文檔,涵蓋了基礎協議規格說明、客戶端API以及合約標準規范等等內容。
所以使用合約的用戶們需要自己升級客戶端程序來保證針對這個攻擊的安全性。
重排攻擊(Reordering attack)
這種攻擊是指礦工或其他方試圖通過將自己的信息插入列表(list)或映射(mapping)中來與智能合約參與者進行“競爭”,從而使攻擊者有機會將自己的信息存儲到合約中。
當一個用戶使用 bet() 函數下注以后,因為實際的數據是存儲在鏈上的,所以任何人都可以簡單地通過調用公有狀態變量 playerBetsNumber 這個mapping看到所下注的數字。
這個mapping是用來表示每個人所選擇的數字的,所以,結合交易數據,你就可以很容易地看到他們各自下注了多少以太幣。這可能會發生在 distributePrizes() 函數中,因為它是在隨機數生成處理的回調中被調用的。
因為這個函數起作用的條件在其結束之前才會被重置,所以這就有了重排攻擊(reordering attack)的風險。
因此,我的建議就像我之前談的那樣:在 distributePrizes() 函數開始時就重置下注人數來避免其產生非預期的行為。
短地址攻擊(Short address attack)
這種攻擊是由Golem團隊發現的針對ERC20代幣的攻擊:
一個用戶創建一個空錢包,這并不難,它只是一串字符,例如:【0xiofa8d97756as7df5sd8f75g8675ds8gsdg0】
然后他使用把地址中的最后一個0去掉的地址來購買代幣:也就是用【0xiofa8d97756as7df5sd8f75g8675ds8gsdg】作為收款地址來購買1000代幣。
如果代幣合約中有足夠的余額,且購買代幣的函數沒有檢查發送者地址的長度,以太坊虛擬機會在交易數據中補0,直到數據包長度滿足要求
以太坊虛擬機會為每個1000代幣的購買返回256000代幣。這是一個虛擬機的bug,并且仍未被修復。所以如果你是一個代幣合約的開發者,請確保對地址長度進行了檢查。
但我們這個合約因為并不是ERC20代幣合約,所以這種攻擊并不能適用。
你可以參考這篇文章【http://vessenes.com/the-erc20-short-address-attack-explained/】來獲得更多關于這種攻擊的信息。
4、合約中發現的嚴重漏洞
審計中并未發現嚴重漏洞。
5、合約中發現的中等漏洞
checkPlayerExists() 應該是一個常態(constant)函數,然而實際上它并不是。因此這增加了調用這個函數的gas消耗,當有大量對此函數的調用發生時會產生很大的問題。
應該把它改為常態函數來避免昂貴的消耗gas的執行。
譯者注:
Solidity語言中的常態(constant)函數,指的是在運行時不會改變合約狀態的函數,也就是不會改變合約級別的狀態變量(state variable)的值的函數。因為狀態變量的更改是會保存到鏈上的,所以對狀態變量的更改都要消耗gas(來支付給礦工),這是非常昂貴的。在本例中,因為 checkPlayerExists() 函數中訪問了狀態變量 playerBetsNumber 來判斷是否已經有人下過注了,雖然這是個合約級別的變量,但這個函數并沒有改變它的值,所以這個函數應該聲明為 constant 以節省其對gas的消耗。
6、低嚴重性的漏洞
assert() 和 require() 大體上是相同的,但assert函數一般用來在更改合約狀態之后做校驗,而require通常在函數的開頭用做輸入參數的檢查。
7、逐行評注
第1行:你在版本雜注(pragma version)中使用了脫字符號(^)來指定使用高于 0.4.11 版本的編譯器。
這不是一個好實踐。因為大版本的變化可能會使你的代碼不穩定,所以我推薦使用一個固定的版本,比如‘0.4.11’。
第14行:你定義了一個 uint 類型的變量 totalBet ,這個變量名是不合適的,因為它保存的是所有下注的合計值。我推薦使用 totalBets 作為變量名,而不是 totalBet 。
第24行:你用大寫字母定義了一個常量(constant variable),這是一個好實踐,可以使人知道這是個固定的、不可變的變量。
第30行:就像我之前提到的,你定義了一個未使用的數組 player 。如果你不打算使用它,就把它刪除。
第60行:函數 checkPlayerExists() 應該被聲明為 constant 。因為它并沒有更改合約狀態,把它聲明為 constant 可以節省下每次運行它所要消耗的gas。
即使函數默認是public類型,但顯式地給函數指定類型仍然是一個好實踐,它可以避免任何困惑。這里可以在這個函數聲明的末尾確切地加上public聲明。
第61行:你沒有檢查輸入參數 player 被正常傳入且格式正確。請確保在函數開頭使用 require(player != address(0)); 語句來檢查傳入地址是否為0。為了以防萬一,最好也要檢查地址的長度是否符合要求來應對短地址攻擊。
第69行:同樣建議給 bet() 函數加上可見度(visibilty)關鍵字 public 來避免任何困惑,以明確應該如何使用此函數。
第72行:使用 require() 來檢查函數輸入參數,而不是 assert() 。
同樣的,在函數開頭,一般更經常使用 require() 。請把所有在函數開頭使用的 assert() 改為 require() 。
第90行:你使用了一個對 msg.value 的簡單合計,在value值很大時這會導致溢出。所以我建議你每次對數值進行運算時都要檢查是否會溢出。
第98行: generateNumberWinner() 應該是 internal 函數,因為你肯定不希望任何人都可以從合約以外執行它。
譯者注:
在Solidity語言中, internal 關鍵字的效果,與面向對象語言比如C++、Java中的protected類型基本一致,此關鍵字限定的函數或者狀態變量,僅在當前合約及當前合約的子合約(contacts deriving from this contract)中可以訪問。 private 關鍵字則與其他語言中的此關鍵字相同,由其限定的函數或者狀態變量僅在當前合約中可以訪問。
第103行:你把 oraclize_newRandomDSQuery() 函數的結果保存在了一個bytes32類型的變量中。調用callback函數并不需要這么做,而且你也沒有在其他地方再用到這個變量,所以我建議不要用變量保存這個函數的返回值。
第110行: __callback() 函數應該聲明為 external ,因為你只希望它從外部被調用。
譯者注:
在Solidity中,函數關鍵字 public 和 external 在gas的消耗上是有區別的。因為 public 的函數既可以在合約外調用,又可以在合約內調用,所以虛擬機會在運行時為其分配內存,拷貝其所用到的所有變量。而 external 的函數只允許從合約外部進行調用,其調用會直接從calldata(即函數調用的二進制字節碼數據)中獲取參數,虛擬機不會為其分配內存并拷貝變量值,所以其gas消耗比 public 的函數要低很多。
第117行:這里的 assert() 應該使用 require() ,就像我先前解釋的那樣。
第119行:你使用了 sha3() 函數,但這并不是一個好的實踐。實際的算法使用的是keccak256,并不是sha3。所以我建議這里更明確地改為使用 keccak256() 。
第125行: distributePrizes() 函數應該被聲明為 internal 。
譯者注:
此函數與第98行的 generateNumberWinner() 函數一樣,聲明為 internal 或者 private 都是可以的。區別僅在于你希不希望子合約中可以使用它們。
第129行:盡管你在這里用了一個變長數組的大小來控制循環次數,但其實也沒有多糟糕,因為獲勝者的數量被限制為小于100。
8、審計總結
總體上講,這個合約的代碼有很好的注釋,清晰地解釋了每個函數的目的。
下注和分發獎勵的機制非常簡單,不會帶來什么大問題。
我最終的建議是需要更加注意函數的可見性聲明,因為這對于明確函數應該供誰來執行的問題非常重要。然后就是需要在編碼中考慮 assert 、 require 和 keccak 的使用上的最佳實踐。
這是一個安全的合約,可以在其運行期間保證資金安全。