広告

第14話:不正な操作は許さない!require, assert, revertで堅牢なコントラクトを作る

2025年7月31日

第14話:不正な操作は許さない!require, assert, revertで堅牢なコントラクトを作る

概要

スマートコントラクトの安全性を支える、3つのエラー処理(require, assert, revert)の完全ガイドです。外部入力の検証に用いるrequireと、内部エラーを検出するassertの違いを明確化。

さらに、現代のベストプラクティスであるrevertと「カスタムエラー」が持つ、優れたガス効率と豊富な情報伝達能力を解説。堅牢でユーザーに優しいDAppを構築するための必須知識が身につきます。

目次
Solidity学習講座:最終版 目次(全20話)
Solidity学習講座:最終版 目次(全20話)

基礎編 第1話:未来のインターネットへようこそ!Solidityとスマートコントラクトの全体像 第2話:準備は1分!ブラウザだけで開発できる「Remix IDE」の基本操作 第3話:記念すべき初コント ...

続きを見る

はじめに

第13話では、ETHを送金する際の最重要セキュリティパターン「Checks-Effects-Interactions」を学びました。その最初のステップ「Checks」において、require文がいかに重要な門番の役割を果たすか、実感していただけたと思います。

スマートコントラクトは、一度デプロイすると修正が困難な、お金が絡むプログラムです。そのため、予期せぬ入力や不正な状態を徹底的に排除する**「防御的プログラミング」**の発想が不可欠です。

Solidityには、この防御を固めるための強力なツールとして、3つのエラー処理機能が用意されています。

  1. require: 外部からの入力や状態を検証する
  2. assert: 内部的なロジックの不整合をチェックする
  3. revert: より柔軟でガス効率の良いエラー処理を行う

あなたはrequireには既によく慣れ親しんでいるはずです。しかし、これら3つの微妙な違いと、それぞれの適切な使い分けを理解することは、あなたのコントラクトの安全性と品質を決定づける、極めて重要な知識なのです。

エラー処理の基本:リバート(Revert)

これら3つの機能に共通するのは、条件が満たされない場合にトランザクションを**「リバート(revert)」**させることです。リバートが発生すると、以下の2つのことが起こります。

  1. 状態のロールバック: そのトランザクション内で行われた全ての状態変更(状態変数の値の変更など)が、すべて取り消され、トランザクションが始まる前の状態に完全に戻ります。
  2. ガス代の返金: トランザクションの実行に使われなかった残りのガス代は、トランザクションの呼び出し元(ユーザー)に返金されます。

これにより、不正な操作によってコントラクトの状態が中途半端に書き換わることを防ぎ、ユーザーの無駄なガス代消費を最小限に抑えることができます。

1. require():外部を監視する「門番」 💂

requireは、あなたが最も頻繁に使うことになるエラー処理機能です。その主な役割は、関数の実行条件が満たされているかをチェックすることです。

  • 主な用途:
    • ユーザーからの入力値の検証 (require(_amount > 0, "金額は0より大きくしてください");)
    • 関数の呼び出し元の検証 (require(msg.sender == owner, "オーナーではありません");)
    • コントラクトの状態の検証 (require(saleIsActive, "セールは終了しました");)
  • 構文: require(条件式, "エラーメッセージ");
  • 思想: 「この関数を実行するには、この条件を満たしていることが**必須(require)**です。満たしていないなら、あなたは門前払いです。」というイメージです。外部からのリクエストに対する、第一の防御壁となります。

requireは、コントラクトが外部とやり取りする際の、あらゆる前提条件を確認するために使います。

2. assert():内部を監視する「番犬」 🐕

assertは、requireとは対照的に、内部的なエラーや、本来ありえないはずのバグを検出するために使われます。

  • 主な用途:
    • 不変条件(invariant)のチェック。例えば、「コントラクトの総預金額が、個人の預金額の合計を下回ることは絶対にない」といった、コードが正常なら常に真であるべき条件の確認。
    • 数値のオーバーフロー/アンダーフローのチェック(※Solidity 0.8.0以降では自動でチェックされるため、この用途は減少しました)。
  • 構文: assert(条件式); (エラーメッセージは取らない)
  • 思想: 「私のコードが完璧なら、この条件は**絶対に(assert)**真のはずだ。もしこれが偽になるなら、それは外部のせいではなく、このコントラ- クト自体に致命的なバグが存在する証拠だ。」というイメージです。

