こんにちは!スマートコントラクトの開発に挑戦している皆さん、日々のコーディングお疲れ様です。
ブロックチェーンの世界では、「一度デプロイしたスマートコントラクトは修正できない」というのが基本ルールですよね。バグが見つかったら終わり……なんて、なんだか冷や汗が流れてしまいます。そんな悩みを解決するために生まれたのが、今回テーマにする「プロキシ(Proxy)パターン」を用いたコントラクトのアップグレード可能性です。
でも、この便利な仕組み、実はサイバー攻撃者にとっては格好の標的なんです。今回は、身近な「防犯」の例えを交えながら、攻撃のメカニズムとしっかりとした防御のコツを、一歩ずつ優しく紐解いていきましょう!
—
1. 家の鍵の仕組みで理解する「プロキシ(Proxy)パターン」
まずは、プロキシパターンが一体どんな仕組みなのか、私たちの身近な「家の鍵」を例にしてイメージしてみましょう。
皆さんが住んでいるマンションを想像してください。あなた(ユーザー)は、いつも通りエントランスの「管理人さん(プロキシ)」に声をかけます。「〇〇号室に行きたいです」と伝えると、管理人さんは奥にある本当の部屋の鍵を開け、あなたを案内してくれます。
- プロキシ(Proxy / 管理人さん):アドレスが変わらない窓口。ユーザーからのリクエストをすべて受け付けます。
- ロジック(Logic / 実際の部屋):実際の処理を行う本体。必要に応じて、中の家具や間取り(コード)を新しいものにリフォーム(アップグレード)できます。
ブロックチェーン上では、ユーザーは「管理人さん(プロキシ)」のアドレスだけに資金や命令を送ります。そして、裏側で「実際の部屋(ロジック)」の場所をこっそり新しいものに切り替えることで、アドレスを変えずに機能だけをアップデートできるようにしているわけです。とっても便利ですよね!
—
2. 攻撃者が狙う「二大の罠」
しかし、この便利な仕組みには、現場のエンジニアが思わず冷や汗をかいてしまうような、恐ろしい盲点(脆弱性)が潜んでいます。攻撃者は、この管理人さんと実際の部屋の「連携の隙」を巧みに突いてきます。
罠その1:ストレージ衝突(Storage Collision)の恐怖
先ほどのマンションの例えで、管理人さんが「メモ帳」の同じページに、住民の荷物と自分の私物をぐちゃぐちゃに書き込んでしまったらどうなるでしょうか? 「あれ、これ誰の荷物だっけ?」とパニックになりますよね。
これがスマートコントラクトでいう「ストレージ衝突」です。
プロキシとロジックは、変数のデータを保存する「棚(ストレージ・スロット)」の番号を共有しています。もし、ロジック側で変数の順番をうっかり変えてしまうと、プロキシの大切な「管理者アドレス」が、ロジック側の「ただの残高データ」で上書きされてしまうのです。
結果どうなるか? 攻撃者に「私がこのマンションのオーナーです!」と書き換えられてしまい、全財産を奪われてしまいます。
罠その2:初期化関数の乗っ取り
現実世界でも、新築のマンションに引っ越したとき、前の住人が使っていた合鍵がそのまま使えたら怖いですよね?
スマートコントラクトのアップグレード可能な世界でも同じことが起き得ます。ロジックコントラクト単体では、初期化関数(initializeなど)に誰も鍵をかけていない状態だと、デプロイされた瞬間に攻撃者が一番乗りで「私がオーナーです」と初期化を実行してしまうのです。これを「初期化関数の乗っ取り」と呼びます。
—
3. 実装コードで見る危険性と対策
それでは、実際に安全なプロキシコントラクトの設計と、気をつけるべきポイントをコードで見ていきましょう。今回は、OpenZeppelin等の標準的なライブラリをベースにしたUUPS(Universal Upgradeable Proxy Standard)パターンの例を覗いてみます。
まずは、攻撃を防ぐための「初期化の保護」と「アクセス制御」が実装された安全なロジックコントラクトのサンプルです。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// OpenZeppelinのアップグレード安全なベースコントラクトをインポート
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract MySecureVault is Initializable, OwnableUpgradeable, UUPSUpgradeable {
uint256 public secretNumber;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
// コンストラクタでロジック側の初期化を防ぐため、即座に無効化(disableInitializers)します。
// これにより、ロジックコントラクト単体が乗っ取られるのを防ぎます!
_disableInitializers();
}
function initialize(address initialOwner) initializer public {
// オーナー権限とアップグレード権限を安全に初期化します
__Ownable_init(initialOwner);
__UUPSUpgradeable_init();
}
function setSecretNumber(uint256 _number) public onlyOwner {
secretNumber = _number;
}
// UUPSパターンにおけるアップグレードの権限チェック
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
ここがポイント!安全な設計の裏側
1. コンストラクタでの _disableInitializers() の呼び出し
- 先ほどお話しした「初期化関数の乗っ取り」を防ぐための必須テクニックです。ロジックコントラクトがデプロイされた直後に初期化を封じることで、攻撃者が勝手に鍵を開けるのを防ぎます。
2. _authorizeUpgrade 関数での権限チェック
- 「誰でも勝手に新しいマンションの設計図に入れ替えていいよ」となっては大変ですよね。この関数に
onlyOwnerなどの厳格な修飾子をつけることで、許可された管理者だけがアップグレードを行えるようにします。
—
4. 現場のインシデントから学ぶ、今日からできる防犯対策
実務の現場でプロキシコントラクトを扱う際は、コードを書くだけでなく、以下のようなインフラ・運用面での「二重の鍵」をかけることが重要です。
- 自動テストツール(SlitherやEchidnaなど)の導入
- ストレージレイアウトの変更ミスや、初期化漏れを検知する静的解析ツールをCI/CDパイプラインに必ず組み込みましょう。「うっかり変数の順番を変えちゃった」というヒューマンエラーを機械が防いでくれます。
- マルチシグ(複数の鍵)の採用
- アップグレード権限をたった1つのウォレット(EOA)に持たせるのは、玄関の鍵をマットの下に置いて出かけるようなものです。Gnosis Safeなどのマルチシグウォレットを使い、複数人の承認がないとアップグレードできない体制を作りましょう。
—
まとめ
プロキシパターンは、スマートコントラクトに柔軟性をもたらす素晴らしい技術ですが、一歩間違えるとシステム全体の乗っ取りを許してしまう諸刃の剣です。
「管理人さんと部屋の仕組み」「合鍵の管理」といった身近な防犯意識を持ちながら、安全なライブラリと厳格な初期化・権限管理を徹底していきましょう。焦らず一歩ずつ、セキュアなWeb3開発のスキルを身につけていきましょうね!
コメント