Solidity語言學習筆記————26、Assert, Require, Revert 和 Exceptions

FLy_鵬程萬里發表於2018-07-02

Assert, Require, Revert 和 Exceptions

Solidity使用state-reverting異常來處理錯誤。 這種異常將回滾當前呼叫(及其所有子呼叫)狀態的所有變化,並將錯誤標誌給呼叫者。 
函式assertrequire可以用於檢查條件,如果條件不滿足則丟擲異常。 
assert函式只能用於測試內部錯誤,並檢查不變數。 
應該使用require函式來確認input或合約狀態變數滿足條件,或者驗證呼叫外部合約的返回值。 
如果正確使用,分析工具可以評估合約和函式呼叫是否assert失敗。 正常執行的程式碼不應該assert失敗;如果發生這種情況,則需要修復合約中的bug。

還有另外兩種方法可以觸發異常:

  • revert函式可用於標記錯誤並回滾當前的呼叫。
  • throw關鍵字也可以用作revert()的替代方法。
註解
從0.4.13版本,throw關鍵字已被棄用,將來會被淘汰。

當異常發生在子呼叫中時,它們會自動“冒泡”(即異常被重新丟擲)。 
但也有例外:send和底層函式calldelegatecall 和 callcode——在異常情況下返回false,而不是“冒泡”。

警告
如果呼叫的帳戶不存在,calldelegatecall 和 callcode的返回值會是true,EVM就是這麼設計的。如果需要,必須在呼叫之前檢查賬戶是否存在。

捕捉異常目前還做不到。

在下面的示例中,您可以看到如何使用require來輕鬆檢查輸入條件,以及assert如何用於內部錯誤檢查:

pragma solidity ^0.4.0;

contract Sharer {
    function sendHalf(address addr) public payable returns (uint balance) {
        require(msg.value % 2 == 0); // 只允許偶數
        uint balanceBeforeTransfer = this.balance;
        addr.transfer(msg.value / 2);
        //因為轉賬失敗而引發異常
        //不能在這裡回撥,我們不可能
        //仍然有一半的錢。
        assert(this.balance == balanceBeforeTransfer - msg.value / 2);
        return this.balance;
    }
}

在以下情況下會生成assert風格異常:

  • 以過大或負數索引訪問陣列(比如x[i] ,i >= x.length 或 i < 0)
  • 以過大或負數索引訪問固定長度的bytesN
  • 除數或模數為零(例如5/023%0)。
  • 對負數進行位移
  • 將過大或負數轉化為列舉型別。
  • 呼叫內部函式型別的0初始化(zero-initialized)變數。
  • assert的條件為false

在以下情況下會生成require風格異常:

  • 呼叫throw
  • 呼叫require並且條件為false
  • 通過訊息呼叫來呼叫函式,但是它沒有正確完成(即,用盡了gas,沒有匹配的函式,或者丟擲了異常),除非使用底層別的操作callsenddelegatecallcallcode。 底層別的操作不會丟擲異常,而是通過返回false來表示失敗。
  • 使用new關鍵字建立合約但是失敗。
  • 執行一個外部函式呼叫,其指向不包含程式碼的合約。
  • 合約通過public函式接收Ether,但沒有payable修飾符。包括建構函式和回退函式。
  • 合約通過一個publicgetter函式接收Ether。
  • 如果.transfer()失敗。

在內部,Solidity對require-style異常執行一個revert操作(0xfd指令),並執行一個無效操作(指令0xfe)來丟擲一個assert-style異常。 在這兩種情況下,這將導致EVM revert對狀態所做的所有更改。 revert的原因是沒有安全的方式來繼續執行,因為沒有發生預期的效果。 因為我們要保留交易的原子性,所以最安全的做法是恢復所有的變化,並使整個事務(或至少呼叫)無效。 請注意,assert風格的異常消耗呼叫中可用的所有gas,而require風格的異常將不會消耗從Metropolis版本開始的任何gas。


相關文章