assertが失敗するということは、コントラクトが予期せぬ深刻な状態に陥っていることを示します。そのため、かつてのバージョンではassertが失敗すると残りのガスを全て消費する「ペナルティ」がありましたが、v0.8.0以降ではrequireと同様にガスを返金する仕様に変わりました。しかし、「assertの失敗 = コントラクトのバグ」という開発者向けのセマンティクスは今も変わりません。日常的な開発であなたがassertを書く機会は、requireに比べてずっと少ないでしょう。

3. revert()とカスタムエラー:新時代の柔軟なエラー処理 📣

revertは、if文などの複雑なロジックの中で、手動でトランザクションをリバートさせたい場合に使います。そして、これと**「カスタムエラー」を組み合わせることが、現代のSolidity開発における最も推奨されるエラー処理方法**です。

カスタムエラーとは? structのように、あなた自身がエラーの「型」を定義できる機能です。

Solidity
// 1. カスタムエラーを定義する
// NotOwnerという名前のエラーを定義
error NotOwner();

// 金額不足エラー。必要な額と、実際の額をパラメータとして渡せる
error InsufficientBalance(uint required, uint available);

このように定義したカスタムエラーを、revertキーワードを使って呼び出します。

Solidity
contract VendingMachine {
    address public owner;
    uint public price = 0.1 ether;

    // カスタムエラーの定義
    error NotOwner();
    error IncorrectValue(uint sent, uint required);
    error AlreadyPaid();

    mapping(address => bool) public hasPaid;

    constructor() {
        owner = msg.sender;
    }

    function purchase() public payable {
        // if文とrevertを組み合わせる
        if (msg.value != price) {
            // requireの代わりに、カスタムエラーでrevert
            revert IncorrectValue({ sent: msg.value, required: price });
        }
        if (hasPaid[msg.sender]) {
            revert AlreadyPaid();
        }
        hasPaid[msg.sender] = true;
    }

    function withdraw() public {
        if (msg.sender != owner) {
            revert NotOwner();
        }
        // ...出金処理
    }
}

なぜカスタムエラーが優れているのか?

  1. 圧倒的なガス効率: requireのエラーメッセージ(文字列)を保存・返却するのに比べて、カスタムエラーは劇的にガス代が安いです。ユーザーに優しいコントラクトを作る上で、非常に重要な最適化です。
  2. 高い可読性: revert NotOwner(); のように、エラー名自体がその意味を明確に示します。
  3. 豊富な情報: revert InsufficientBalance(100, 50); のように、エラーに関する**文脈情報(パラメータ)**をフロントエンドに渡すことができます。これにより、フロントエンドは「残高が不足しています」だけでなく、「100wei必要ですが、あなたの残高は50weiです」といった、より親切なエラーメッセージをユーザーに表示できます。

まとめ:いつ、どれを使うべきか?

requireassertrevert + カスタムエラー
主な用途外部入力・状態の検証内部ロジック・不変条件のチェック複雑な分岐、高効率なエラー
思想「あなたは条件を満たしていない」「私のコードにバグがある」「この理由で処理を中断します」
ガス効率標準(昔は悪い)最高
推奨度⭐⭐⭐⭐⭐⭐⭐⭐⭐

結論:

  • 単純な入力値やアクセス権のチェックには、手軽な**require**を使いましょう。
  • 自分のコードの不変条件を確認したい、という稀なケースでは**assert**を検討します。
  • if-else文の中でのエラー処理や、ガス効率と豊富なエラー情報を重視するなら、積極的に**revertとカスタムエラー**を使いましょう。これが現代のスタンダードです。

次回予告

お疲れ様でした!あなたは今、状況に応じて最適なエラー処理を使い分け、より堅牢で、よりユーザーに優しく、よりプロフェッショナルなコントラクトを書くための知識を身につけました。

これまでのレッスンで、「ガス代」という言葉が何度も出てきましたね。ガス代を意識することは、Solidity開発者にとって呼吸をするのと同じくらい自然なことです。しかし、そもそも「ガス」とは一体何なのでしょうか?

次回、**第15話「コスト意識を持とう!イーサリアムの「ガス (Gas)」とは?簡単な節約術」**では、ついにこのイーサリアムの心臓部である「ガス」の仕組みを解き明かします。何がガス代を高くするのか、そして、ユーザーのためにガス代を節約するにはどうすればよいのか、具体的なテクニックと共に学んでいきましょう。

目次
Solidity学習講座:最終版 目次(全20話)
Solidity学習講座:最終版 目次(全20話)

基礎編 第1話:未来のインターネットへようこそ!Solidityとスマートコントラクトの全体像 第2話:準備は1分!ブラウザだけで開発できる「Remix IDE」の基本操作 第3話:記念すべき初コント ...

続きを見る

-Solidity