安全考量

本文英文版原地址:http://solidity-cn.readthedoc...
由於本人英語能力有限,使用谷歌翻譯,本篇好多地方不通順。有能力的能夠直接看英文版本。html

雖然構建按預期工做的軟件一般很是容易,但要檢查人們以不能預料到的方式使用它,要困可貴多。數組

在Solidity中,這更加劇要,由於您可使用智能合約來處理令牌(tokens)或可能更有價值的東西。此外,智能合約的每一次執行都在公開場合進行,除此以外,源代碼一般是可用的。安全

固然,你須要考慮有多大的風險:你能夠將智能合約與對公衆開放的Web服務(以及對惡意行爲者)以及甚至開放源代碼進行比較。若是您只將該購物清單存儲在該Web服務上,則可能沒必要太在乎,但若是您使用該Web服務管理您的銀行帳戶,則應該更加當心。app

本節將列出一些陷阱和通常安全建議,但固然可能永遠不會完整。另外,請記住,即便您的智能合約代碼沒有缺陷,編譯器或平臺自己也可能有錯誤。能夠在已知錯誤列表中找到編譯器的一些公開已知安全相關錯誤列表,這些錯誤也是機器可讀的。請注意,有一個錯誤賞金程序涵蓋了Solidity編譯器的代碼生成器。ide

與往常同樣,使用開源文檔,請幫助咱們擴展本節(特別是,一些示例不會受到傷害)!模塊化

陷阱函數

私人信息和隨機性
您在智能合約中使用的全部內容都是公開可見的,即便是標記爲private的本地變量和狀態變量。區塊鏈

若是你不但願礦工可以做弊,在智能合同中使用隨機數字是很是嚴峻的一件事。ui

重入(Re-Entrancy)開放源代碼

合同(A)與另外一合同(B)的任何互動以及乙方的任何轉讓均將控制移交給該合同(B)。 這使得在這個交互完成以前B能夠回調A. 舉一個例子,下面的代碼包含一個bug(它只是一個片斷而不是一個完整的合同):

pragma solidity ^0.4.0;

//這個函數包含一個bug---不要使用

contract Fund {
    /// 合約的映射。
    mapping(address => uint) shares;
    /// 撤回你的份額。
    function withdraw() public {
        if (msg.sender.send(shares[msg.sender]))
            shares[msg.sender] = 0;
    }
}

這裏的問題不是太嚴重,由於做爲send的一部分,gas是有限的,但它仍然暴露出一個弱點:以太(Ether)轉移老是包含代碼執行,因此接收方多是一個合約,能夠調回撤回。 這將讓它獲得屢次退款,並基本上檢索合同中的全部以太網。 特別是,如下合同將容許攻擊者屢次withdraw,由於它使用默認轉發全部剩餘gascall

pragma solidity ^0.4.0;

// 這個函數包含一個bug---不要使用
contract Fund {
    mapping(address => uint) shares;
    function withdraw() public {
        if (msg.sender.call.value(shares[msg.sender])())
            shares[msg.sender] = 0;
    }
}

爲了不從新入侵(Re-Entrancy),您可使用Checks-Effects-Interactions模式,以下所述:

pragma solidity ^0.4.11;

contract Fund {
    mapping(address => uint) shares;
    function withdraw() public {
        var share = shares[msg.sender];
        shares[msg.sender] = 0;
        msg.sender.transfer(share);
    }
}

請注意,重入不只影響以太轉移,還影響其餘合同上的任何功能調用的。 此外,您還必須考慮多合同狀況。 被called的合同能夠修改您依賴的另外一份合同的狀態。

Gas限制和循環
沒有固定迭代次數的循環(例如取決於存儲值的循環)必須當心使用:因爲區塊中gas限制,交易只能消耗必定量的gas。 不管是明確的仍是僅僅因爲正常的操做,循環中的迭代次數可能會超出區塊中gas限制,這會致使整個合同在某個點停滯。 這可能不適用於僅用於從區塊鏈讀取數據的constant函數。 儘管如此,這些功能可能會被其餘合同做爲鏈上操做的一部分進行調用,並將其拖延。 請在合同文件中明確說明這些狀況。

