こんにちは!スマートコントラクトの開発に挑戦している皆さん、日々のコーディングお疲れ様です。「ブロックチェーンの世界はロマンがあるけれど、セキュリティの話になると、なんだか急に難しくなって緊張する…」そんな風に思っていませんか?
今回は、スマートコントラクトのセキュリティにおいて、絶対に避けて通れないテーマである「低レベルコール(callやdelegatecall)の危険性と対策」について、身近な例えを交えながら、一歩ずつ優しく紐解いていきたいと思います。
難しい専門用語が出てきても、私がしっかりサポートしますので、リラックスして読み進めてくださいね!
—
1. 家の鍵に例える「低レベルコール」の恐怖
まずは、スマートコントラクトの世界を私たちの身の回りの生活に置き換えて考えてみましょう。
皆さんが暮らす家には、頑丈な玄関のドアと、専用の「鍵」がありますよね。通常のスマートコントラクト同士のやり取りは、いわば「信頼できる合鍵を持っている家族や知人が、インターホンを押して家に入ってくる状態」です。誰が来たのか、何をしに来たのかが明確で、安全性が保たれています。
しかし、今回お話する「低レベルコール(callやdelegatecall)」を使うというのは、例えるなら「どんな見た目の人格者でも、あるいは泥棒であっても、合鍵なしで玄関のドアをこじ開けて家の中に招き入れてしまう万能の合鍵」を渡してしまうようなものなんです。
泥棒は「戻り値のチェック漏れ」を狙っている
Solidity(イーサリアムのスマートコントラクトを書く言語)では、他のコントラクトを呼び出すときにcallという機能を使うことがあります。このcallは、相手がどんなヤツであっても、とりあえず呼び出しを実行してくれる非常に強力な機能です。
ここで問題になるのが「戻り値(成否の結果)のチェック」です。
例えば、あなたが誰かに「この封筒を届けてきて!」と頼んだとします。普通のやり取りなら、相手がちゃんと届けてくれたかどうか(成功したかどうか)を確認しますよね?
しかし、プログラムの世界でこの確認(success変数のチェック)をうっかりサボってしまうと、「相手の家が火事になっていようが、泥棒の罠であっても、プログラムは何事もなかったかのように次の処理へ進んでしまう」という恐ろしい事態が起きます。攻撃者は、この「確認の甘さ」を突き、あなたの金庫からこっそり資産を持ち去ってしまうのです。
—
2. 危険なコードと安全なコードの比較
新人開発者がやりがちな「危なっかしいコード」と、それをしっかりと守る「安全なコード」を比較してみましょう。
⚠️ 危険な実装例(戻り値を無視するパターン)
以下のコードを見てください。一見すると普通に他のアドレスへETHを送金しているように見えますが、決定的な欠陥があります。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract DangerousBank {
// 危険な送金関数
function unsafeSend(address payable recipient) public payable {
// callを使っていますが、戻り値(success)を受け取っていません!
// 相手のコントラクトでエラーが発生しても、この関数は「成功した」と勘違いします。
recipient.call{value: msg.value}("");
}
}
このコードの何がいけないか分かりますか?そう、recipient.call{value: msg.value}("") の実行結果が true (成功)だったのか false (失敗)だったのかを、プログラムが全く気にしていない点です。もし相手の処理が途中で失敗しても、自分の手元の残高だけが減り、相手に届いていない…なんて理不尽なことが起きてしまいます。
✨ 安全な実装例(戻り値のチェックとrequire)
では、これをどのように直せばよいのでしょうか?答えは簡単。「ちゃんと結果を確認して、ダメなら即座にストップする」ことです。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SafeBank {
// 安全な送金関数
function safeSend(address payable recipient) public payable {
// callの戻り値(success)とデータ(data)を受け取る変数を用意します
(bool success, ) = recipient.call{value: msg.value}("");
// 【重要】もし処理が失敗していたら、ここでトランザクションを完全に巻き戻す(Revertする)
require(success, "ETHの送金に失敗しました。処理を中断します。");
}
}
このように、require(success, ...) という「門番」を置いてあげるだけで、不正な相手やトラブルのある呼び出しからコントラクトをガッチリと守ることができます。これなら安心ですよね!
—
3. さらに安全性を高める「ホワイトリスト管理」
戻り値をチェックするだけでも十分強力ですが、実務の現場ではさらに一歩進んだ防衛策を取ります。それが「ホワイトリスト(信頼できるアドレス帳)の管理」です。
先ほどの泥棒の例えに戻りましょう。「誰でも彼でも家に入れる」のではなく、「あらかじめ管理者が安全だと確認した人だけをリストに登録し、その人以外からの呼び出し、あるいはその人以外への呼び出しを一切拒否する」という仕組みを作ります。
ホワイトリストの実装イメージ
コントラクトの中に「信頼できる宛先リスト」を持たせ、低レベルコールを実行する前に「このアドレスは本当にリストに載っているか?」を厳しくチェックします。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract WhitelistManager {
// 管理者アドレス
address public owner;
// 信頼できる宛先を管理するホワイトリスト(アドレス => 許可されているか)
mapping(address => bool) public whiteList;
constructor() {
owner = msg.sender; // デプロイした人をオーナーに設定
}
// オーナーだけがホワイトリストにアドレスを追加できる
function addToWhitelist(address target) public {
require(msg.sender == owner, "オーナー権限が必要です。");
whiteList[target] = true;
}
// ホワイトリストに登録された安全な宛先だけにcallを実行する関数
function callTrustedContract(address target, bytes memory data) public {
// 【重要】呼び出し先がホワイトリストに登録されているか厳重にチェック!
require(whiteList[target], "このアドレスはホワイトリストに登録されていません!");
// 安全性が確認されたので低レベルコールを実行
(bool success, ) = target.call(data);
require(success, "外部コントラクトの実行に失敗しました。");
}
}
このように、「信頼の置ける相手をあらかじめリスト化(ホワイトリスト化)する」という一手間を加えるだけで、予期せぬ悪意あるコントラクトとの予期せぬインタラクションを根元から断つことができます。現実世界のセキュリティ対策(身元確認や会員制サロンのような仕組み)と全く同じですね。
—
4. まとめ:一歩ずつ、確実なセキュリティ意識を育てよう
今回は、低レベルコール(call / delegatecall)の危険性と、その対策について解説しました。
- 低レベルコールは非常に強力だが、リスクも大きい
- 呼び出しの際は必ず戻り値(
success)をチェックし、requireで保護する - 予期せぬコントラクトへの呼び出しを防ぐために、ホワイトリスト管理を取り入れる
セキュリティの世界は広大で、最初は覚えることが多くて圧倒されてしまうかもしれません。でも、今日学んだ「基本の確認を怠らないこと」や「信用できるものだけを選別すること」は、どんな最先端のブロックチェーン開発現場でも共通する最も大切なマインドセットです。
焦らず、一歩ずつ、自分の手で安全なコードを書けるようになっていきましょう!次回の解説もお楽しみに!
コメント