「部屋の鍵を渡したはずが、持ち物まで入れ替わってた?」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. 変数の並び順は、全コントラクトで一貫させる。
セキュリティに「絶対」はありません。しかし、こうした泥臭い仕組みを知り、丁寧に設計することで、攻撃者に付け入る隙をぐっと減らすことができます。
皆さんのコントラクトが、誰からも侵されない堅牢な城であることを願っています。何か不明な点があれば、またいつでも聞いてくださいね!一歩ずつ、一緒に強くなっていきましょう。
コメント