[スマートコントラクト・プログラミング]Solidityのエラーハンドリング

プログラミング

スマートコントラクトを作成するためのプログラミング言語であるSolidityのエラーハンドリングについて記載します。

Solidityのエラーハンドリング

SolidityはプログラミングされたSolidityのソースコードがまずバイトコードにコンパイルされ、コンパイル時に構文エラーチェックが行われます。

エラーはコンパイル時と実行時によく発生することがありますが、このコンパイル時に文法的に問題ないか確認することは可能です。

しかし、ランタイムエラーは事前に認知しておくことが難しく、主にコントラクトの実行中によく発生します。

Solidityには、これらコントラクトの実行時に発生するエラー(Runtimeエラー)を処理するための多くの関数が用意されています。

SolidityのEVM Codeとは?

Solidityのエラーハンドリング処理を行う上でSolidityのコンパイル処理の概念を理解することが重要です。

まずここでSolidityのEVM Codeについて簡単にまとめておきたいと思います。

バイトコードとは、一般的に言って仮想マシン上で動作するために作られた実行可能な中間コードのことを指します。

JavaなどやC++などの他のプログラミング言語にも存在しています。

Solidityでいうバイトコードとはイーサリアム仮想マシンであるEVMで実行する中間コードのことです。

最終的にEVMはこのバイトコードをバイナリコードに変換して処理を実行します。

バイナリコードという大分類の中にバイトコードという分類があるイメージを持ってもらえるとわかりやすいかもしれません。

下記はSolidityの簡単なコントラクトを記載したソースコードです。

このSolidityのソースコードをコンパイルするとbytecodeのobjectが表示されます。

上記ソースコードのbytecodeは下記になります。

$ bytecode
{
“object”: “6080604052348015600f57600080fd5b5060a48061001e6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80636d4ce63c14602d575b600080fd5b60336050565b604051808381526020018281526020019250505060405180910390f35b600080600060019050600060029050808202818301935093505050909156fea265627a7a7231582044a265c35ae06fc4f598c9d34543e8fb9606dd34b4bba1884ba356c065d0b96764736f6c63430005110032”,
}

イーサリアムネットワークにDeployしているICOやDeFiの一部プロジェクトでソースコードを公開していないプロジェクトが多くあります。

これらのソースコードはbytecodeでイーサリアムネットワーク上で確認可能です。

このbytecodeを逆コンパイルすることによって、Solidityのソースコードを復元することが出来ます。

下記ツールは、Ethereum コントラクトのバイトコードをより読みやすい Solidity のようなコードに逆コンパイルしてくれるツールです。

Online Solidity Decompiler

ソースコードを非公開にしているプロジェクトのソースコードを確認することが出来るようになるのです。

簡単な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つを使ってエラー処理を行うことが推奨されています。

  1. Requireステートメント
  2. Assertステートメント
  3. Revertステートメント

Requireステートメント

requireステートメントとは、関数を実行するための前提条件を記載するために利用するエラーハンドリング手法です。

つまり、コードを実行する前に満たすべき制約を付ける処理と言えるでしょう。

引数を1つ受け取り、requireステートメントで値のチェック後に真偽値を返します。また、カスタム文字列のメッセージオプションもあります。

チェックした値がfalseの場合、例外が発生し、実行は終了する形になります。

未使用のガスは呼び出し元に戻され、元の状態に戻されます。

以下は、requireタイプの例外が発生する場合の例です。

  • require()が false となるような引数で呼ばれた場合
  • メッセージで呼び出された関数が正しく終了しない場合
  • new キーワードを使用してコントラクトを作成し、その処理が正しく終了しない場合
  • コードレスコントラクトが外部関数を対象とした場合
  • public getter メソッドを使用してコントラクトにエーテルが送信された場合
  • transfer()メソッドに失敗した場合
  • falseになる条件でassertが呼び出された場合
  • 関数のゼロ初期化変数が呼び出されたとき
  • enumに大きな値や負の値を変換したとき
  • 値がゼロで除算またはモジュロされるとき
  • 大きすぎる、または負のインデックスで配列にアクセスするとき

下記ソースコードはrequireステートメントで値の事前チェックを行うスマートコントラクトを記載した例です。

上記ソースコードをコンパイルしてevenCheckファンクションとuint8Checkファンクションを呼び出してみましょう。

まずは、問題ない値を入力してみます。

evenCheckファンクションに10の引数を与えて呼び出してみます。10は偶数のためtrueが返ってきます。

$ call to requireFunctionCheck.evenCheck
{
“0”: “bool: true”
}

uint8Checkファンクションに100の引数を与えて呼び出してみます。100はUintのデータ型として問題ない値のため、Input is Uint8とメッセージが返ってきます。

$ call to requireFunctionCheck.uint8Check
{
“0”: “string: Input is Uint8”
}

次にわざと問題のある数値を入れてエラー処理が行われるか見てみましょう。

evenCheckファンクションに9の引数を与えて呼び出してみます。偶数ではないため、VM errorが出力されます。ソースコードに基づいた正常な動作です

