ブロックチェーンの「間取り」を間違えると家ごと乗っ取られる? delegatecall の恐ろしい罠
こんにちは!セキュリティの世界へようこそ。今日は、ブロックチェーン開発において「最もやってはいけないミス」の一つ、「delegatecall(デリゲートコール)によるストレージ衝突」について解説します。
「ストレージ衝突? 何だか難しそう……」と思ったあなたも大丈夫。身近な例え話から、少しずつ紐解いていきましょう。
—
1. 例え話で理解する:あなたの家の「間取り図」が書き換わる恐怖
想像してみてください。あなたは、最新のスマート家電を管理する「コントローラー」という名の家を建てたとします。この家には、以下の3つの引き出しがあります。
1. 引き出しA:家の鍵
2. 引き出しB:金庫の暗証番号
3. 引き出しC:庭の植木の名前
さて、あなたは家をアップグレードするために、「外部の便利なツール(ライブラリ)」を借りてくることにしました。ここで使うのが delegatecall です。
delegatecall とは、「自分の家の引き出しを、他人のツールに直接操作させる」という仕組みです。
ここで大問題が発生します。もし、そのツールが「引き出しAには金庫の暗証番号を入れるべきだ」という全く別の間取り図を持っていたらどうなるでしょう?
あなたが「引き出しAの内容を更新して!」と頼んだ瞬間、ツールは「よし、金庫の暗証番号を更新するぞ!」と勘違いして、大事な情報を上書きしてしまうのです。これが「ストレージ衝突」の正体です。
—
2. なぜこれが「脆弱性」になるのか?
ブロックチェーン(イーサリアムなど)において、コントラクトのデータは「スロット」と呼ばれる箱に順番に格納されています。
- スロット0:
owner(誰が管理者か) - スロット1:
balance(いくら持っているか)
delegatecall を使うと、呼び出し先のコードが「自分の家のスロット」を勝手に書き換えます。もし呼び出し先のストレージ構造が少しでもズレていたら、本来書き換えるはずのない owner の値が、攻撃者のアドレスに書き換わってしまうというわけです。
—
3. 実践!脆弱なコードを見てみよう
まずは、「やってはいけない例」を見てみましょう。
// 脆弱なコントラクト(家主)
contract VulnerableWallet {
address public owner; // スロット0
uint256 public balance; // スロット1
function updateBalance(address logic, uint256 newBalance) public {
// 危険な呼び出し:外部のロジックにストレージを操作させる
logic.delegatecall(abi.encodeWithSignature("setBalance(uint256)", newBalance));
}
}
// 攻撃者が用意した不正なライブラリ
contract MaliciousLibrary {
address public attacker; // スロット0(ここでオーナーが書き換わる!)
uint256 public balance; // スロット1
function setBalance(uint256 _balance) public {
// 本来はバランスを変えるはずが、自分をオーナーに昇格させる
attacker = msg.sender;
}
}
このコードでは、VulnerableWallet の owner が、MaliciousLibrary の attacker と同じ「スロット0」に配置されています。これにより、バランスを変えるつもりが、家の所有権を奪われてしまうのです。
—
4. どうやって防ぐのか?一歩ずつ対策を学ぼう!
この攻撃を防ぐための「防犯対策」は大きく分けて2つあります。
① ストレージレイアウトを完全に一致させる
プロキシ(代理人)を使う場合は、呼び出し元と呼び出し先のストレージ構造を1ビットの狂いもなく一致させなければなりません。しかし、これは非常にミスが起きやすい手法です。
② ライブラリには library キーワードを使う
Solidityには、最初から delegatecall を安全に使うための library という仕組みがあります。これを使うと、ライブラリ側は自分自身のストレージを持つことができないため、そもそも「衝突」が発生しません。
// 安全なライブラリの書き方
library SafeMath {
// 状態変数を持たないので衝突のしようがない
function add(uint256 a, uint256 b) internal pure returns (uint256) {
return a + b;
}
}
—
最後に:セキュリティは「疑うこと」から始まる
今回学んだ「ストレージ衝突」は、一見すると些細なミスに見えます。しかし、ハッカーはこの「わずかな間取りのズレ」を見逃しません。
- 外部からのコードを呼び出すときは「自分の家の引き出し」に触れさせないこと。
- どうしても使うなら、間取り図(ストレージレイアウト)が完全に一致しているか、何度も確認すること。
これらを意識するだけで、あなたの書くスマートコントラクトは格段に強固なものになります。「便利さ」の裏には常にリスクが潜んでいる。それを忘れずに、一歩ずつ安全なコードを書いていきましょう!
もし、「自分のコードが安全か不安……」という方がいたら、まずはOpenZeppelinが提供している UUPSUpgradeable などの標準的なプロキシパターンを調べることから始めてみてください。業界のプロたちが作った「鉄壁の防犯門」をまずは活用するのが、一番の近道ですよ。
コメント