本篇預計閱讀時間 15 分鐘, 實作時間 30 分鐘。
在開始之前先來思考底下的練習題,閱讀後即可練習完成。
練習題
- 
	
練習題 1
- 
		
小明想要發行 NFT,希望用戶可以在 2022 年1 月1 號才能開始鑄造 Mint,請問如何利用
require()寫一個函式幫小明完成這個任務。 
 - 
		
 - 
	
練習題 2
- 
		
承上題,小明有一個
uint maxMint = 20;,他希望每次用戶不能鑄造 mint 超過這個整數,如何幫助小明利用require()寫一個函式完成這個任務。 
 - 
		
 - 
	
練習題 3
- 
		
承上題,小明有一個
bool pause = false;,他希望每次用戶鑄造 mint 前都要確認這個布林值已查看合約是否是停止狀態,請問如何利用assert()寫一個函式幫小明完成這個任務。 
 - 
		
 - 
	
練習題 4
- 
		
請問何種異常處理會直接把程式停止?
 
 - 
		
 - 
	
練習題 5
- 
		
請實作可以啟動、暫停、凍結、刪除的智能合約。
 
 - 
		
 - 
	
練習題 6
- 
		
請問以下程式碼的執行結果為何?
 
 - 
		
 

try feed.getData(token) returns (uint v) {
    require(1 == 2, "Will revert");
} catch (bytes memory /*lowLevelData*/) {
    return (0);
}
面對異常處理,我們得先簡單理解交易以及錯誤(Transactions and Errors),並且條列一些在 Solidity 中異常處理的性質。
- 
	
交易是自動化的,一旦送出便沒有辦法在過程中透過人為終止
 - 
	
拋出 Errors 是會回復狀態的行為,並不會讓錯誤發生。
 - 
	
語法包含
require,assert,revert,在更早的版本還有throw; - 
	
除了以下 Low-Level 的函數以外,皆為接連發生的(cascade)
- 
		
Address.send,address.call,address.delegatecall,address.staticcall 
 - 
		
 - 
	
Revert和require可以回傳錯誤訊息 
0.4.10 版的 Solidity 新增了 require(), assert(), revert() 這三個語法
- 
	
require()用來檢查較不嚴重的錯誤,通常是在執行前就檢驗合理的輸入或條件,可以退回未使用到的 gas - 
	
assert()用來檢查較嚴重的錯誤,會像以前一樣拿走所有的 gasLimit 的手續費 - 
	
revert()跟require()基本上相同,但是revert()沒有包括狀態檢查 
0.6 之後新增了 try/catch 可以使用

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
contract Exception{
    mapping(address => uint64) public balanceReveived;
    
    function receiveMoney() public payable{
        assert(balanceReceived[msg.sender] + uint64(msg.value) >= balanceReceived[msg.sender])
        balanceReveived[msg.sender] += uint64(msg.value);
    }
    function withdrawMoney(address payable _to, uint64 _amount) public {
    require(balanceReceived[msg.sender] <= _amount, "You don't have enough ether");
    assert(balanceReceived[msg.sender] >=     balanceReceived[msg.sender] - _amount)
    balanceReceived[msg.sender] -= _amount;
_to.transfer(_amount);
    }
}
以上例子之中我們可以發現:
- 
	
在加上
assert之前,當我們想要匯入 10 ethers 兩次,實際上合約不會收到 20 ethers,原因是來自於uint64 - 
	
加上
assert之後,當我們想要匯入 10 ethers 兩次,便會在第二次動作時接收到回傳的錯誤。 
使用異常處理的最主要目的是使當前的執行被停止或撤銷,並且把原先改變的狀態回復到原本狀態,包含帳戶在原先行為後的餘額。在異常發生時,Solidity 會執行一個回退的操作(0xfd)並且讓 EVM 把所有「狀態改變」回復到原先的狀態。
Throw
- 
	
於 Solidity 0.4.10 版本後移除
 

if (msg.sender != owner) {throw;}
Require
require() 用於檢驗簡單的條件:
- 
	
合理的輸入
 - 
	
執行前必要的簡單條件
 - 
	
回傳一則訊息
 

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
contract error {
    function testRequire(uint _i) public pure {
        require(_i > 10, "Input must be greater than 10");
    }
}
Require 會被觸發的情況及性質如下:
- 
	
