智慧合約語言 Solidity 教程系列9 - 錯誤處理

Tiny熊發表於2018-04-07

這是Solidity教程系列文章第9篇介紹Solidity 錯誤處理。
Solidity系列完整的文章列表請檢視分類-Solidity

寫在前面

Solidity 是以太坊智慧合約程式語言,閱讀本文前,你應該對以太坊、智慧合約有所瞭解,
如果你還不瞭解,建議你先看以太坊是什麼

歡迎訂閱區塊鏈技術專欄閱讀更全面的分析文章。

什麼是錯誤處理

錯誤處理是指在程式發生錯誤時的處理方式,Solidity處理錯誤和我們常見的語言不一樣,Solidity是通過回退狀態的方式來處理錯誤。發生異常時會撤消當前呼叫(及其所有子呼叫)所改變的狀態,同時給呼叫者返回一個錯誤標識。注意捕捉異常是不可能的,因此沒有try ... catch...。

為什麼Solidity處理錯誤要這樣設計呢?
我們可以把區塊鏈理解為是全球共享的分散式事務性資料庫。全球共享意味著參與這個網路的每一個人都可以讀寫其中的記錄。如果想修改這個資料庫中的內容,就必須建立一個事務,事務意味著要做的修改(假如我們想同時修改兩個值)只能被完全的應用或者一點都沒有進行。
學習過資料庫的同學,應該理解事務的含義,如果你對事務一詞不是很理解,建議你搜尋一下“資料庫事務“。
Solidity錯誤處理就是要保證每次呼叫都是事務性的。

如何處理

Solidity提供了兩個函式assert和require來進行條件檢查,如果條件不滿足則丟擲異常。assert函式通常用來檢查(測試)內部錯誤,而require函式來檢查輸入變數或合同狀態變數是否滿足條件以及驗證呼叫外部合約返回值。
另外,如果我們正確使用assert,有一個Solidity分析工具就可以幫我們分析出智慧合約中的錯誤,幫助我們發現合約中有邏輯錯誤的bug。

除了可以兩個函式assert和require來進行條件檢查,另外還有兩種方式來觸發異常:

  1. revert函式可以用來標記錯誤並回退當前呼叫
  2. 使用throw關鍵字丟擲異常(從0.4.13版本,throw關鍵字已被棄用,將來會被淘汰。)

當子呼叫中發生異常時,異常會自動向上“冒泡”。 不過也有一些例外:send,和底層的函式呼叫call, delegatecall,callcode,當發生異常時,這些函式返回false。

注意:在一個不存在的地址上呼叫底層的函式call,delegatecall,callcode 也會返回成功,所以我們在進行呼叫時,應該總是優先進行函式存在性檢查。

在下面通過一個示例來說明如何使用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;
    }
}

我們實際執行下,看看異常是如何發生的:

  1. 首先開啟Remix,貼入程式碼,點選建立合約。如下圖:
    智慧合約語言 Solidity 教程系列9 - 錯誤處理

  2. 執行測試1:附加1wei (奇數)去呼叫sendHalf,這時會發生異常,如下圖:

智慧合約語言 Solidity 教程系列9 - 錯誤處理

  1. 執行測試2:附加2wei 去呼叫sendHalf,執行正常。
  2. 執行測試3:附加2wei以及sendHalf引數為當前合約本身,在轉賬是發生異常,因為合約無法接收轉賬,錯誤提示上圖類似。

assert型別異常

在下述場景中自動產生assert型別的異常:

  1. 如果越界,或負的序號值訪問陣列,如i >= x.length 或 i < 0時訪問x[i]
  2. 如果序號越界,或負的序號值時訪問一個定長的bytesN。
  3. 被除數為0, 如5/0 或 23 % 0。
  4. 對一個二進位制移動一個負的值。如:5<<i; i為-1時。
  5. 整數進行可以顯式轉換為列舉時,如果將過大值,負值轉為列舉型別則丟擲異常
  6. 如果呼叫未初始化內部函式型別的變數。
  7. 如果呼叫assert的引數為false

require型別異常

在下述場景中自動產生require型別的異常:

  1. 呼叫throw
  2. 如果呼叫require的引數為false
  3. 如果你通過訊息呼叫一個函式,但在呼叫的過程中,並沒有正確結束(gas不足,沒有匹配到對應的函式,或被呼叫的函式出現異常)。底層操作如call,send,delegatecall或callcode除外,它們不會丟擲異常,但它們會通過返回false來表示失敗。
  4. 如果在使用new建立一個新合約時出現第3條的原因沒有正常完成。
  5. 如果呼叫外部函式呼叫時,被呼叫的物件不包含程式碼。
  6. 如果合約沒有payable修飾符的public的函式在接收以太幣時(包括建構函式,和回退函式)。
  7. 如果合約通過一個public的getter函式(public getter funciton)接收以太幣。
  8. 如果.transfer()執行失敗

當發生require型別的異常時,Solidity會執行一個回退操作(指令0xfd)。
當發生assert型別的異常時,Solidity會執行一個無效操作(指令0xfe)。
在上述的兩種情況下,EVM都會撤回所有的狀態改變。是因為期望的結果沒有發生,就沒法繼續安全執行。必須保證交易的原子性(一致性,要麼全部執行,要麼一點改變都沒有,不能只改變一部分),所以需要撤銷所有操作,讓整個交易沒有任何影響。

注意assert型別的異常會消耗掉所有的gas, 而require從大都會版本(Metropolis, 即目前主網所在的版本)起不會消耗gas。

參考視訊

我們也推出了目前市面上最全的視訊教程:深入詳解以太坊智慧合約語言Solidity
目前我們也在招募體驗師,可以點選連結瞭解。

參考文獻

歡迎來我的知識星球深入淺出區塊鏈討論區塊鏈技術,同時我也會為大家提供區塊鏈技術解答,作為星友福利,星友可加入區塊鏈技術付費交流群。
深入淺出區塊鏈 - 系統學習區塊鏈,打造最好的區塊鏈技術部落格。

相關文章