【入門編】 デリゲートコール(delegatecall)の危険性とストレージ衝突 – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

こんにちは!スマートコントラクトの開発に挑戦している皆さん、日々のコーディングお疲れ様です。「ブロックチェーンの世界は無限の可能性があって面白いけれど、セキュリティの話になると急に専門用語が増えて難しく感じる…」そんな風に思っていませんか?

今回は、スマートコントラクトのセキュリティにおいて、新人開発者が絶対に知っておかなければならない重要テーマ、delegatecall(デリゲートコール)の危険性とストレージ衝突について、身近な例えを交えながら一歩ずつ優しく紐解いていきたいと思います。

まるで泥棒が合鍵の仕組みを悪用して家を乗っ取るかのような、スリリングで奥深い世界を一緒に覗いてみましょう!

—

1. 家の鍵の貸し借り(delegatecall)に例える基本の仕組み

まずは、イーサリアムなどのブロックチェーンで使われるスマートコントラクトの世界を、私たちの「お家」に例えて考えてみましょう。

スマートコントラクトの中には、自分自身ですべての機能を実装するのではなく、「便利な機能が詰まった外部のプログラム(ライブラリ)」を借りてきて実行したい場面がよくあります。

通常の呼び出し(call)は、「他人の家に遊びに行って、その家の道具や設備を使わせてもらう」ようなものです。あくまで主導権は相手の家にあります。

これに対して、今回主役となる delegatecall(デリゲートコール)は、少し特殊です。
これは、「自分の家に、凄腕のリフォーム業者さんを呼んで、自分の家のリフォームをそのまま作業してもらう」ようなものになります。

  • 作業している場所(ストレージ / データの保存場所): あなたの家
  • 作業している内容や手順(コード): リフォーム業者さんの持ってきた設計図

つまり delegatecall とは、「呼び出し先(業者さん)のコードを使いながら、データの保存や書き換えは、すべて呼び出し元(あなたの家)の領域で行う」という、非常に強力でちょっぴり危険な仕組みなのです。

—

2. なぜ危ないの?「ストレージ衝突」という罠

「自分の家を効率よくリフォームしてもらえるなら便利じゃない!」と思いますよね。ここで、大きな落とし穴(脆弱性)が潜んでいます。それが 「ストレージ衝突(Storage Collision)」 です。

これを防犯の例えで説明しましょう。

あなたの家には、引き出しが上から順番に並んでいます。

  • 引き出し 1段目:金庫の暗証番号
  • 引き出し 2段目:通帳
  • 引き出し 3段目:日用品

リフォーム業者さん(ライブラリ)にも、彼らなりの設計図(引き出しの順番)があります。

  • 業者さんの設計図 1段目:お茶の葉の置き場所
  • 業者さんの設計図 2段目:金庫の暗証番号

ここで、もしあなたと業者さんの間で「引き出しの順番」の認識がズレていたらどうなるでしょうか?

業者さんが「よし、設計図の2段目にある金庫の暗証番号を書き換えよう!」と作業したとき、あなたの家では、なんと「通帳」が入っている大切な2段目の引き出しを勝手に書き換えられてしまった! という事態が起きます。

これが、スマートコントラクトにおける「ストレージ衝突」の正体です。変数の定義順序がズレているだけで、意図しない大切なデータ(例えば「コントラクトの所有者(オーナー)」の住所など)が、悪意ある第三者に書き換えられてしまうのです。

—

3. 実際のコードで見てみよう(脆弱なパターン)

百聞は一見に如かず。実際に、どのようなコードでこの問題が起きるのか、 Solidityのコードを使って見ていきましょう。まずは「危ない書き方」の例です。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// 【危険なライブラリ側】
// 業者さんの設計図(変数の順番に注目してください)
contract LibraryContract {
    address public owner; // 1番目:オーナーのアドレス
    uint256 public storedValue; // 2番目:保存された数値
}

