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

「部屋の鍵を渡したはずが、持ち物まで入れ替わってた?」delegatecallの落とし穴

こんにちは!セキュリティの世界へようこそ。今日は、ブロックチェーン開発で誰もが一度はぶつかる「delegatecall(デリゲートコール)」という強力で、それゆえに危険な機能についてお話しします。

「自分のコードじゃない機能を、外部のライブラリから借りてきて実行する」――これ、プログラミングではよくありますよね。でも、その借り方がまずいと、あなたの家の金庫の中身が、知らないうちに他人のものと入れ替わってしまう……そんな恐ろしいことが起きてしまうのです。

今回は、なぜそんなことが起きるのか、どうやって防げばいいのかを、日常の例えを交えて紐解いていきましょう!

—

1. delegatecall とは何か?(家で例えると)

想像してみてください。あなたは自分の家の金庫(コントラクト)を持っています。この金庫には、「金塊(変数A)」と「銀貨(変数B)」が入っています。

ある日、あなたは「計算が面倒だから、隣の家の専門家(ライブラリコントラクト)に計算してもらう機能を取り入れよう」と考えました。ここで使うのが delegatecall です。

  • 通常の呼び出し(Call): 隣の家の専門家を呼び出し、「計算して結果だけ教えて」と頼むこと。金庫の中身は安全です。
  • delegatecall: 「隣の家の専門家を自分の家の中に招き入れ、自分の金庫を自由に使わせてあげること」です。

一見便利ですが、もしその専門家が「金塊を捨てる」という悪意を持っていたり、計算の仕方を間違えて「金塊の棚に銀貨を詰め込む」ような設計だったりしたらどうでしょう? あなたの金庫の中身はめちゃくちゃになりますよね。これが「ストレージ衝突」の正体です。

—

2. なぜ「ストレージ衝突」が起きるのか

Solidityの世界では、変数は「スロット」という番号付きの棚に順番に並べられています。

  • スロット0:金塊
  • スロット1:銀貨

もし、呼び出される側のライブラリが、うっかり「スロット0に別のものを入れる」という処理をしていたら……。呼び出し元の金庫の「金塊」が上書きされて消滅します。これがデータ破壊のメカニズムです。

危険なコードの例

// 呼び出し元のコントラクト
contract MyVault {
    uint256 public gold;  // スロット0
    uint256 public silver; // スロット1

    function update(address lib, uint256 _val) public {
        // 外部のライブラリを自分の権限で実行させてしまう
        lib.delegatecall(abi.encodeWithSignature("set(uint256)", _val));
    }
}

// 悪意ある(またはバグのある)ライブラリ
contract MaliciousLib {
    uint256 public dummy; // スロット0を占拠してしまう!
    
    function set(uint256 _val) public {
        dummy = _val; // 本来は別の変数のはずが、MyVaultのスロット0(gold)を上書き!
    }
}

このコードを実行すると、MyVault の gold が、外部の dummy によって意図せず書き換えられてしまいます。

—

3. どうすれば防げるのか?(防御の心得)

一歩ずつ対策を学んでいきましょう!ポイントは「相手を信頼しすぎないこと」です。

① ストレージレイアウトを完全に一致させる

ライブラリ側と呼び出し元側で、変数の定義順序を完全に一致させましょう。これは鉄則です。

② ライブラリにはステートを持たせない(重要!)

最も安全なのは、ライブラリ側に「状態(変数)」を持たせないことです。計算だけを行う「ライブラリ専用」の記述方法を使いましょう。

// libraryキーワードを使うと、状態変数を持てないので安全!
library MathLib {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        return a + b;
    }
}

③ プロキシパターンには「EIP-1967」を使う

スマートコントラクトのアップグレードなどで delegatecall を使う場合は、自作せずに、世界中のセキュリティリサーチャーが検証済みの「EIP-1967」などの標準仕様を使いましょう。これらはストレージの場所が衝突しないように、特定の場所にデータを配置する仕組みを備えています。

—

まとめ:セキュリティは「疑うこと」から始まる

delegatecall は非常に強力なツールです。しかし、使い方を誤ると、自分の城の壁を自分で壊すようなことになりかねません。

1. 「自分の家の中(ストレージ)」を他人にいじらせない意識を持つ。
2. ライブラリには「状態(変数)」を持たせない設計にする。
3. 変数の並び順は、全コントラクトで一貫させる。

セキュリティに「絶対」はありません。しかし、こうした泥臭い仕組みを知り、丁寧に設計することで、攻撃者に付け入る隙をぐっと減らすことができます。

皆さんのコントラクトが、誰からも侵されない堅牢な城であることを願っています。何か不明な点があれば、またいつでも聞いてくださいね!一歩ずつ、一緒に強くなっていきましょう。

コメント

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