$ call to requireFunctionCheck.evenCheck
{
call to requireFunctionCheck.evenCheck errored: VM error: revert.
revert
The transaction has been reverted to the initial state.
Note: The called function should be payable if you send value and the value you send should be less than your current balance.
Debug the transaction to get more information.
}

uint8Checkファンクションに300の引数を与えて呼び出してみます。uint8のデータ型に当てはまらないため、Errorが出力されます。ソースコードに基づいた正常な動作です

$ call to requireFunctionCheck.uint8Check
{
errored: Error encoding arguments: Error: invalid BigNumber string (argument=”value”, value=”1000.”, code=INVALID_ARGUMENT, version=bignumber/5.5.0)
}

Assertステートメント

Assertステートメントは、条件が評価された後、ブール値を返すエラーハンドリング処理をします。

返り値に基づいて、プログラムは実行を継続するか、例外を投げるかのどちらかを取ります。

Assert文は、コントラクトの実行前に現在の状態や関数の状態を確認するために使用され、未使用のガスを返す代わりに、ガスをすべて消費して元の状態に戻します。

以下は、assertステートメントの例外が発生する場合の例です。

  • falseになるような条件でassertが呼ばれた場合
  • ゼロ初期化された関数の変数が呼び出された場合
  • 大きな値や負の値をenumに変換した場合
  • 大きすぎる、または負のインデックスで配列にアクセスするとき

下記ソースコードはassertステートメントで値のチェックを行うスマートコントラクトを記載した例です。

上記ソースコードをコンパイルしてcheckValueOver1000ファンクションを呼び出してみましょう。

まずは、問題ない値を入力してみます。

_xに100、_yに500をの引数を与えてcheckValueOver1000ファンクションを呼び出してみます。

$ call to requireFunctionCheck.checkValueOver1000
{
“uint256 _x”: “100”,
“uint256 _y”: “500”
}

次にgetResultファンクションを呼び出してみましょう。定義した通り、100と500の合計は1000を超えていないため、Total Value Not Over 1000が返ってきます。

$ call to requireFunctionCheck.getResult
{
“0”: “string: Total Value Not Over 1000”
}

次にエラーハンドリングするケースを見てみます。

_xに900、_yに200の引数を与えてcheckValueOver1000ファンクションを呼び出してみます。

invalid opcode.というEVMのエラーが出力されます。1000を超えている為正常なエラーハンドリング処理がされていることが確認できるかと思います。

$ call to requireFunctionCheck.checkValueOver1000
{
transact to assertFunctionCheck.checkValueOver1000 errored: VM error: invalid opcode.
invalid opcode
The execution might have thrown.
Debug the transaction to get more information.
}

Revertステートメント

Revertステートメントでは、例外を発生させたり、エラーを表示したり、関数呼び出しを元に戻すために使用します。

revertステートメントを呼び出すと、例外がスローされ、未使用のガスが返され、状態が元の状態に戻ることを意味します。

revertステートメントは、先に紹介したrequireステートメントと非常によく似ていますが、下記違いがあります。

Requireとの主な違い

1.Returnすることが出来る(値を返す)

2.残りのガス代を発信者に払い戻す

下記ソースコードはrevertステートメントでOverflowという文字列を返すスマートコントラクトを記載した例です。

上記ソースコードをコンパイルしてcheckファンクションを呼び出してみましょう。

まずは、問題ない値を入力してみます。

_xに10、_yに10をの引数を与えてcheckファンクションを呼び出してみます。

$ call to requireFunctionCheck.check
{
“0”: “string: No Overflow”,
“1”: “uint256: 20”
}

次に例外が発生するパターンをテストしてみます。

_xに300、_yに200をの引数を与えてcheckファンクションを呼び出してみます。

VMエラーが発生します。正常にエラーハンドリング出来ていることが確認できるかと思います。

$ call to requireFunctionCheck.check
{
call to revertFunctionCheck.check errored: VM error: revert.
revert
The transaction has been reverted to the initial state.
Reason provided by the contract: “Overflow”.
Debug the transaction to get more information.
}

最後に

今回紹介したSolidityの仕様やコードをRemix IDEHardhatで実際に実行やテストをして確認してみましょう。

実際に動かしたり、テストすることで理解が深まるかと思います。

hardhatのインストール手順

Ethereum(イーサリアム)のDApps開発環境Hardhatのインストール手順
Ethereum(イーサリアム)のローカル開発環境であるHardhatのインストール手順を記載します。 Hardhatとは? Hardhatは、Solidityなどで開発したイーサリアムのスマートコントラクト、Dappsをコ...

HardhatでスマートコントラクトのDeploy手順

[Hardhat]SolidityでHello Worldのスマートコントラクトを作成
Solidityを使ってHello Worldを作成してみましょう SolidityでHello World! 前提条件 ・Hardhat デフォルト・フォルダ構成 Hardhatについては下記記事を参照ください。 ...
タイトルとURLをコピーしました