// 【危険な呼び出し元側】
// あなたの家(コントラクト)
contract VulnerableProxy {
    // ⚠️【致命的なミス】ライブラリとは逆に定義してしまっています!
    uint256 public storedValue; // 1番目:数値
    address public owner;       // 2番目:オーナー

    address public libraryAddress;

    constructor(address _library) {
        libraryAddress = _library;
        owner = msg.sender; // 最初は自分がオーナー
    }

    // 外部のライブラリを使って値を更新する関数
    function updateValue(uint256 _newValue) public {
        // delegatecallを使うことで、VulnerableProxyのデータ領域が書き換わります
        (bool success, ) = libraryAddress.delegatecall(
            abi.encodeWithSignature("setValue(uint256)", _newValue)
        );
        require(success, "Delegatecall failed");
    }
}

このコードの何が怖いか分かりますか?
VulnerableProxy(あなたのお家)では、1番目に storedValue(数値)、2番目に owner(オーナー)が配置されています。

しかし、もし攻撃者がこの仕組みの隙をついて LibraryContract 側のロジックを巧みに悪用したり、変数のレイアウトの不一致を突いた場合、owner 変数(本来は2番目にあるべき大切なお金や権利の管理場所)が、業者さんの1番目の処理によって上書きされてしまうのです。結果として、コントラクトのオーナー権限が綺麗に奪い取られてしまいます。

—

4. 一歩ずつ対策を学んでいきましょう!安全な実装方法

「じゃあ、delegatecall なんて怖くて使えないよ…」と思ったそこのあなた、安心してください!
実務の世界では、プロキシパターン(コントラクトのアップグレード機能など)で delegatecall は今でも頻繁に使われています。ただし、鉄則のルールを守ることで安全に運用できるのです。

対策のポイント

1. ストレージレイアウトを完全に一致させる
呼び出し元と呼び出し先のコントラクトで、状態変数(owner や storedValue など)の宣言する「順番」と「データ型」を1ミリも狂わずに完全に一致させること。
2. アップグレード可能プロキシの標準規格を使う
OpenZeppelinなどの信頼されたセキュリティ企業が提供している Initializable やアップグレード用の標準ライブラリ(UUPSプロキシなど)をそのまま使い、自分でストレージの順番を適当に定義しないこと。

安全な設計のサンプルコードも見てみましょう。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// 【安全なライブラリ側】
contract SafeLibrary {
    address public owner;       // 1番目:オーナー
    uint256 public storedValue; // 2番目:数値
}

// 【安全な呼び出し元側】
contract SafeProxy {
    // ✅ 順番をライブラリと完全に一致させています!
    address public owner;       // 1番目:オーナー
    uint256 public storedValue; // 2番目:数値

    address public libraryAddress;

    constructor(address _library) {
        libraryAddress = _library;
        owner = msg.sender;
    }

    function updateValue(uint256 _newValue) public {
        // 同じレイアウトであれば、ストレージの衝突は起きません
        (bool success, ) = libraryAddress.delegatecall(
            abi.encodeWithSignature("setValue(uint256)", _newValue)
        );
        require(success, "Delegatecall failed");
    }
}

このように、変数の順番をしっかり揃えるだけで、予期せぬデータ書き換えを防ぐことができます。地味ですが、現場のエンジニアにとっては命綱となる非常に大切なルールです。

—

5. まとめ

今回は delegatecall の仕組みとストレージ衝突の危険性について、お家のリフォームに例えて解説しました。

  • delegatecall は「自分の家に業者の作業員を呼んで、自分の家のデータを直接いじってもらう」便利な機能。
  • しかし、呼び出し元と呼び出し先で「引き出しの順番(ストレージレイアウト)」がズレていると、大切なオーナー権限などが勝手に書き換えられてしまう。
  • 対策として、変数の順番とデータ型を絶対に一致させること、そして信頼された標準ライブラリの設計を遵守することが何よりも重要。

セキュリティの世界は一見すると難しく感じられますが、私たちの日常のルールや防犯の意識と結びつけて考えると、とても本質的で面白いものです。

「一歩ずつ、安全なコードへの理解を深めていくこと」。それがプロのWeb3エンジニアへの一番の近道です。これからも一緒に楽しくセキュリティを学んでいきましょう!

コメント

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