寫在前面:HiBlock區塊鏈社區成立了翻譯小組,翻譯區塊鏈相關的技術文檔及資料,本文爲Solidity文檔翻譯的第五部分《安全考量》,特發佈出來邀請solidity愛好者、開發者作公開的審校,您能夠添加微信baobaotalk_com,驗證輸入「solidity」,而後將您的意見和建議發送給咱們,也能夠在文末「留言」區留言,有效的建議咱們會採納及合併進下一版本,同時將送一份小禮物給您以示感謝。數組
儘管在一般狀況下編寫一個按照預期運行的軟件很簡單, 但想要確保沒有人可以以出乎意料的方式使用它就困難多了。安全
在 Solidity 中,這一點尤其重要,由於智能合約能夠用來處理通證,甚至有多是更有價值的東西。 除此以外,智能合約的每一次執行都是公開的,並且源代碼也一般是容易得到的。微信
固然,你老是須要考慮有多大的風險: 你能夠將智能合約與公開的(固然也對惡意用戶開放)、甚至是開源的網絡服務相比較。 若是你只是在某個網絡服務上存儲你的購物清單,則可能沒必要太在乎, 但若是你使用那個網絡服務管理你的銀行賬戶, 那就須要特別小心了。網絡
本節將列出一些陷阱和通常性的安全建議,但這絕對不全面。 另外,請時刻注意的是即便你的智能合約代碼沒有 bug, 但編譯器或者平臺自己可能存在 bug。 一個已知的編譯器安全相關的 bug 列表能夠在 已知bug列表 找到, 這個列表也能夠用程序讀取。 請注意其中有一個涵蓋了 Solidity 編譯器的代碼生成器的 bug 懸賞項目。app
咱們的文檔是開源的,請一如既往地幫助咱們擴展這一節的內容(況且其中一些例子並不會形成損失)!模塊化
私有信息和隨機性函數
在智能合約中你所用的一切都是公開可見的,即使是局部變量和被標記成 private 的狀態變量也是如此。學習
若是不想讓礦工做弊的話,在智能合約中使用隨機數會很棘手 (譯者注:在智能合約中使用隨機數很難保證節點不做弊, 這是由於智能合約中的隨機數通常要依賴計算節點的本地時間獲得, 而本地時間是能夠被惡意節點僞造的,所以這種方法並不安全。 通行的作法是採用 鏈外的第三方服務,好比 Oraclize 來獲取隨機數)。區塊鏈
重入ui
任何從合約 A 到合約 B 的交互以及任何從合約 A 到合約 B 的 以太幣的轉移,都會將控制權交給合約 B。 這使得合約 B 可以在交互結束前回調 A 中的代碼。 舉個例子,下面的代碼中有一個 bug(這只是一個代碼段,不是完整的合約):
pragma solidity ^0.4.0;
// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract Fund {
/// Mapping of ether shares of the contract. mapping(address => uint) shares; * /// Withdraw your share.* ** function** withdraw() **public **{ if (msg.sender.send(shares[msg.sender])) shares[msg.sender] = 0; }
}
這裏的問題不是很嚴重,由於有限的 gas 也做爲 send 的一部分,但仍然暴露了一個缺陷: 以太幣Ether 的傳輸過程當中老是能夠包含代碼執行,因此接收者能夠是一個回調進入 withdraw 的合約。 這就會使其屢次獲得退款,從而將合約中的所有 以太幣 取走。 特別地,下面的合約將容許一個攻擊者屢次獲得退款,由於它使用了 call ,默認發送全部剩餘的 gas。
pragma solidity ^0.4.0;
// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract Fund { /// Mapping of ether shares of the contract. ** mapping**(address => uint) shares; * /// Withdraw your share.* function withdraw() public { ** if** (msg.sender.call.value(shares[msg.sender])()) shares[msg.sender] = 0; }
}
爲了不重入,你可使用下面撰寫的「檢查-生效-交互」(Checks-Effects-Interactions)模式:
pragma solidity ^0.4.11;
contract Fund { * /// Mapping of ether shares of the contract.* ** mapping**(address => uint) shares; * /// Withdraw your share.* ** function** withdraw() public { ** var** share = shares[msg.sender]; shares[msg.sender] = 0; msg.sender.transfer(share); }
}
請注意重入不只是 以太幣 傳輸的其中一個影響,還包括任何對另外一個合約的函數調用。 更進一步說,你也不得不考慮多合約的狀況。 一個被調用的合約能夠修改你所依賴的另外一個合約的狀態。
gas 限制和循環
必須謹慎使用沒有固定迭代次數的循環,例如依賴於 存儲 值的循環: 因爲區塊 gas 有限,交易只能消耗必定數量的 gas。 不管是明確指出的仍是正常運行過程當中的,循環中的數次迭代操做所消耗的 gas 都有可能超出區塊的 gas 限制,從而致使整個合約在某個時刻驟然中止。 這可能不適用於只被用來從區塊鏈中讀取數據的 constant 函數。 儘管如此,這些函數仍然可能會被其它合約看成 鏈上 操做的一部分來調用,並使那些操做驟然中止。 請在合約代碼的說明文檔中明確說明這些狀況。
發送和接收以太幣
目前不管是合約仍是「外部帳戶」都不能阻止有人給它們發送以太幣。合約能夠對一個正常的轉帳作出反應並拒絕它,但還有些方法能夠不經過建立消息來發送以太幣。其中一種方法就是單純地向合約地址「挖礦」,另外一種方法就是使用 selfdestruct(x) 。
若是一個合約收到了 |ether|(且沒有函數被調用),就會執行 fallback 函數。 若是沒有 fallback 函數,那麼 |ether| 會被拒收(同時會拋出異常)。 在 fallback 函數執行過程當中,合約只能依靠此時可用的「gas 津貼」(2300 gas)來執行。 這筆津貼並不足以用來完成任何方式的存儲訪問。 爲了確保你的合約能夠經過這種方式收到以太幣,請你覈對 fallback 函數所需的 gas 數量 (在 Remix 的「詳細」章節會舉例說明)。
有一種方法能夠經過使用 addr.call.value(x)() 向接收合約發送更多的 gas。 這本質上跟 addr.transfer(x) 是同樣的, 只不過前者發送全部剩餘的 gas,而且使得接收者有能力執行更加昂貴的操做 (它只會返回一個錯誤代碼,並且也不會自動傳播這個錯誤)。 這可能包括回調發送合約或者你想不到的其它狀態改變的狀況。 所以這種方法不管是給誠實用戶仍是惡意行爲者都提供了極大的靈活性。
若是你想要使用 address.transfer 發送以太幣,你須要注意如下幾個細節:
若是接收者是一個合約,它會執行本身的 fallback 函數,從而能夠回調發送以太幣的合約。
若是調用的深度超過 1024,發送以太幣也會失敗。因爲調用者對調用深度有徹底的控制權,他們能夠強制使此次發送失敗; 請考慮這種可能性,或者使用 send 而且確保每次都覈對它的返回值。 更好的方法是使用一種接收者能夠取回以太幣的方式編寫你的合約。
發送 以太幣也可能由於接收方合約的執行所需的 gas 多於分配的 gas 數量而失敗 (確切地說,是使用了 require , assert, revert , throw 或者由於這個操做過於昂貴) - 「gas 不夠用了」。 若是你使用 transfer 或者 send 的同時帶有返回值檢查,這就爲接收者提供了在發送合約中阻斷進程的方法。 再次說明,最佳實踐是使用 「取回」模式而不是「發送」模式。
調用棧深度
外部函數調用隨時會失敗,由於它們超過了調用棧的上限 1024。 在這種狀況下,Solidity 會拋出一個異常。 惡意行爲者也許可以在與你的合約交互以前強制將調用棧設置成一個比較高的值。
請注意,使用 .send() 時若是超出調用棧 並不會 拋出異常,而是會返回 false。 低級的函數好比 .call(),.callcode() 和 .delegatecall() 也都是這樣的。
tx.origin
永遠不要使用 tx.origin 作身份認證。假設你有一個以下的錢包合約:
pragma solidity ^0.4.11;
// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contractTxUserWallet { address owner;
** function** TxUserWallet()** public** { owner = msg.sender; }
** function transferTo(address dest, uint amount) public** { require(tx.origin == owner); dest.transfer(amount); }
}
如今有人欺騙你,將 以太幣發送到了這個惡意錢包的地址:
**pragma **solidity ^0.4.11;
interface TxUserWallet { ** function** transferTo(address dest, uint amount) public;
}
contract TxAttackWallet { address owner;
** function** TxAttackWallet()** public** { owner = msg.sender; }
** function**() public { TxUserWallet(msg.sender).transferTo(owner,msg.sender.balance); }
}
若是你的錢包經過覈查 msg.sender 來驗證發送方身份,你就會獲得惡意錢包的地址,而不是全部者的地址。 可是經過覈查 tx.origin ,獲得的就會是啓動交易的原始地址,它仍然會是全部者的地址。 惡意錢包會當即將你的資金抽出。
細枝末節
在 for (var i = 0; i < arrayName.length; i++) { ... } 中, i 的類型會變爲 uint8 , 由於這是保存 0 值所需的最小類型。若是數組超過 255 個元素,則循環不會終止。
constant 關鍵字並非編譯器強制的,另外也不是以太坊虛擬機強制的。
所以一個「聲明」爲 constant 的函數可能仍然會發生狀態發生變化。 - 不佔用完整 32 字節的類型可能包含「髒高位」。這在當你訪問 msg.data 的時候尤其重要 —— 它帶來了延展性風險:
限定以太幣的數量
限定 存儲 在一個智能合約中 以太幣(或者其它通證)的數量。 若是你的源代碼、編譯器或者平臺出現了 bug,可能會致使這些資產丟失。 若是你想控制你的損失,就要限定 以太幣 的數量。
保持合約簡練且模塊化
保持你的合約短小精煉且易於理解。 找出無關於其它合約或庫的功能。 有關源碼質量能夠採用的通常建議: 限制局部變量的數量以及函數的長度等等。 將實現的函數文檔化,這樣別人看到代碼的時候就能夠理解你的意圖,並判斷代碼是否按照正確的意圖實現。
使用「檢查-生效-交互」(Checks-Effects-Interactions)模式
大多數函數會首先作一些檢查工做(例如誰調用了函數,參數是否在取值範圍以內,它們是否發送了足夠的 以太幣 ,用戶是否具備通證等等)。 這些檢查工做應該首先被完成。
第二步,若是全部檢查都經過了,應該接着進行會影響當前合約狀態變量的那些處理。 與其它合約的交互應該是任何函數的最後一步。
早期合約延遲了一些效果的產生,爲了等待外部函數調用以非錯誤狀態返回。 因爲上文所述的重入問題,這一般會致使嚴重的後果。
請注意,對已知合約的調用反過來也可能致使對未知合約的調用,因此最好是一直保持使用這個模式編寫代碼。
包含故障-安全(Fail-Safe)模式
儘管將系統徹底去中心化能夠省去許多中間環節,但包含某種故障-安全模式仍然是好的作法,尤爲是對於新的代碼來講:
你能夠在你的智能合約中增長一個函數實現某種程度上的自檢查,好比「 以太幣是否會泄露?」, 「通證的總和是否與合約的餘額相等?」等等。 請記住,你不能使用太多的 gas,因此可能須要經過 鏈外 計算來輔助。
若是自檢查沒有經過,合約就會自動切換到某種「故障安全」模式, 例如,關閉大部分功能,將控制權交給某個固定的可信第三方,或者將合約轉換成一個簡單的「退回個人錢」合約。
使用形式化驗證能夠執行自動化的數學證實,保證源代碼符合特定的正式規範。 規範仍然是正式的(就像源代碼同樣),但一般要簡單得多。
請注意形式化驗證自己只能幫助你理解你作的(規範)和你怎麼作(實際的實現)的之間的差異。 你仍然須要檢查這個規範是不是想要的,並且沒有漏掉由它產生的任何非計劃內的效果。
本文內容來源於HiBlock區塊鏈社區翻譯小組,感謝全體譯者的辛苦工做。
如下是咱們的社區介紹,歡迎各類合做、交流、學習:)