require(X)其中 X 為false - 
	
經由訊息呼叫函數,但此函數並沒有被適當地完成(像是 gas 不足、找不到對應函式、名稱錯誤)
 - 
	
使用外部函數呼叫了一個沒有任何程式碼的合約
 - 
	
合約裡面的物件沒有被宣告 payable modifier 卻接收到了 Ether
 - 
	
唯讀的函式(getter function)接收到了 Ether
 - 
	
Address.transfer()執行失敗 
這類的錯誤並不包含以 Low-Level 的函式呼叫,也就是說我們之前在 address 小節提到的 .call、.delegatecall、.staticcall 這些函式並不會拋出異常,只會透過返回 false 來表達交易失敗。
Assert
assert() 必須指被用來檢測內部錯誤,意思就是用來測試或 debug,並且用來檢測不變量(不該被改變的常數等)。
以下的例子是 num 這個變數必須只為 0,在我們的預想之中它得是個常數不該被更改,所以為了確認我們沒有忽略它被改變的情況,這裡加上 assert() 在 num 被改變時使程式強制中止。

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
contract error {
    function testAssert(uint _i) public view {
        assert(_i == 0);
    }
}
Asserted 會被觸發的情況及性質如下:
- 
	
assert(X)當 X 之值為false - 
	
踩到了超出範圍的 index,像是以負值訪問陣列或 bytes。
 - 
	
除以 0 或取除以 0 之餘數(5/0 or 23%0)
 - 
	
位元移動運算(Byteshifting)時位移量為負
 - 
	
轉換負值或過大的數字成
enum資料結構 
Assert 和 Require 的比較
- 
	
當
require面對 Revert operation(0xfd) 時會送回剩餘的gas - 
	
當
assert面對 Invalid operation(0xfe) 時會消耗全部的gas - 
	
Assert 用於檢測完全不該在合約裡出現的情況
 - 
	
Require 用來檢測使用者的輸入等錯誤情況
 
Revert
revert() 用於確認複雜狀態,意思就是說我們必須寫上一連串的巢狀 if-else 或其他條件式選擇來過濾各種情形,最後才加上 revert() 如果我們進到了錯誤結果。

//SPDX-License-Identifier: MIT
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");
          }
        }
    }
}
Revert 會回復交易狀態,與其他的異常處理語法比較可見下列比較例子:

if (amount > msg.value ether){
    revert("Not Enough Ether Provided");
}
// 另解 // Alternative way to do it:
require(amount <= msg.value ether, "Not Enough Ether Provided");
Try/Catch
try/catch 用於確認外部執行函數和合約建立時(在一個合約裡面建立另外一個合約時)的錯誤,可以利用類條件式選擇來排除錯誤情況。
CATCH
我們有幾種宣告 catch 的方法:
- 
	
catch Error (string memory /*reason*/):當我們宣告過了revert("reasonString")或require(false, "reasonString"),且這些異常處理被觸發的話,catch則會被執行。 - 
	
catch Panic(uint errorCode) { ... }:此類型的catch會在各種assert、除以零、異常的陣列取值、運算溢位等情況發生時被執行。 - 
	
catch (bytes memory /*lowLevelData*/):當沒有其他符合的catch子句時,或是在decoding 錯誤訊息時出現問題、甚至外部呼叫時出現 assertion failing,便會執行此類catch。 - 
	
如果我們不在乎任何的 Error Data,可以使用
catch { ... } 

// SPDX-License-Identifier: MIT // 以下例子取自 Solidity 官方文件 pragma solidity ^0.8.10; contract Foo {address public owner; constructor(address _owner) { require(_owner != address(0), "invalid address"); assert(_owner != 0x0000000000000000000000000000000000000001);owner = _owner; } function myFunc(uint x) public pure returns (string memory) { require(x != 0, "require failed"); return "my func was called"; } } contract Bar { event Log(string message); event LogBytes(bytes data); Foo public foo; constructor() { foo = new Foo(msg.sender); } // Example of try / catch with external call // tryCatchExternalCall(0) => Log("external call failed") // tryCatchExternalCall(1) => Log("my func was called")function tryCatchExternalCall(uint _i) public {try foo.myFunc(_i) returns (string memory result) {emit Log(result); } catch { emit Log("external call failed"); } } // Example of try / catch with contract creation //tryCatchNewContract(0x0000000000000000000000000000000000000000) => Log("invalid address")// tryCatchNewContract(0x0000000000000000000000000000000000000001) => LogBytes("") //tryCatchNewContract(0x0000000000000000000000000000000000000002) => Log("Foo created") function tryCatchNewContract(address _owner) public { try new Foo(_owner) returns (Foo foo) { // you can use variable foo hereemit Log("Foo created"); } catch Error(string memory reason) { // catch failing revert() and require()emit Log(reason); } catch (bytes memory reason) { // catch failing assert()emit LogBytes(reason); } } }
TRY
try 這個關鍵字必須要根據「外部函式呼叫」或「創建合約」這類的行為來定義,例如:new ContractName()。
而其中的 returns 部份是可有可無的,若需要的話,宣告 returns 部份必須要符合外部函式呼叫的回傳型態。

