プロキシパターンにこれほど多くのリスクがあるなら、なぜ業界はアップグレード可能な設計を思い切って放棄し、すべて不変コントラクトに移行しないのか?
不変コントラクトにも独自の代償があり、それが別の形で現れているにすぎないからだ。あるプロトコルが数億ドル相当の資産を管理していながらコードが一切変更できない場合、後になってどんなロジックの欠陥(たとえ小さなものであっても)が発見されると、唯一の対処法は全く新しいコントラクトをデプロイし、すべてのユーザーに資産を手動で移行するよう求めることになる——このプロセス自体が本質的に遅く、摩擦に満ちており、ユーザーは怠慢や新コントラクトへの不信から欠陥のある旧バージョンに取り残されたままになる可能性もある。実際に負うリスクは、プロキシパターンよりも必ずしも低いとは限らない。
これが、業界が「アップグレード可能か不変か、どちらが絶対的に優れているか」という合意に未だに至っていない理由でもある。この選択はむしろ、「このプロトコルはバグ修正にどれだけ迅速な対応力が必要か」対「このプロトコルはそのアップグレードの柔軟性のためにどれだけの分散化度合いを犠牲にしてもよいか」というトレードオフの問題として捉えられる傾向にある。Uniswapのような中核コントラクトが不変性を選択しているのは、プロトコルのロジックが比較的安定しており、コミュニティが「いったんデプロイされたコントラクトのルールは一方的に変更されない」という約束を非常に重視しているためだ。一方で、まだ急速に反復開発を続けている多くの新興プロトコルは、集中化されたアップグレード能力と引き換えに突発的な脆弱性への迅速な対応力を得るため、プロキシパターンを選ぶ傾向にある。
Parity事件における「ライブラリコントラクトが初期化されていなかった」とは具体的にどういう意味か?なぜ初期化されていないことがこれほど深刻な結果を招いたのか?
アップグレード可能なコントラクトのアーキテクチャでは、実装コントラクト(またはライブラリコントラクト)自体のコンストラクタは通常直接使用されない。プロキシコントラクトはDELEGATECALLを通じて実装コントラクトのコードを借用しつつ、自分自身のストレージを使用するためだ——これは、実装コントラクト自身のストレージ状態が理論上は空のままであるべきことを意味し、実際の初期化動作は、initialize()という別の関数を通じて、デプロイ後に手動で呼び出すことで完了させる必要がある。問題はまさにここにある——開発者がこの初期化関数の呼び出しを忘れた場合、あるいはこの関数が「一度しか呼び出せない」という保護をきちんと施していなかった場合、誰でもその関数を先に呼び出して、自分自身をそのコントラクトの所有者に設定できてしまう可能性がある。
Parity事件では、まさにこのライブラリコントラクトの初期化関数が正しくロックされていなかったために、あるユーザー(おそらく好奇心か誤操作によるもの)が初期化関数を呼び出し、自分自身を所有者に設定し、続いてそのコントラクトに組み込まれていたselfdestruct機能を発動させ、ライブラリ全体のコードをチェーンから消し去ってしまった。このライブラリに依存していたすべてのプロキシウォレットは、本質的には呼び出しを転送するだけで自らはロジックを一切保持していなかったため、ライブラリが消滅した瞬間、これらのウォレットは何もない空間を指す空の殻と化し、中の資産を読み取ったり移動させたりできるコードはどこにも残らなくなった。
一般ユーザーはEtherscanで「Read as Proxy」を確認する以外に、プロキシコントラクトのアップグレード権限が適切に管理されているかを判断する方法は他にあるか?
アップグレード制御アドレスがマルチシグウォレットかどうかを確認する以外に、そのマルチシグの署名しきい値の設定をさらに調べることができる——例えば「5人の署名者のうち3人の同意が必要」と「2人の署名者のうち1人の同意が必要」では、代表する安全水準が大きく異なる。しきい値が低すぎると、マルチシグの保護効果は単一の鍵とほとんど変わらなくなる。この情報は通常、使用されているマルチシグツール(Gnosis Safeなど)の公式インターフェースで確認でき、現在の署名者リストと必要なしきい値が表示されている。
もう一つ調べる価値のある手がかりは、タイムロック機構の有無だ——タイムロックとは、アップグレードの指示が既に送信されていても、実際に有効になるまでに公開された遅延期間(例えば48時間)を待たなければならないことを意味する。この期間中、コミュニティやユーザーは理論上、これから行われるアップグレードの内容を観察でき、何か不審な点があれば、事前に資産を引き出す機会がまだ残されている。あるプロトコルのアップグレードが「取引送信後、即座に有効になる」場合、ユーザーには反応する時間がまったくないことを意味し、タイムロックによる保護があるプロトコルとは、実際に負うリスクのレベルがまったく異なる。これらの情報は通常、コントラクトコード、公式文書、ガバナンスフォーラムを突き合わせて確認する必要があり、単一のページで直接まとめて見せてくれるわけではないが、大口の資産を預ける前に時間をかけて確認する価値はある。
自分の資産を保有しているプロトコルがアップグレード可能なコントラクトで、アップグレード権限もマルチシグでないことが分かった場合、すぐに資産を引き出すべきか?
必ずしも即座にパニック的に引き出す必要はないが、これは無視できる技術的な細部ではなく、確かに真剣に評価すべきリスクシグナルだ。アップグレード可能なコントラクト自体が危険を意味するわけではない——信頼性が高く長期間安定して運営されている多くのプロトコルもプロキシパターンを採用している。重要な違いは「アップグレード権限のガバナンスの質」であり、「アップグレード可能な設計を採用しているかどうか」という二者択一の判断ではない。アップグレード権限がマルチシグではないとしても、支配アドレス自体が著名な機関のウォレットで、公開された透明なガバナンス記録が確認できるなら、リスク評価は完全に匿名で経歴を確認できない支配アドレスの場合とはまったく異なる。
より実践的なアプローチは、「アップグレード権限のガバナンスの質」を、既に行っているデューデリジェンスのチェックリストに組み込むことだ——監査報告書、TVL規模、チーム背景と同じ検証レベルに置き、単独で白黒の判断を下すのではない。評価の結果、リスクが自分の許容範囲を超えると感じたなら、単一のリスク要因ですぐに全額清算するよりも、段階的にエクスポージャーを減らす方が実践的だ。しかし、アップグレード権限の集中、タイムロックの欠如、チーム背景の検証困難といった複数のリスク要因が同時に見つかった場合は、確かに警戒レベルを高め、ポジションの縮小を真剣に検討すべきである。
スマートコントラクトには設計上の根本的な特性がある——一度デプロイされると、コードは変更できない。これが「不変性」という信頼保証の基盤だ。しかし現実のプロトコルは継続的なバグ修正や新機能追加を必要とするため、「プロキシパターン」が業界標準の解決策となった——不変に見える表面の下で、コントラクトの実際のロジックを交換可能な状態に保つ仕組みだ。この記事では、プロキシパターンが実際どのように機能するのか、なぜそれが利便性とリスクの両方の源になるのかを解説し、実際に損失を発生させた3つの事例を通じて、この設計選択が具体的にどのように問題を引き起こすかを示す。
すべてのアップグレード可能なコントラクトは同じ基盤メカニズムに依存している——DELEGATECALLオペコードだ。ユーザーがプロキシコントラクトの何らかの関数を呼び出すと、プロキシ自体はロジックを実行せず、DELEGATECALLを通じて別の「実装コントラクト」に呼び出しを転送するが、実行時にはプロキシ自身のストレージを使用する。これにより、実装コントラクトのコードロジックはいつでも新バージョンに交換でき、新バージョンのロジックが同じプロキシアドレスに紐づいている限り、ユーザーの呼び出しエントリポイントと資産の保管場所はまったく変更する必要がない——これこそが、コントラクトアドレスの不変性を壊すことなく「アップグレード」を実現可能にする鍵だ。
業界には現在、主に3種類のプロキシパターン実装方式があり、それぞれリスクの輪郭が異なる。UUPS(EIP-1822)はアップグレードロジックを実装コントラクト自体に記述し、プロキシコントラクトを極限まで簡素化する。利点はガスの節約だが、代償として新しい実装バージョンごとにアップグレード関数を正しく保持しなければならない——もしあるバージョンのデプロイ時にこれが漏れていれば、コントラクトは永久にロックされ、二度とアップグレードできなくなる。Transparent Proxyは OpenZeppelinによって普及したパターンで、管理者権限と一般ユーザーの呼び出しを分離し、関数セレクタの衝突を回避するが、アップグレード権限は単一の管理者アドレスに集中する——このアドレスが侵害されれば、攻撃者は直接ロジックコントラクトを交換できる。Beacon Proxyは第3の要素——ビーコンコントラクト——を導入し、複数のプロキシインスタンスが同じビーコンを共有する。ビーコンを一度アップグレードすれば、それに依存するすべてのプロキシが同期して更新される。同一ロジックを大量にデプロイするシナリオに適しているが、代償として「ビーコンの管理権限」がシステム全体のルートレベル権限に等しくなる——ビーコンが悪意あるバージョンにアップグレードされれば、それに依存するすべてのプロキシが同時に被害を受ける。
2017年のParityマルチシグウォレット事件は、このデザインパターンにおける最も早く、そして最も痛ましい教訓の一つだ——複数のウォレットで共有されていたライブラリコントラクトが、正しく初期化されたことがなかったため、あるユーザーによって偶発的にselfdestructが発動されてしまった。これによりライブラリは瞬時に消滅し、それに依存していたすべてのプロキシウォレットは、もはや存在しないロジックコントラクトを指し示すことになり、実行可能なコードを永久に失った——総額約3億ドルの資産がそれ以降永久にロックされ、動かせなくなった。2024年のRadiant Capital事件は、プロキシ権限が侵害された場合の直接的な結果を示している——攻撃者はマルチシグ署名者を侵害することで、貸付プールコントラクトをアップグレードする権限を取得し、transferOwnership()を実行してロジックコントラクトを悪意あるバージョンに置き換え、約5,000万ドルの損失をもたらした。同年のIoTeXクロスチェーンブリッジ事件はさらに直接的だった——攻撃者は単純にupgrade()関数を呼び出し、すべてのセキュリティチェックを取り除いた悪意あるバージョンで実装コントラクトをそのまま置き換え、約440万ドルの損失をもたらした。3つの事件の手法は異なるが、共通点がある——プロキシパターンは「誰がコントラクトのロジックを変更できるか」という問題を、「誰があのアップグレード鍵を握っているか」という問題に単純化してしまうのだ。
次にどこかのDeFiプロトコルに資産を預ける前に、それがしばらく稼働しているか、監査を受けているかを確認するだけでなく、もう一段階踏み込む価値がある——Etherscanでそのコントラクトアドレスを調べ、「Contract」タブに「Read as Proxy」と表示されているかを確認するのだ。もし表示されていれば、これはアップグレード可能なコントラクトであり、それが指し示す実装コントラクトのアドレスを追跡し、アップグレード権限が現在どのアドレスに握られているかを確認するとよい。支配アドレスが一般的な外部所有アカウント(EOA)であり、マルチシグウォレットでない場合、あるいはアップグレードが即座には有効にならないようにするタイムロック機構が見当たらない場合、これはこのプロトコルの中核ロジックが、理論上あなたに何の警告もなく単一の秘密鍵によって完全に書き換えられうることを意味する——これはコントラクトが監査を受けているかどうかとはまったく別のリスク次元だ。監査がチェックするのは「現在のバージョンのコードが正しく書かれているか」であり、「今後入れ替わるコードが信頼できる人物によって書かれるかどうか」には関与しない。