本篇預計閱讀時間 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); // 不會到這
}