こんにちは!ブロックチェーン開発やIoTの現場に飛び込んだばかりの新人エンジニアの皆さん、日々の業務本当にお疲れ様です。
スマートコントラクトの世界って、一度ブロックチェーンにデプロイ(公開)してしまうと、後から修正するのがすごく大変ですよね。「あ、ここに書き忘れがあった!」と気づいても、鍵をかけ忘れたままお宝を置いて出かけてしまったようなもので、冷や汗が止まらなかった経験がある方もいるかもしれません。
今回は、そんなスマートコントラクトの世界でひそかに恐れられている「プロキシコントラクトの初期化不備(Uninitialized Proxy)」という脆弱性について、身近な「合鍵」の防犯にたとえながら、一緒に優しく紐解いていきたいと思います。一歩ずつ、確実にマスターしていきましょう!
—
1. 家の鍵にたとえて理解する「プロキシコントラクト」の仕組み
まず、今回の主役である「プロキシ(Proxy)」について、少しイメージを膨らませてみましょう。
皆さんが新しいお家を建てたとします。そのお家の「玄関のドア(プロキシ)」はいつも同じ場所にありますが、中の「家具や間取り(ロジックコントラクト)」は、気分や家族構成の変化に合わせていつでもリフォームして取り替えられるような最新の仕組みになっているとします。これが、アップグレード可能なプロキシパターンの基本です。
ここで問題になるのが、「新しく引っ越してきたときに、誰が玄関の鍵を新しく設定するか」という点です。
もし、ハウスメーカーが鍵を差し込んだままの状態で家に鍵をかけず、「あとは住む人が勝手に鍵を設定しておいてね」と放置してしまったらどうなるでしょうか?
あなたが引っ越しの荷解きをしている間に、通りすがりの泥棒が先に家に上がり込んで、「ここから先は俺の家だ!」とマスターキー(所有者権限)を自分のものに書き換えてしまったら……もう大変ですよね。家の中の財産はすべて奪われてしまいます。
スマートコントラクトの世界でもこれと全く同じことが起きます。それが「プロキシの初期化不備」なんです。
—
2. 攻撃者はどうやってコントラクトを乗っ取るのか?
ブロックチェーンの世界における「プロキシコントラクト」は、実際の処理を行う「ロジック(実装)コントラクト」の住所を指し示しているだけの、いわば「看板」のような存在です。
この看板を新しく設置したとき、誰がこの看板の管理者(オーナー)であるかを最初に登録する関数(これを initialize 関数などと呼びます)が存在します。
もし、この初期化関数に「誰でも何度でも呼び出せる」という隙があったり、そもそもデプロイ直後に誰も初期化していなかったりすると、恐ろしい事態が起きます。
攻撃者は、次のような手順であなたのお宝を狙っています。
1. 放置された看板を発見する: 開発者がテストネットやメインネットにプロキシをデプロイしたものの、初期化関数を呼び忘れている(あるいは保護し忘れている)コントラクトを、監視ツールを使って嗅ぎつけます。
2. 先回りして初期化する: 開発者より先に、攻撃者がそのプロキシの初期化関数を呼び出します。その際、引数に「自分自身のウォレットアドレス」を指定します。
3. オーナー権限を奪う: これにより、プロキシのオーナー(管理者)は攻撃者に書き換わります。
4. すべてをコントロールドする: オーナーになった攻撃者は、アップグレード機能を使って悪意あるプログラム(中身のロジック)にすり替え、コントラクト内に眠っていた資金をすべて根こそぎ持ち去ってしまいます。
ほんの一瞬の油断が、プロジェクト全体の致命傷になってしまうわけですね。
—
3. 脆弱なコードと修正パッチの実装例
それでは、具体的にどのようなコードが危険で、どう直せば安全になるのかを見ていきましょう。OpenZeppelinなどの標準的なライブラリを使った安全な書き方を覚えるのが一番の近道です。
❌ 危険なコード例(初期化関数が無防備な状態)
以下のコードは、初期化関数 initialize に「誰が呼んでもいいよ」という状態(アクセス制限がない)になっており、さらに二重呼び出しを防ぐガードもありません。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
// 【危険】初期化の保護が不十分なプロキシ用ロジックの例
contract VulnerableLogic is Initializable {
address public owner;
// 誰でもこの関数を呼べてしまうため、最初に実行した人にオーナーを奪われます
function initialize(address _owner) public {
owner = _owner; // 簡単にオーナー権限が書き換わる
}
}
⭕ 安全な修正パッチの適用手順
この脆弱性を防ぐためには、主に2つのアプローチをとります。
1. デプロイと同時に初期化する(Constructorでの対策):
プロキシパターンを使う場合、ロジックコントラクト側の constructor(コンストラクタ)で _disableInitializers() という関数を呼び出します。これにより、ロジックコントラクト単体が直接初期化されるのを物理的に不可能にし、攻撃者が元のロジック側を乗っ取るのを防ぎます。
2. Initializer修飾子の活用:
初期化関数には必ず initializer 修飾子をつけ、「人生で一度しか実行できない」ようにロックをかけます。
それでは、安全に修正されたコードを見てみましょう。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
// 【安全】適切に保護されたロジックコントラクトの例
contract SecureLogic is Initializable, OwnableUpgradeable {
/// @notice ロジックコントラクト自体の乗っ取りを防ぐため、デプロイ時に初期化を無効化する
constructor() {
_disableInitializers();
}
/// @notice プロキシ経由で最初に一度だけ呼ばれる初期化関数
/// @param _initialOwner このコントラクトを管理する正当なオーナーのアドレス
function initialize(address _initialOwner) public initializer {
// OpenZeppelinのOwnable初期化関数を呼び、オーナーを設定する
__Ownable_init(_initialOwner);
}
// ここにビジネスロジックを記述していきます
function doSomethingSecure() external onlyOwner {
// オーナーだけが実行できる大切な処理
}
}
このように、constructor 内での _disableInitializers() の呼び出しと、initializer 修飾子の組み合わせが、あなたの大切なコントラクトを守る頑丈な「二重の鍵」となります。
—
4. 実務で絶対にやっておきたいイン実務インシデント対策
コードを直したら安心……ではありません。現場のエンジニアとして、次のチェックリストを必ずルーティンに組み込んでおきましょう。
- スクリプトデプロイ時の自動化:
手動で「デプロイしてから後で初期化する」という手順を踏まないようにしましょう。HardhatやFoundryなどのデプロイメントスクリプト(JavaScript / TypeScript)の中で、デプロイと初期化をワンストップ(アトミック)で実行するように記述を自動化します。
- テストネットでのリハーサル:
メインネットにデプロイする前に、必ずテストネット(Sepoliaなど)でデプロイ直後のプロキシに対して「第三者として初期化関数を呼べるか」をテストコードで検証(ファジングやユニットテスト)してください。もしエラー(revert)が返ってくれば、正しく保護されている証拠です。
—
おわりに
いかがでしたでしょうか?
プロキシコントラクトの初期化不備は、仕組みを知っていれば防ぎやすい一方で、うっかり見落としがちな盲点でもあります。「家の鍵をかけ忘れていないか?」という意識を持つだけで、スマートコントラクトのセキュリティレベルは劇的に向上します。
最初は覚えることが多くて大変に感じるかもしれませんが、安全なコードの書き方を一つずつ身につけていけば、自信を持って信頼されるプロダクトを世の中に送り出すことができますよ。
一歩ずつ、確実にセキュリティの技術を自分のものにしていきましょう!それではまた次回の記事でお会いしましょう。
コメント