【入門編】 DoS攻撃:ガス制限によるコントラクト停止 – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

こんにちは!ブロックチェーン開発の世界へようこそ。
スマートコントラクトを書き始めると、「いかに効率よく動かすか」よりも「まずは動くものを作ろう」となりがちですよね。でも、ちょっと待ってください。そのコード、実はとっても危ない「落とし穴」を隠し持っているかもしれません。

今回は、スマートコントラクトを狙う厄介な罠の一つ、「ガス制限によるDoS(サービス停止)攻撃」について、身近な例えを交えながら優しく紐解いていきたいと思います。一歩ずつ、確実に安全な書き方をマスターしていきましょう!

—

1. 家のポストで想像してみよう!「ガス制限」とDoS攻撃の正体

ブロックチェーンの世界には、現実世界のような「電気代」や「ガス代」に似た、「Gas(ガス)」という仕組みがあります。コントラクト(プログラム)を動かすためには、このガスを消費しなければなりません。そして、1回のトランザクション(処理のお願い)で使えるガスの量には上限、つまり「ガス制限(Gas Limit)」が決められています。

ここで、こんなシチュエーションを想像してみてください。

あなたは一軒家の家主で、毎日たくさんの郵便物を受け取っています。
ある日、悪意あるイタズラ男が、「一度に10,000通もの手紙」をあなたの小さな郵便受けに無理やり詰め込もうとしました。

  • あなたの郵便受けの容量(ガス制限)は、一度に100通までしか処理できません。
  • イタズラ男は、あなたに対して「この10,000通の処理が終わるまで、次の普通の郵便物を受け付けないで!」と強制する仕組みを作りました。

結果どうなるでしょうか?
あなたは10,000通の手紙を仕分けるだけで力尽き(ガス枯渇)、本当に必要な重要なお知らせ(他のユーザーからの大事な取引)を一切受け取れなくなってしまいますよね。これが、スマートコントラクトにおけるDoS攻撃(ガス制限によるコントラクト停止)の仕組みです。

—

2. なぜ起こる?「Push型」という危険な設計パターン

スマートコントラクトの開発で、やりがちな失敗の代表例が「Push型(押し付け型)」と呼ばれる設計です。

例えば、ゲームの参加者全員に一斉にお金を配る(配当を支払う)機能を、以下のようなループ処理で書いたとしましょう。

// 【危険なコード例】Push型による配当支払い
contract DangerousDividend {
    address[] public investors; // 投資家たちのリスト

    // 投資家を追加する関数
    function addInvestor(address _investor) public {
        investors.push(_investor);
    }

    // 全員に一斉にお金を配る関数(ここに罠があります!)
    function distributeDividends() public payable {
        // investorsの配列が長くなればなるほど、ループ回数が増えます
        for (uint256 i = 0; i < investors.length; i++) {
            // 1人ずつ強制的に送金(Push)していく
            (bool success, ) = investors[i].call{value: 1 ether}("");
            require(success, "Transfer failed"); // もし1人でも送金に失敗すると全体がストップ!
        }
    }
}

何が問題なの?

このコードでは、investors(投資家リスト)の人数が増えれば増えるほど、distributeDividends 関数を実行するのに必要なガスが膨れ上がります。
そして、Ethereumなどのネットワークが持つ1ブロックあたりのガス上限を超えてしまうと、誰もこの関数を二度と実行できなくなってしまいます。 さらに、悪意あるユーザーがわざと失敗するコントラクトアドレスを登録しておけば、上記の require(success, ...) によって意図的に処理をクラッシュさせることが可能です。

これが、実務の現場で頭を悩ませる「ガス枯渇によるコントラクトの完全凍結」のリアルな姿です。

—

3. 救世主は「Pull型」!安全な設計パターンへの書き換え

では、どうすればこの恐怖のループ攻撃からコントラクトを守れるのでしょうか?
答えはシンプルです。先ほどの郵便受けの例えに戻りましょう。

「郵便受けに無理やり詰め込ませる(Push)」のではなく、「郵便局(コントラクト)の窓口に、各自が自分のタイミングで取りに来てもらう(Pull)」という仕組みに変えればいいのです。これをセキュリティの世界では 「Pull over Push(プル・オーバー・プッシュ)」 と呼びます。

実際のコードを見てみましょう。

// 【安全なコード例】Pull型による配当受け取り
contract SafeDividend {
    // 各ユーザーの引き出し可能な残高を管理するマップ
    mapping(address => uint256) public pendingWithdrawals;

    // 配当金をプールにチャージする関数
    function depositDividends() public payable {
        // ここでは単にお金を預かるだけで、ループ処理はしません!
    }

    // ユーザー自身が自分のタイミングでお金を引き出す(Pull)関数
    function withdrawDividend() public {
        uint256 amount = pendingWithdrawals[msg.sender];
        require(amount > 0, "No funds to withdraw");

        // 再entrancy攻撃対策として、先に残高をゼロにする
        pendingWithdrawals[msg.sender] = 0;

        // 安全に送金を実行
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }

    // 管理者などが各ユーザーの未払い残高を登録する関数(必要に応じて配列を使わない設計に)
    function setPendingWithdrawal(address _investor, uint256 _amount) public {
        pendingWithdrawals[_investor] = _amount;
    }
}

Pull型のここがすごい!

  • ループ処理の排除: コントラクト側が一斉送信を行う必要がなくなるため、配列の長さに依存したガス制限の超過(DoS)が物理的に起こらなくなります。
  • 責任の分散: もし送金処理に失敗しても、影響を受けるのはそのトランクスアクションを実行した本人だけ。他のユーザーは何の影響も受けずに自分の資金を引き出せます。

—

まとめ:安全なスマートコントラクト開発に向けて

今回は、ガス制限を利用したDoS攻撃の仕組みと、それを防ぐための「Pull over Push」という強力なデザインパターンについて解説しました。

  • ループ処理の増加に要注意: 配列の要素数を増やすことでガス制限を突破してしまう設計は避ける。
  • 基本は「Pull型」: ユーザー自身にアクション(引き取りなど)を起こしてもらう仕組みをファーストチョイスにする。

セキュリティの世界は奥が深いですが、一つひとつの攻撃パターンと防御の原理原則(今回の例えで言えば「郵便を押し付けさせず、取りに来てもらう」こと)を丁寧に理解していけば、決して怖くありません。

実務の開発現場でも、この「Pull over Push」の精神を忘れずに、堅牢で美しいスマートコントラクトを一緒に作っていきましょう!一歩ずつ、確実にスキルアップしていってくださいね。

コメント

タイトルとURLをコピーしました