スマートコントラクトを作成するためのプログラミング言語であるSolidityのエラーハンドリングについて記載します。
Solidityのエラーハンドリング
SolidityはプログラミングされたSolidityのソースコードがまずバイトコードにコンパイルされ、コンパイル時に構文エラーチェックが行われます。
エラーはコンパイル時と実行時によく発生することがありますが、このコンパイル時に文法的に問題ないか確認することは可能です。
しかし、ランタイムエラーは事前に認知しておくことが難しく、主にコントラクトの実行中によく発生します。
Solidityには、これらコントラクトの実行時に発生するエラー(Runtimeエラー)を処理するための多くの関数が用意されています。
SolidityのEVM Codeとは?
Solidityのエラーハンドリング処理を行う上でSolidityのコンパイル処理の概念を理解することが重要です。
まずここでSolidityのEVM Codeについて簡単にまとめておきたいと思います。
バイトコードとは、一般的に言って仮想マシン上で動作するために作られた実行可能な中間コードのことを指します。
JavaなどやC++などの他のプログラミング言語にも存在しています。
Solidityでいうバイトコードとはイーサリアム仮想マシンであるEVMで実行する中間コードのことです。
最終的にEVMはこのバイトコードをバイナリコードに変換して処理を実行します。
バイナリコードという大分類の中にバイトコードという分類があるイメージを持ってもらえるとわかりやすいかもしれません。
下記はSolidityの簡単なコントラクトを記載したソースコードです。
1 2 3 4 5 6 7 8 9 10 |
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; contract returnSample { function get() public pure returns(uint multiple, uint sum){ uint a = 1; uint b = 2; return(a*b, a+b); } } |
このSolidityのソースコードをコンパイルするとbytecodeのobjectが表示されます。
上記ソースコードのbytecodeは下記になります。
イーサリアムネットワークにDeployしているICOやDeFiの一部プロジェクトでソースコードを公開していないプロジェクトが多くあります。
これらのソースコードはbytecodeでイーサリアムネットワーク上で確認可能です。
このbytecodeを逆コンパイルすることによって、Solidityのソースコードを復元することが出来ます。
下記ツールは、Ethereum コントラクトのバイトコードをより読みやすい Solidity のようなコードに逆コンパイルしてくれるツールです。
ソースコードを非公開にしているプロジェクトのソースコードを確認することが出来るようになるのです。
簡単なSolidityのバイトコードおよびコンパイルの簡単な概念の説明は以上となります。
Solidityの3つのエラーハンドリング手法
Solidityで発生する実行時のエラーには、主なエラーとして下記のようなランタイムエラーが存在しています。
- out-of-gasエラー(ガス不足)
- over flowエラー
- Divide by zeroエラー
- array-out-of-indexエラー等
Solidityのバージョン4.10までは、エラーをハンドリングするための手法としてthrow文が利用されていました。
複数のif…else文で発生しうるエラーをthrow文で処理する必要があり、考えうる複数のパターンをチェックしなければならず、多くのガスを消費する形になっていました。
この問題を解決するためSolidityのバージョン4.10以降では、assert, require, revertといった新しいエラーハンドリングの手法が導入されています。
現在ではthrowを使ったエラーハンドリングは推奨されておらず、この3つを使ってエラー処理を行うことが推奨されています。
- Requireステートメント
- Assertステートメント
- Revertステートメント
Requireステートメント
requireステートメントとは、関数を実行するための前提条件を記載するために利用するエラーハンドリング手法です。
つまり、コードを実行する前に満たすべき制約を付ける処理と言えるでしょう。
引数を1つ受け取り、requireステートメントで値のチェック後に真偽値を返します。また、カスタム文字列のメッセージオプションもあります。
チェックした値がfalseの場合、例外が発生し、実行は終了する形になります。
未使用のガスは呼び出し元に戻され、元の状態に戻されます。
以下は、requireタイプの例外が発生する場合の例です。
- require()が false となるような引数で呼ばれた場合
- メッセージで呼び出された関数が正しく終了しない場合
- new キーワードを使用してコントラクトを作成し、その処理が正しく終了しない場合
- コードレスコントラクトが外部関数を対象とした場合
- public getter メソッドを使用してコントラクトにエーテルが送信された場合
- transfer()メソッドに失敗した場合
- falseになる条件でassertが呼び出された場合
- 関数のゼロ初期化変数が呼び出されたとき
- enumに大きな値や負の値を変換したとき
- 値がゼロで除算またはモジュロされるとき
- 大きすぎる、または負のインデックスで配列にアクセスするとき
下記ソースコードはrequireステートメントで値の事前チェックを行うスマートコントラクトを記載した例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// SPDX-License-Identifier: MIT pragma solidity >=0.4.16 <0.9.0; contract requireFunctionCheck { // 入力チェックされた数値がUint8であるか確認するuint8Checkファンクション function uint8Check(uint _value) public pure returns(string memory){ //入力された値が0以上か確認するRequireステートメント require(_value >= 0, "invalid uint8"); //入力された値が255以下か確認するRequireステートメント require(_value <= 255, "invalid uint8"); return "Input is Uint8"; } // 入力チェックされた数値が偶数か確認するevenCheckファンクション function evenCheck(uint _value) public pure returns(bool){ //入力された値を2で割ると0になるか確認するRequireステートメント require(_value % 2 == 0); return true; } } |
上記ソースコードをコンパイルしてevenCheckファンクションとuint8Checkファンクションを呼び出してみましょう。
まずは、問題ない値を入力してみます。
evenCheckファンクションに10の引数を与えて呼び出してみます。10は偶数のためtrueが返ってきます。
uint8Checkファンクションに100の引数を与えて呼び出してみます。100はUintのデータ型として問題ない値のため、Input is Uint8とメッセージが返ってきます。
次にわざと問題のある数値を入れてエラー処理が行われるか見てみましょう。
evenCheckファンクションに9の引数を与えて呼び出してみます。偶数ではないため、VM errorが出力されます。ソースコードに基づいた正常な動作です
uint8Checkファンクションに300の引数を与えて呼び出してみます。uint8のデータ型に当てはまらないため、Errorが出力されます。ソースコードに基づいた正常な動作です
Assertステートメント
Assertステートメントは、条件が評価された後、ブール値を返すエラーハンドリング処理をします。
返り値に基づいて、プログラムは実行を継続するか、例外を投げるかのどちらかを取ります。
Assert文は、コントラクトの実行前に現在の状態や関数の状態を確認するために使用され、未使用のガスを返す代わりに、ガスをすべて消費して元の状態に戻します。
以下は、assertステートメントの例外が発生する場合の例です。
- falseになるような条件でassertが呼ばれた場合
- ゼロ初期化された関数の変数が呼び出された場合
- 大きな値や負の値をenumに変換した場合
- 大きすぎる、または負のインデックスで配列にアクセスするとき
下記ソースコードはassertステートメントで値のチェックを行うスマートコントラクトを記載した例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// SPDX-License-Identifier: MIT pragma solidity >=0.4.16 <0.9.0; contract assertFunctionCheck { bool result; // 入力された2つの値の合計が1000以下か確認する関する function checkValueOver1000(uint _x, uint _y) public { uint total = _x + _y; //Assertファンクションで1000以下であることを確認する assert(total<=1000); result = true; } // assert文の結果を表示する関数の定義 function getResult() public view returns(string memory){ if(result == true){ return "Total Value Not Over 1000"; } else{ return "Total Value Over 1000"; } } } |
上記ソースコードをコンパイルしてcheckValueOver1000ファンクションを呼び出してみましょう。
まずは、問題ない値を入力してみます。
_xに100、_yに500をの引数を与えてcheckValueOver1000ファンクションを呼び出してみます。
次にgetResultファンクションを呼び出してみましょう。定義した通り、100と500の合計は1000を超えていないため、Total Value Not Over 1000が返ってきます。
次にエラーハンドリングするケースを見てみます。
_xに900、_yに200の引数を与えてcheckValueOver1000ファンクションを呼び出してみます。
invalid opcode.というEVMのエラーが出力されます。1000を超えている為正常なエラーハンドリング処理がされていることが確認できるかと思います。
Revertステートメント
Revertステートメントでは、例外を発生させたり、エラーを表示したり、関数呼び出しを元に戻すために使用します。
revertステートメントを呼び出すと、例外がスローされ、未使用のガスが返され、状態が元の状態に戻ることを意味します。
revertステートメントは、先に紹介したrequireステートメントと非常によく似ていますが、下記違いがあります。
Requireとの主な違い
1.Returnすることが出来る(値を返す)
2.残りのガス代を発信者に払い戻す
下記ソースコードはrevertステートメントでOverflowという文字列を返すスマートコントラクトを記載した例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// SPDX-License-Identifier: MIT pragma solidity >=0.4.16 <0.9.0; contract revertFunctionCheck { function check(uint _x, uint _y) public pure returns(string memory, uint){ uint sum = _x + _y; if(sum < 0 || sum > 255){ //Overflowという文字列をrevertで返す revert("Overflow"); } else{ return ("No Overflow", sum); } } } |
上記ソースコードをコンパイルしてcheckファンクションを呼び出してみましょう。
まずは、問題ない値を入力してみます。
_xに10、_yに10をの引数を与えてcheckファンクションを呼び出してみます。
次に例外が発生するパターンをテストしてみます。
_xに300、_yに200をの引数を与えてcheckファンクションを呼び出してみます。
VMエラーが発生します。正常にエラーハンドリング出来ていることが確認できるかと思います。
最後に
今回紹介したSolidityの仕様やコードをRemix IDEやHardhatで実際に実行やテストをして確認してみましょう。
実際に動かしたり、テストすることで理解が深まるかと思います。
hardhatのインストール手順
HardhatでスマートコントラクトのDeploy手順