iT邦幫忙

2022 iThome 鐵人賽

DAY 8
0

Exception handling

Synchronization Link Tree


Intro.

Exception handling 是大多數程式語言都有的語法,通常是用來避免各種錯誤狀況直接讓程式 shut dowm,還有讓這些錯誤發生時可以改用其他方式處理。與 if/else 的意義不同,有時候我們如果沒有去處理這些異常狀況可能連判斷 statement 都來不及就導致錯誤發生。

在 Solidity 中有幾種 error 的處理方法,分別是遠古時期的 throe,0.4.10 版的 Solidity 新增的 require(), assert(), revert() 這三個語法以及 0.6 之後新增的 try/catch

  • require() 用來檢查較不嚴重的錯誤,通常是在執行前就檢驗合理的輸入或條件,可以退回使用到的 gas
  • assert() 用來檢查較嚴重的錯誤,會像以前一樣拿走所有的手續費
  • revert()require() 基本上相同,但是 revert() 沒有包括狀態檢查,而是在經過判斷之後直接回傳 error msg
  • try/catch 可以使用於外部執行函數和合約建立時(在一個合約裡面建立另外一個合約時)的錯誤,可以利用類條件式選擇來排除錯誤情況。

等下會更詳細的敘述這些異常處理的語法。

Exception Types

require

Require 常常用於檢驗簡單的條件,當 require 被觸發時可以回復交易狀況。當一個 get function(read-only) 函式或者沒有宣告 payable 的 function 如果接收到了 ether 也會觸發 require 錯誤,這筆交易會失敗而且退回到沒有接收匯款前的狀態。總而言之交易失敗時或者各種輸入錯誤(檢查 Input 合法與否)時的錯誤會是以「回復到交易前的狀態」的 require 為主。

require(input > min, "Input must be greater than min!");

assert

Assert 常常被用來檢測內部錯誤,出現錯誤時不會退回到交易前的狀態,會消耗全部的 gas,使程式運作強制中止。例如常數不變量、記憶體溢位或非法訪問資料結構、位元移動運算距離為負等等,都屬於 assert 的範疇。

asser(input == 0);

revert

Revert 跟 Require 依樣會回復交易狀態,只是沒有檢測 statement 的過程,而是遇到 revert 時就會直接出現錯誤,所以可以用於判斷複雜狀態,例如把 revert 包在巢狀 if-else 中過濾各種情形,最後如果遇到了 revert 就回傳 error msg。

pragma solidity ^0.8.11;

contract error {
    function testRevert(uint _i) public pure {
        if (_i <= 10) {
            if (_i != 10){
                revert("Input must be equal 10");
            }
        }
    }
}

使用異常處理的最主要目的是使當前的執行被停止或撤銷,並且把原先改變的狀態回復到原本狀態,包含帳戶在原先行為後的餘額。在異常發生時,Solidity 會執行一個回退的操作(0xfd)並且讓 EVM 把所有「狀態改變」回復到原先的狀態。

Error

在進到更深入的 revert/require error msg 以及 try/catch 之前,我們先看一下 solidity 中的 error msg 格式與 error type。

在 Solidity 中 error msg 回傳的格式為:

  • bytes4 function selector
  • 以 function selector 定義的 ABI encoded 參數

回傳的 error type 有兩種,分別是 Panic 和 Error,定義方式為:

  • Panic: "0x4e487b71" = keccak256("Panic(uint256)"):用於嚴重錯誤(內部記憶體錯誤)
  • Error: "0x08c379a0" = keccak256("Error(string)":用於正常錯誤

require 回傳的錯誤類型為 error,而 assert 回傳的錯誤類型為 panic。

Panics

我們可以透過查表知道 panics 的錯誤訊息是什麼:

  • 0x01: If you call assert with an argument that evaluates to false.
  • 0x11: If an arithmetic operation results in underflow or overflow outside of an unchecked { ... } block.
  • 0x12; If you divide or modulo by zero (e.g. 5 / 0 or 23 % 0).
  • 0x21: If you convert a value that is too big or negative into an enum type.
  • 0x22: If you access a storage byte array that is incorrectly encoded.
  • 0x31: If you call .pop() on an empty array.
  • 0x32: If you access an array, bytesN or an array slice at an out-of-bounds or negative index (i.e. x[i] where i >= x.length or i < 0).
  • 0x41: If you allocate too much memory or create an array that is too large.
  • 0x51: If you call a zero-initialized variable of internal function type.

Errors

我們也可以自訂 Error Terms,然後使用 revert 回傳這個 error type。

error MyError(uint256 _errorCode, uint256 _balance);

function throwMyError(bool _fail) external {
    if (_fail) {
        revert MyError(23, bal);
    }
    val = 29;
}

綜上所述,其實在客製完 error type 以及妥善在 function 布置好異常處理的情形下,我們是能透過「錯誤」來回傳訊息的,例如在 nestes call 中我們能夠透過不斷 revert 一個定義好的 error type 來做到在不同 contract 傳遞模組化訊息的目的!

這是非常高階的技巧,詳細內容可以看我之前發表在 TEM 的文章:以 EIP-3668 進行鏈下 / 跨鏈資料傳遞

Try / Catch

終於回到 try/catch 了,我們可以直接看官方文件的例子:

function doStuff4(bool _fail) external {
    try conB.stuff1(_fail) returns (uint256 v) {
        val = v;
        return;
    } catch Error(string memory reason) {
        ...
        // revert(reason);
    } catch Panic(uint256 errorCode) {
        ...
    } catch (bytes memory lowLevelData) {
        ...
    }
}

在其中我們可以看到三種 error types,包含:

  • catch Error(string memory reason) { ... }:這跟我們之前提到的 error msg type 中的 "Error" 是一樣的,可以直接包裝 string 並進行異常處理。
  • catch Panic(uint256 errorCode) { ... }:與我們之前提到的 revert 的 "Panic" error msg tpye 相同,需要去判斷 errorCode 為何知道 assert 原因是什麼。
  • catch (bytes memory lowLevelData) { ... }:如果並非以上兩者也有 low-level error data 的選項。
  • 當然如果不想知道 error type 是什麼或原因為何,也可以直接 catch { ... } 來做異常處理。

Closing

需要特別注意如果是使用 low-level functions 例如:calldelegatecallstaticcall。錯誤時會回傳 false 而不是產生 error,所以我們要去 catch 這個 false

例如:

function claim() public payable {
    require(!claimed[msg.sender], "You have been claimed!");

    claimed[msg.sender] = true;

    uint256 amountInEther = 0.001 ether;
    (bool sent, bytes memory data) = payable(msg.sender).call{
        value: amountInEther
    }("");
    require(sent, "Failed to withdraw Ether");
}

Reference


最後歡迎大家拍打餵食大學生0x2b83c71A59b926137D3E1f37EF20394d0495d72d


上一篇
Day 7 - Function Signature & Function Selector
下一篇
Day 9 - Assembly
系列文
Smart Contract Development Breakdown30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言