![]()
// SPDX-License-Identifier: GPL-3.0 // 以下程式碼取自 Solidity 官方文件 pragma solidity >=0.8.1; interface DataFeed { function getData(address token) external returns (uint value); } contract FeedConsumer { DataFeed feed; uint errorCount; function rate(address token) public returns (uint value, bool success) { // Permanently disable the mechanism if there are // more than 10 errors. require(errorCount < 10); try feed.getData(token) returns (uint v) { return (v, true); } catch Error(string memory /*reason*/) { // This is executed in case // revert was called inside getData // and a reason string was provided.errorCount++; return (0, false); } catch Panic(uint /*errorCode*/) { // This is executed in case of a panic, // i.e. a serious error like division by zero // or overflow. The error code can be used // to determine the kind of error. errorCount++;return (0, false); } catch (bytes memory /*lowLevelData*/) { // This is executed in case revert() was used. errorCount++; return (0, false); } } }
練習題解答
- 
	
練習題解答 1
- 
		
小明想要發行 NFT,希望用戶可以在 2022 年1 月1 號才能開始鑄造 Mint,請問如何利用
require()寫一個函式幫小明完成這個任務。 
 - 
		
 

function checkTime() view public {
require(block.timestamp > 1640966400);
// 設定區塊時間戳來定義開放時間,Sat Jan 01 2022 00:00:00 GMT+0800 // ( 台北標準時間 )
}
- 
	
練習題解答 2
- 
		
承上題,小明有一個
uint maxMint = 20;,他希望每次用戶不能鑄造 mint 超過這個整數,如何幫助小明利用require()寫一個函式完成這個任務。 
 - 
		
 

function checkMaxMint(uint _mintAmount, uint maxMintAmount) view
public {
require(_mintAmount > 0); // 每次必須鑄造超過 0 個 require(_mintAmount <= maxMintAmount);
// 鑄造的數量不可以大於每次最大鑄造數量
}
- 
	
練習題解答 3
- 
		
承上題,小明有一個
bool pause = false;,他希望每次用戶鑄造 mint 前都要確認這個布林值已查看合約是否是停止狀態,請問如何利用assert()寫一個函式幫小明完成這個任務。 
 - 
		
 

function checkPaused() view public { 
    assert(!paused);
}
- 
	
練習題解答 4
- 
		
請問何種異常處理會直接把程式停止?
 
 - 
		
 
Assert()
- 
	
練習題解答 5
- 
		
請實作可以啟動、暫停、凍結、刪除的智能合約。
 
 - 
		
 

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.11 contract Pause {
    address owner;
    bool paused; // 預設為 false
    constructor() public{ owner = msg.sender;
}
    function setPaused(bool _paused) public { require(msg.sender == owner, "You are not the Owner"); paused = _paused;
}
    function destroySmartContract(address payable _to) public { require(msg.sender == owner, "You are not the Owner"); selfdestruct(_to);
    } 
}
- 
	
練習題解答 6
- 
		
請問以下程式碼的執行結果為何?
 
 - 
		
 

try feed.getData(token) returns (uint v) {
    require(1 == 2, "Will revert");
} catch (bytes memory /*lowLevelData*/) {
    return (0);
}

//SPDX-License-Identifier: MIT
try feed.getData(token) returns (uint v) {
require(1 == 2, "Will revert"); // 這裡會被執行 
} catch (bytes memory /*lowLevelData*/) {
return (0); // 不會到這 
}
