その金庫、横からこじ開けられるかも?「selfdestruct」と資金凍結の罠
こんにちは!セキュリティリサーチャーの視点から、今日はWeb3開発で新人が必ず一度は躓く(そして痛い目を見る)「コントラクトの自己破壊(selfdestruct)」というトピックについてお話しします。
「ブロックチェーンって改ざんできないから安全なんじゃないの?」と思っている方、実はコントラクトの「残高」に関しては、思わぬ盲点があるんです。家の鍵を頑丈にしても、隣の壁を壊されたら意味がないのと一緒です。一歩ずつ、紐解いていきましょう!
—
1. そもそも「selfdestruct」って何?
スマートコントラクトには、自分自身をブロックチェーン上から消し去り、残っている資金を指定したアドレスに強制送金する selfdestruct という関数が存在します(※最近のアップデートで非推奨になりつつありますが、既存のコントラクトには無数に眠っています)。
これを「悪意ある攻撃者」が使うとどうなるか。本来、お金を受け取る機能を持たないはずのコントラクトに、強制的にETH(イーサリアム)を送りつけることができるのです。
家の鍵に例えると…
あなたの家(スマートコントラクト)の玄関には「お金を受け取るためのゲート」がなく、鍵がかかっているとします。しかし、攻撃者は「隣の壁を破壊(selfdestruct)」して、あなたの家のリビングに無理やり現金を投げ込むことができます。
「えっ、お金が増えるならいいじゃん!」と思いましたか? ここが落とし穴なんです。
—
2. なぜ「強制送金」が脆弱性になるのか
多くの開発者は、コントラクトの残高を確認する際、以下のようにコードを書きます。
// 悪い例:残高に依存するロジック
function withdrawAll() public {
// もし攻撃者が無理やりETHを送りつけていたら、残高が予期せぬ数値になり、
// ロジックが狂って資金が引き出せなくなる可能性があります。
require(address(this).balance == totalDeposited, "残高が一致しません!");
payable(msg.sender).transfer(address(this).balance);
}
もし攻撃者が selfdestruct を使って 1 wei でも余計に送ると、address(this).balance と totalDeposited が一致しなくなり、withdrawAll 関数が二度と動かなくなります。 これが「資金凍結」の正体です。
—
3. どうすれば防げるの?
対策はシンプルです。「コントラクトの残高を信じすぎるな」ということです。
対策:残高をロジックの判定に使わない
自分の家の中にある金庫の重さを測るのではなく、「誰がいくら預けたか」という帳簿(マッピング)を自分で管理しましょう。
// 良い例:帳簿で管理する
mapping(address => uint256) public balances;
function deposit() public payable {
// 預け入れた額を自分の帳簿に記録する
balances[msg.sender] += msg.value;
}
function withdraw() public {
uint256 amount = balances[msg.sender];
require(amount > 0, "預け入れがありません");
// 帳簿を先にゼロにする(再入攻撃対策)
balances[msg.sender] = 0;
// 自分の帳簿の分だけ送金する
payable(msg.sender).transfer(amount);
}
このように、address(this).balance(外からいくらでも操作可能な物理的な残高)を判定に使わず、自分自身が確実に把握している balances という帳簿だけを信頼するのが、Web3セキュリティの鉄則です。
—
セキュリティリサーチャーからのアドバイス
「たかが残高チェック」と侮ってはいけません。IoTの世界でも、センサーの値をそのまま制御ロジックに使うと、外から磁石を近づけられて数値を誤魔化されるのと同じです。「外部からの入力(今回は強制送金)で、システムの前提条件が簡単に覆る場所はないか?」と常に疑う癖をつけてください。
最初は難しく感じるかもしれませんが、こうやって一つずつ「泥棒の手口」を知ることで、あなたの書くコードはより強固なものになっていきます。
次は「再入攻撃(Reentrancy)」という、さらに巧妙な泥棒の手口について解説する予定です。準備はいいですか?また次回の記事でお会いしましょう!
コメント