發送和接收Ether

  • 合同和「外部帳戶」都不能阻止有人送他們Ether。 合同能夠做出反應並拒絕按期轉移,但有些方法能夠在不建立消息呼叫的狀況下移動Ether。 一種方法是簡單地"mine to"合同地址和第二種方式使用selfdestruct(x)
  • 若是合同收到Ether(沒有調用函數),則執行回退函數。 若是它沒有後備功能,Ether將被拒絕(經過拋出異常)。 在執行回退功能時,合同只能依靠當時可用的「 gas津貼」(2300 gas)。 這筆津貼不能以任何方式訪問存儲。 爲確保您的合同可以以此方式接收Ether,請檢查故障預置功能的gas請求(例如,在Remix的「詳細信息」部分中)。
  • 有一種方法可使用addr.call.value(x)()將更多gas轉發給接收合同。 這與addr.transfer(x)基本相同,只是它轉發了全部剩餘的gas並打開了接收方執行更昂貴的操做的能力(而且它只返回失敗代碼而且不會自動傳播錯誤)。 這可能包括回撥發送合約或您可能沒有想到的其餘狀態更改。 所以它爲誠實用戶提供了極大的靈活性,同時也爲惡意行爲者提供了很大的靈活性
  • 若是你想使用address.transfer發送Ether,有一些細節須要注意:

    1.若是收件人是合同,它將致使其執行回退功能,從而能夠回撥發送合同。
    2.發送Ether可能會因呼叫深度超過1024而失敗。因爲caller徹底控制呼叫深度,所以可能會強制傳送失敗; 考慮這種可能性或使用發送,並確保始終檢查其返回值。 更好的是,用收款人能夠取消Ether的模式寫下你的合同。
    3.發送Ether也可能失敗,由於收貨合同的執行須要的gas超過了分配的數量(明確地經過使用要求,斷言,還原,拋出或由於操做太昂貴) - 它「耗盡gas」(OOG)。 若是您使用轉帳或發送返款金額支票,這可能爲收件人提供阻止發送合同中進度的手段。 一樣,這裏的最佳作法是使用「撤回」模式而不是「發送」模式。

Callstack深度
外部函數調用可能會隨時失敗,由於它們超過了1024的最大調用堆棧。在這種狀況下,Solidity會引起異常。 惡意行爲者在與你的合同進行交互以前可能會強制調用堆棧的high value。

請注意,若是調用堆棧已耗盡,則.send()不會引起異常,但在此狀況下返回false。 低等級函數.call().callcode().delegatecall()的行爲方式相同。

tx.origin
切勿使用tx.origin進行受權。 假設你有這樣的錢包合約:

pragma solidity ^0.4.11;

//這個函數包含一個bug---不要使用
contract TxUserWallet {
    address owner;

    function TxUserWallet() public {
        owner = msg.sender;
    }

    function transferTo(address dest, uint amount) public {
        require(tx.origin == owner);
        dest.transfer(amount);
    }
}

如今有人欺騙你將ether發送到這個攻擊錢包的地址:

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關鍵字目前不禁編譯器強制執行。此外,它不是由EVM強制執行的,因此「聲稱」保持不變的合約功能可能仍會致使狀態發生變化。
  • 不佔用完整32字節的類型可能包含「髒高位」。 有一點很是重要,若是您訪問msg.data(這構成了可塑性風險):您可使用原始字節參數爲0xff0000010x00000001調用函數f(uint8 x)。這兩種方法都是與合同相關聯的,並且它們看起來都像x相關的數字1。可是msg.data會有所不一樣,因此若是您使用keccak256(msg.data)作任何事情,您將獲得不一樣的結果。

推薦作法
限制Ether的量。
限制能夠存儲在智能合約中的Ether(或其餘tokens)數量。 若是您的源代碼,編譯器或平臺有錯誤,這些資金可能會丟失。 若是你想限制你的損失,限制Ether的數量。

保持小型化和模塊化
保持合同規模小,易於理解。 在其餘合同或庫中找出無關的功能。 關於源代碼質量的通常建議固然適用:限制局部變量的數量,函數的長度等等。 記錄你的功能,以便其餘人能夠看到你的意圖是什麼,以及它是否與代碼不一樣。

使用檢查 - 效果 - 互動(Checks-Effects-Interactions )模式
大多數函數將首先執行一些檢查(誰調用函數,是範圍內的參數,他們是否發送了足夠多的Ether,人員是否具備tokens等)。 這些檢查應該先完成。

做爲第二步,若是全部檢查都經過了,則應該對當前合同的狀態變量產生影響。 與其餘合同的交互應該是任何功能的最後一步。

早期合同延遲了一些效果,並等待外部函數調用以非錯誤狀態返回。 因爲上述重入問題,這一般是一個嚴重的錯誤。

請注意,對已知合同的調用也可能致使對未知合同的調用,因此最好始終應用此模式。

包含故障安全模式
在使系統徹底分散化的同時將刪除任何中介,這多是一個好主意,特別是對於新代碼,可能包含某種故障安全機制:

您能夠在智能合約中添加一個函數,執行一些自我檢查,如「有任何Ether泄露?」,「tokens的總和是否等於合同的餘額?」 或相似的東西。 請記住,你不能使用太多的gas,因此經過脫鏈(off-chain)計算可能須要幫助。

若是自檢失敗,合同會自動切換到某種「故障安全」模式,例如,禁用大部分功能,將控制權移交給固定和受信任的第三方,或者僅將合同轉換爲簡單的「 把個人錢還給我「合同。

形式化驗證
使用形式驗證,能夠執行自動化的數學證實,證實源代碼符合特定的正式規範。 規範仍然是正式的(就像源代碼同樣),但一般要簡單得多。

請注意,形式驗證自己只能幫助你理解你所作的事情(規範)和你如何作(實際實現)之間的差別。 您仍然須要檢查規格是不是您想要的,而且您沒有錯過任何意想不到的效果。

相關文章
相關標籤/搜索