[Solidityのはじめ方] Part12: エラー処理(require, revert, assert)

はじめに:なぜエラー処理が重要なのか?

スマートコントラクトは、一度デプロイされると変更が非常に困難です。そのため、予期せぬ動作や不正な利用を防ぐために、堅牢なエラー処理メカニズムを実装することが極めて重要になります。エラー処理を怠ると、資金の損失やコントラクトの機能不全につながる可能性があります。

Solidityでは、主に以下の3つの関数を使ってエラー処理を行います。

  • require()
  • revert()
  • assert()

これらはトランザクションを元に戻し、ステート(状態)の変更を取り消す機能を持っていますが、それぞれ用途や挙動が異なります。それぞれの使い方と違いをしっかり理解しましょう!

1. require():条件チェックと入力検証

require() は、関数の実行条件やユーザーからの入力が有効かどうかをチェックするために最も一般的に使用されます。

第1引数に評価する条件式を取り、その結果が false の場合にトランザクションをリバート(取り消し)します。第2引数には、エラーメッセージ(文字列)を指定でき、エラー発生時にユーザーに理由を伝えるのに役立ちます。

主な用途:

  • 関数の実行に必要な条件(例:特定のユーザーのみ実行可能)のチェック
  • 外部から渡された引数の妥当性検証(例:値が0より大きいか)
  • 外部コントラクト呼び出しの結果検証

コード例:

pragma solidity ^0.8.20;

contract VendingMachine {
    address public owner;
    mapping(address => uint) public balances;

    constructor() {
        owner = msg.sender;
    }

    function purchase(uint amount) public payable {
        // 条件1: 支払い額が1 Ether以上であること
        require(msg.value >= 1 ether, "You must pay at least 1 Ether.");

        // 条件2: 購入数量が1以上であること
        require(amount > 0, "Amount must be greater than zero.");

        balances[msg.sender] += amount;
    }

    function withdraw() public {
        // 条件3: コントラクトのオーナーのみ引き出し可能
        require(msg.sender == owner, "Only the owner can withdraw.");

        payable(owner).transfer(address(this).balance);
    }
}

require() が失敗した場合、未使用のガスは呼び出し元に返却されます。

2. revert():より複雑な条件でのエラー発生

revert() は、require() と同様にトランザクションをリバートしますが、より複雑なロジックや条件分岐の末にエラーを発生させたい場合に使用されます。

if 文などの条件分岐内で、特定の条件が満たされなかった場合に明示的にエラーを発生させることができます。オプションでエラーメッセージを指定することも可能です。

Solidity 0.8.4以降では、カスタムエラーも定義できるようになり、revert と組み合わせてより詳細でガス効率の良いエラー報告が可能になりました。

主な用途:

  • 複雑な条件分岐の末にエラーを発生させる
  • カスタムエラーを使用して、より詳細なエラー情報を提供する

コード例(カスタムエラーなし):

pragma solidity ^0.8.20;

contract ExampleRevert {
    uint public value;

    function setValue(uint _newValue) public {
        if (_newValue == 0) {
            revert("Value cannot be zero."); // 条件が満たされない場合に revert
        }
        if (_newValue > 100) {
            revert("Value cannot exceed 100."); // 別の条件
        }
        value = _newValue;
    }
}

コード例(カスタムエラーあり):

pragma solidity ^0.8.20;

contract ExampleCustomError {
    uint public value;

    // カスタムエラーの定義
    error InvalidValue(uint _providedValue);
    error ValueTooHigh(uint _providedValue, uint _limit);

    function setValue(uint _newValue) public {
        if (_newValue == 0) {
            revert InvalidValue(_newValue); // カスタムエラーで revert
        }
        if (_newValue > 100) {
             revert ValueTooHigh(_newValue, 100); // カスタムエラーで revert(引数付き)
        }
        value = _newValue;
    }
}

revert() が呼び出された場合も、未使用のガスは呼び出し元に返却されます。

3. assert():内部エラーと不変条件のチェック

assert() は、通常「起こり得ない」はずのエラーを検出するために使用されます。主に、コントラクト内部の不変条件(常に真であるべき条件)が破られていないかを確認したり、コードのバグを発見したりする目的で使われます。

第1引数に評価する条件式を取り、その結果が false の場合にトランザクションをリバートします。エラーメッセージを指定することはできません。

assert() が失敗するということは、コントラクトのコード自体に深刻なバグが存在する可能性が高いことを示唆します。

重要な違い: require()revert() とは異なり、assert() が失敗した場合、すべてのガスが消費されます。これは、バグに対するペナルティとしての意味合いがあります。

主な用途:

  • オーバーフロー/アンダーフローのチェック(Solidity 0.8.0以降ではデフォルトでチェックされるため、通常は不要)
  • 不変条件のチェック(例:コントラクト内のトークン総供給量が特定の値を超えない)
  • コードのバグ検出

コード例:

pragma solidity ^0.8.20;

contract ExampleAssert {
    uint public totalItems;
    uint constant MAX_ITEMS = 1000;

    function addItem() public {
        // アイテム追加処理...
        totalItems++;

        // 不変条件のチェック: totalItemsがMAX_ITEMSを超えることはありえないはず
        assert(totalItems <= MAX_ITEMS);
    }

    // Solidity 0.8.0未満でのオーバーフローチェック例(現在は非推奨)
    function add(uint a, uint b) internal pure returns (uint) {
        uint c = a + b;
        assert(c >= a); // オーバーフローが発生していないかチェック
        return c;
    }
}

注意: 外部からの入力や外部コントラクトの戻り値の検証に assert() を使うべきではありません。これらの検証には require() を使用してください。

使い分けのまとめ 比較表

それぞれの関数の特徴と使い分けをまとめます。

機能 主な用途 エラーメッセージ 失敗時のガス消費 推奨される使用場面
require(condition, "message") 入力検証、アクセス制御、外部呼び出し結果のチェック 指定可能 (推奨) 未使用分は返却 関数の前提条件、ユーザー入力、外部要因のチェック
revert("message") / revert CustomError() 複雑な条件分岐におけるエラー発生 指定可能 / カスタムエラー 未使用分は返却 if/else などを用いた詳細な条件判定後
assert(condition) 内部エラー検出、不変条件のチェック、バグ検出 指定不可 すべて消費 コードの内部的な整合性チェック(起こるべきでないエラーの検出)

ガス消費について

エラー処理はガスコストにも影響を与えます。

  • require()revert() は、失敗した場合、実行されなかった操作のガスと未使用のガスを返却します。エラーメッセージが長いほど、デプロイ時とエラー発生時のガスコストがわずかに増加します。
  • カスタムエラー (revert CustomError()) は、文字列メッセージを使うよりもガス効率が良いとされています。
  • assert() は、失敗した場合にすべてのガスを消費します。そのため、通常の条件チェックには使用すべきではありません。

効率的なコントラクト開発のためには、適切なエラー処理関数を選択することが重要です。

まとめ

Solidityにおけるエラー処理は、安全で信頼性の高いスマートコントラクトを構築するための基本です。

  • require(): 外部からの入力や実行条件のチェックに使いましょう。
  • revert(): 複雑なロジックの末にエラーを発生させる場合に使いましょう。カスタムエラーも活用しましょう。
  • assert(): コントラクト内部の不変条件や、絶対に起こらないはずのバグを検出するために使いましょう。

これらの関数を適切に使い分けることで、バグを未然に防ぎ、ユーザーにエラーの原因を明確に伝え、ガス効率の良いコントラクトを作成することができます。

参考情報

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です