プロキシ パターンにより、スマート コントラクトはオンチェーン アドレスと状態値を維持しながらロジックをアップグレードできます。プロキシ コントラクトへの呼び出しでは、delegateCall を通じてロジック コントラクトのコードが実行され、プロキシ コントラクトの状態が変更されます。この記事では、代理契約の種類、関連するセキュリティ インシデントと推奨事項、代理契約を使用するためのベスト プラクティスの概要を説明します。**アップグレード可能なコントラクトとプロキシ モードの概要**ブロックチェーンの「改ざん不可能」機能は誰もが知っており、スマート コントラクト コードはブロックチェーンに展開された後は変更できません。したがって、開発者がロジックのアップグレード、バグ修正、またはセキュリティ更新のためにコントラクト コードを更新したい場合は、新しいコントラクトをデプロイする必要があり、新しいコントラクト アドレスが生成されます。この問題を解決するには、プロキシ モードを使用します。プロキシ モードは、コントラクトの展開アドレスを変更せずにコントラクトのアップグレード可能性を実現します。これは、現在最も一般的なコントラクト アップグレード モードです。プロキシ モードは、プロキシ契約とロジック実装契約を含む **アップグレード可能な契約システム**です。プロキシ コントラクトは、ユーザー インタラクション、データおよびコントラクト状態のストレージを処理します。ユーザーがプロキシ コントラクトを呼び出すと、delegatecall() を通じてロジック コントラクトのコードが実行され、それによってプロキシ コントラクトの状態が変更されます。アップグレードは、代理契約所定の格納スロットに記録されている論理契約アドレスを更新することにより実現される。さらに従来の 3 つのプロキシ モードは、透過プロキシ、UUPS プロキシ、およびビーコン プロキシです。**透過的プロキシ**透過プロキシモードでは、アップグレード機能はプロキシ契約に実装されます。プロキシ コントラクトの管理者ロールには、プロキシ コントラクトを操作して、プロキシに対応する論理実装アドレスを更新する直接権限が与えられます。管理者権限を持たない発信者は、通話を実装コントラクトに委任します。注: プロキシ コントラクト管理者は、実装コントラクトと対話できないため、コントラクトの論理実装において重要な役割になることはできません。また、一般ユーザーになることもできません。**UUPS プロキシ**UUPS (Universal Upgradeable Proxy Standard) モードでは、ロジック コントラクトにコントラクト アップグレード機能が実装されます。アップグレード メカニズムはロジック コントラクトに格納されているため、アップグレードされたバージョンではアップグレード関連のロジックを削除して、将来のアップグレードを禁止できます。このモードでは、プロキシ コントラクトへのすべての呼び出しがロジック実装コントラクトに転送されます。**ビーコンプロキシ**ビーコン プロキシ モードでは、ビーコン コントラクトを参照することで、複数のプロキシ コントラクトが同じロジック実装を共有できます。ビーコン コントラクトは、呼び出されるプロキシ コントラクトのロジック実装コントラクトのアドレスを提供します。新しいロジック実装アドレスにアップグレードする場合は、ビーコン コントラクトに記録されているアドレスのみを更新する必要があります。**プロキシの悪用とセキュリティ インシデント**開発者はプロキシ モード コントラクトを利用して、アップグレード可能なコントラクト システムを実装できます。ただし、プロキシ モードにも特定の動作しきい値があり、不適切に使用すると、プロジェクトに壊滅的なセキュリティ問題を引き起こす可能性があります。次のセクションでは、プロキシの誤用に関連するインシデントと、プロキシがもたらす集中化のリスクを紹介します。**プロキシ管理キーの開示**プロキシ管理者は、透過プロキシ モードのアップグレード メカニズムを制御します。管理者の秘密キーが漏洩すると、攻撃者はロジック コントラクトをアップグレードし、プロキシ状態で独自の悪意のあるロジックを実行する可能性があります。2021 年 3 月 5 日、PAID ネットワークは不十分な秘密鍵管理によって引き起こされた「ミント」攻撃を受けました。 PAID ネットワークは、プロキシ管理者の秘密キーを盗んだ攻撃者によって悪用され、アップグレード メカニズムをトリガーしてロジック コントラクトを変更しました。アップグレード後、攻撃者はユーザーの PAID を破壊し、後で販売できる自分用の PAID のバッチを鋳造することができます。コード自体にはセキュリティ上の脆弱性はありませんが、攻撃者はコントラクトをアップグレードするための秘密キーを管理者から入手しました。** 初期化されていない UUPS プロキシ実装 **UUPS プロキシ モードの場合、プロキシ コントラクトの初期化中に、呼び出し元によって初期パラメータがプロキシ コントラクトに渡され、その後、プロキシ コントラクトがロジック コントラクト内の initialize() 関数を呼び出して初期化を実行します。通常、initialize() 関数は「initializer」修飾子で保護され、関数の呼び出しが 1 回のみに制限されます。 initialize() 関数を呼び出した後、プロキシ コントラクトの観点からは、ロジック コントラクトが初期化されます。ただし、ロジック コントラクトの観点から見ると、initialize() がロジック コントラクト内で直接呼び出されないため、ロジック コントラクトは初期化されません。ロジック コントラクト自体が初期化されていない場合、誰でも initialize() 関数を呼び出して初期化し、状態変数を悪意のある値に設定して、ロジック コントラクトを乗っ取る可能性があります。ロジック コントラクトが引き継がれた場合の影響は、システム内のコントラクト コードによって異なります。最悪の場合、攻撃者は UUPS プロキシ モードのロジック コントラクトを悪意のあるコントラクトにアップグレードし、「自己破壊」関数呼び出しを実行する可能性があります。これにより、プロキシ コントラクト全体が役に立たなくなり、コントラクト内の資産が破壊される可能性があります。永久に破壊されて失われます。**場合**① パリティマルチシグフリーズ:ロジックコントラクトが初期化されません。攻撃者は、selfdestruct() を呼び出すことによって、多くのウォレットの初期化をトリガーし、コントラクト内のイーサをロックします。② Harvest Finance、Teller、KeeperDAO、Rivermen はいずれも初期化されていないロジック コントラクトを使用しているため、攻撃者がコントラクトの初期化パラメータを任意に設定し、delegatecall() 中に selfdestruct() を実行してプロキシ コントラクトを破棄することができます。**ストレージの競合**アップグレード可能なコントラクト システムでは、プロキシ コントラクトは状態変数を宣言しませんが、疑似ランダム ストレージ スロットを使用して重要なデータを保存します。プロキシ コントラクトは、宣言された場所に相対的な論理コントラクト状態変数の値を保存します。プロキシ コントラクトが独自の状態変数を宣言し、プロキシ コントラクトとロジック コントラクトの両方が同じストレージ スロットを使用しようとすると、ストレージの競合が発生します。OpenZeppelin ライブラリによって提供されるプロキシ コントラクトは、コントラクト内で状態変数を宣言しませんが、EIP 1967 標準に基づいて、競合を防ぐために、保存する必要がある値 (管理アドレスなど) を特定のストレージ スロットに保存します。**場合**北京時間の2022年7月23日、分散型音楽プラットフォームAudiusがハッキングされ、プロキシ契約に新たなロジックが導入され、ストレージの競合が発生したことが原因であった。プロキシ コントラクトは proxyAdmin アドレス状態変数を宣言しており、ロジック コントラクト コードが実行されると、その値が誤って読み取られます。プロジェクト当事者によってカスタマイズされた proxyAdmin の値が、initialized およびInitialized の値と誤ってみなされたため、initializer 修飾子が間違った結果を返し、これにより攻撃者は、initialize() 関数を再度呼び出し、自分自身に initialize() 関数を管理する権限を与えることができました。契約。その後、攻撃者は Audius 資産を盗むために、投票パラメータを変更し、悪意のある提案を通過させました。**ロジック コントラクトまたは信頼できないコントラクトで delegatecall() を呼び出す**delegatecall() が論理コントラクトに存在し、そのコントラクトが呼び出しのターゲットを適切に検証しないとします。この場合、攻撃者はこの関数を悪用して、管理する悪意のあるコントラクトへの呼び出しを実行したり、ロジック実装を破壊したり、カスタム ロジックを実行したりする可能性があります。同様に、ロジック コントラクトに無制限の address.call() 関数がある場合、攻撃者が悪意を持ってアドレス フィールドとデータ フィールドを提供すると、それがプロキシ コントラクトとして使用される可能性があります。**場合**Pickle Finance、Furucombo、dYdX の攻撃。これらのインシデントでは、脆弱なコントラクトがユーザー トークンによって承認されており、コントラクトのアドレスとデータを呼び出すためにユーザーが提供した call()/delegatecall() がコントラクト内に存在し、攻撃者はtransferFrom() 関数は、ユーザー残高を引き出すためのコントラクトです。 dYdX 事件の際、dYdX は資金を保護するために独自のホワイトハット攻撃を実行しました。**ベストプラクティス****一般的**(1) 必要な場合にのみプロキシ モードを使用するすべての契約をアップグレードする必要があるわけではありません。上で示したように、プロキシ パターンの使用には多くのリスクが伴います。プロキシ管理者はコミュニティの同意なしに契約をアップグレードできるため、「アップグレード可能」プロパティは信頼性の問題も引き起こします。必要な場合にのみ、プロキシ パターンをプロジェクトに統合することをお勧めします。(2) プロキシライブラリは改変しないでください。プロキシ コントラクト ライブラリは複雑で、特にストレージ管理とアップグレード メカニズムを扱う部分が複雑です。変更にエラーがあると、プロキシ コントラクトとロジック コントラクトの動作に影響します。監査中に発見された多数の重大度の高いエージェント関連のバグは、エージェント ライブラリの不適切な変更が原因でした。オーディウス事件は、代理店契約の不適切な変更がもたらす結果を示す代表的な例です。**代理店契約運営・管理のポイント**(1) ロジックコントラクトの初期化攻撃者は初期化されていないロジック コントラクトを乗っ取り、プロキシ コントラクト システムを侵害する可能性があります。そのため、デプロイ後にロジック コントラクトを初期化するか、ロジック コントラクトのコンストラクターで \_disableInitializers() を使用して初期化を自動的に無効にしてください。(2) エージェント管理アカウントのセキュリティを確保するアップグレード可能な契約システムでは通常、契約のアップグレードを管理するために「プロキシ管理者」という特権ロールが必要です。管理キーが漏洩すると、攻撃者は契約を悪意のある契約に自由にアップグレードでき、ユーザーの資産を盗むことができます。ハッキングの潜在的なリスクを回避するために、プロキシ管理者アカウントの秘密キーを慎重に管理することをお勧めします。マルチシグネチャウォレットを使用すると、単一ポイントの鍵管理の失敗を防ぐことができます。(3) 透過的なプロキシ管理のために別のアカウントを使用するプロキシ管理とロジック ガバナンスは、ロジック実装との対話の損失を防ぐために別個のアドレスである必要があります。プロキシ管理と論理ガバナンスが同じアドレスを参照する場合、特権機能を実行する呼び出しは転送されないため、ガバナンス機能の変更が禁止されます。**プロキシ契約ストレージ関連**(1) プロキシコントラクトで状態変数を宣言する場合は注意してくださいAudius ハックで説明されているように、プロキシ コントラクトは独自の状態変数を宣言するときに注意する必要があります。プロキシ コントラクトで通常の方法で宣言された状態変数は、データの読み取りおよび書き込み時にデータの競合を引き起こす可能性があります。プロキシ コントラクトに状態変数が必要な場合は、ロジック コントラクト コードの実行時の競合を防ぐために、値を EIP1967 などのストレージ スロットに保存します。(2) 変数の宣言順序とロジックコントラクトの型を維持するロジック コントラクトの各バージョンは、状態変数の同じ順序とタイプを維持する必要があり、新しい状態変数は既存の変数の最後に追加する必要があります。そうしないと、デリゲート呼び出しによってプロキシ コントラクトによって、保存されている不正な値が読み取られたり、上書きされたりする可能性があり、古いデータが新しく宣言された変数に関連付けられる可能性があり、アプリケーションに深刻な問題を引き起こす可能性があります。(3) 基本契約にストレージギャップを含めるロジック コントラクトでは、新しいロジック実装をデプロイするときに新しい状態変数を予測するために、コントラクト コードにストレージ ギャップを含める必要があります。新しい状態変数を追加した後、ギャップのサイズを適切に更新する必要があります。(4) コンストラクタや宣言処理では状態変数の値を設定しないでください。宣言中またはコンストラクター内で状態変数を割り当てると、ロジック コントラクトの値にのみ影響し、プロキシ コントラクトには影響しません。不変でないパラメータは、initialize() 関数を使用して割り当てる必要があります。**契約の継承**(1) アップグレード可能なコントラクトは、他のアップグレード可能なコントラクトからのみ継承できます。アップグレード可能な契約は、アップグレード不可能な契約とは構造が異なります。たとえば、コンストラクターはエージェント状態の変更と互換性がなく、initialize() 関数を使用して状態変数を設定します。別のコントラクトを継承するコントラクトは、継承されたコントラクトの initialize() 関数を使用して、それぞれの変数を割り当てる必要があります。 OpenZeppelin ライブラリを使用する場合、または独自のコードを作成する場合は、アップグレード可能なコントラクトが他のアップグレード可能なコントラクトのみを継承できることを確認してください。(2) ロジック コントラクト内で新しいコントラクトをインスタンス化しないでください。Solidity を通じて作成およびインスタンス化されたコントラクトはアップグレードできません。コントラクトは個別にデプロイし、そのアドレスをパラメータとしてアップグレード可能なロジック コントラクトに渡して、アップグレード可能な状態を実現する必要があります。(3) 親契約の初期化リスク親コントラクトを初期化するとき、\_\_{ContractName}\_init 関数はその親コントラクトを初期化します。 \_\_{ContractName}\_init を複数回呼び出すと、親コントラクトの 2 回目の初期化が発生する可能性があります。 \_\_{ContractName}\_init\_unchained() は {ContractName} のパラメータを初期化するだけであり、その親コントラクトの初期化子は呼び出されないことに注意してください。ただし、これは推奨されません。すべての親コントラクトを初期化する必要があり、必要なコントラクトを初期化しないと、将来の実行に問題が発生するためです。**ロジックコントラクトの実装****信頼できないコントラクトへの selfdestruct() または selegatecall()/call() を回避します**コントラクトに selfdestruct() または delegatecall() が含まれている場合、攻撃者がこれらの関数を使用してロジックの実装を破壊したり、カスタム ロジックを実行したりする可能性があります。開発者はユーザー入力を検証し、コントラクトによるデリゲートコール/信頼できないコントラクトへの呼び出しの実行を許可しないようにする必要があります。また、複数のコントラクトのデリゲート チェーンでストレージ レイアウトを管理するのが面倒になるため、ロジック コントラクトで delegatecall() を使用することはお勧めできません。**最後に書きました**プロキシ コントラクトは、展開後にプロトコルがコード ロジックを更新できるようにすることで、ブロックチェーンの不変の性質をバイパスします。ただし、プロキシ コントラクトの開発には依然として細心の注意が必要であり、不適切な実装はプロジェクトのセキュリティとロジックの問題を引き起こす可能性があります。全体として、ベスト プラクティスは、トランスペアレント、UUPS、およびビーコン プロキシ モードのそれぞれが、それぞれのユースケースに合わせて実証済みのアップグレード メカニズムを備えているため、信頼できる広範にテストされたソリューションを使用することです。これに加えて、攻撃者がエージェントのロジックを変更できないように、エスカレーションするエージェントの特権ロールも安全に管理する必要があります。ロジック実装コントラクトでは、攻撃者による selfdestruct() などの悪意のあるコードの実行を防ぐことができる delegatecall() を使用しないように注意する必要もあります。ベスト プラクティスに従うことで、アップグレード可能な柔軟性を維持しながら安定したプロキシ コントラクトのデプロイメントが保証されますが、すべてのコードはプロジェクトを危険にさらす可能性のある新たなセキュリティまたはロジックの問題が発生する傾向があります。したがって、すべてのコードは、プロキシ契約プロトコルの監査とセキュリティ保護の経験を持つセキュリティ専門家のチームによって監査されるのが最適です。
ブロックチェーンの不変性を打ち破る: プロキシ モデルがスマート コントラクトのアップグレードを実現する方法
プロキシ パターンにより、スマート コントラクトはオンチェーン アドレスと状態値を維持しながらロジックをアップグレードできます。プロキシ コントラクトへの呼び出しでは、delegateCall を通じてロジック コントラクトのコードが実行され、プロキシ コントラクトの状態が変更されます。
この記事では、代理契約の種類、関連するセキュリティ インシデントと推奨事項、代理契約を使用するためのベスト プラクティスの概要を説明します。
アップグレード可能なコントラクトとプロキシ モードの概要
ブロックチェーンの「改ざん不可能」機能は誰もが知っており、スマート コントラクト コードはブロックチェーンに展開された後は変更できません。
したがって、開発者がロジックのアップグレード、バグ修正、またはセキュリティ更新のためにコントラクト コードを更新したい場合は、新しいコントラクトをデプロイする必要があり、新しいコントラクト アドレスが生成されます。
この問題を解決するには、プロキシ モードを使用します。
プロキシ モードは、コントラクトの展開アドレスを変更せずにコントラクトのアップグレード可能性を実現します。これは、現在最も一般的なコントラクト アップグレード モードです。
プロキシ モードは、プロキシ契約とロジック実装契約を含む アップグレード可能な契約システムです。
プロキシ コントラクトは、ユーザー インタラクション、データおよびコントラクト状態のストレージを処理します。ユーザーがプロキシ コントラクトを呼び出すと、delegatecall() を通じてロジック コントラクトのコードが実行され、それによってプロキシ コントラクトの状態が変更されます。アップグレードは、代理契約所定の格納スロットに記録されている論理契約アドレスを更新することにより実現される。
さらに従来の 3 つのプロキシ モードは、透過プロキシ、UUPS プロキシ、およびビーコン プロキシです。
透過的プロキシ
透過プロキシモードでは、アップグレード機能はプロキシ契約に実装されます。プロキシ コントラクトの管理者ロールには、プロキシ コントラクトを操作して、プロキシに対応する論理実装アドレスを更新する直接権限が与えられます。管理者権限を持たない発信者は、通話を実装コントラクトに委任します。
注: プロキシ コントラクト管理者は、実装コントラクトと対話できないため、コントラクトの論理実装において重要な役割になることはできません。また、一般ユーザーになることもできません。
UUPS プロキシ
UUPS (Universal Upgradeable Proxy Standard) モードでは、ロジック コントラクトにコントラクト アップグレード機能が実装されます。アップグレード メカニズムはロジック コントラクトに格納されているため、アップグレードされたバージョンではアップグレード関連のロジックを削除して、将来のアップグレードを禁止できます。このモードでは、プロキシ コントラクトへのすべての呼び出しがロジック実装コントラクトに転送されます。
ビーコンプロキシ
ビーコン プロキシ モードでは、ビーコン コントラクトを参照することで、複数のプロキシ コントラクトが同じロジック実装を共有できます。ビーコン コントラクトは、呼び出されるプロキシ コントラクトのロジック実装コントラクトのアドレスを提供します。新しいロジック実装アドレスにアップグレードする場合は、ビーコン コントラクトに記録されているアドレスのみを更新する必要があります。
プロキシの悪用とセキュリティ インシデント
開発者はプロキシ モード コントラクトを利用して、アップグレード可能なコントラクト システムを実装できます。ただし、プロキシ モードにも特定の動作しきい値があり、不適切に使用すると、プロジェクトに壊滅的なセキュリティ問題を引き起こす可能性があります。次のセクションでは、プロキシの誤用に関連するインシデントと、プロキシがもたらす集中化のリスクを紹介します。
プロキシ管理キーの開示
プロキシ管理者は、透過プロキシ モードのアップグレード メカニズムを制御します。管理者の秘密キーが漏洩すると、攻撃者はロジック コントラクトをアップグレードし、プロキシ状態で独自の悪意のあるロジックを実行する可能性があります。
2021 年 3 月 5 日、PAID ネットワークは不十分な秘密鍵管理によって引き起こされた「ミント」攻撃を受けました。 PAID ネットワークは、プロキシ管理者の秘密キーを盗んだ攻撃者によって悪用され、アップグレード メカニズムをトリガーしてロジック コントラクトを変更しました。
アップグレード後、攻撃者はユーザーの PAID を破壊し、後で販売できる自分用の PAID のバッチを鋳造することができます。コード自体にはセキュリティ上の脆弱性はありませんが、攻撃者はコントラクトをアップグレードするための秘密キーを管理者から入手しました。
** 初期化されていない UUPS プロキシ実装 **
UUPS プロキシ モードの場合、プロキシ コントラクトの初期化中に、呼び出し元によって初期パラメータがプロキシ コントラクトに渡され、その後、プロキシ コントラクトがロジック コントラクト内の initialize() 関数を呼び出して初期化を実行します。
通常、initialize() 関数は「initializer」修飾子で保護され、関数の呼び出しが 1 回のみに制限されます。 initialize() 関数を呼び出した後、プロキシ コントラクトの観点からは、ロジック コントラクトが初期化されます。
ただし、ロジック コントラクトの観点から見ると、initialize() がロジック コントラクト内で直接呼び出されないため、ロジック コントラクトは初期化されません。ロジック コントラクト自体が初期化されていない場合、誰でも initialize() 関数を呼び出して初期化し、状態変数を悪意のある値に設定して、ロジック コントラクトを乗っ取る可能性があります。
ロジック コントラクトが引き継がれた場合の影響は、システム内のコントラクト コードによって異なります。最悪の場合、攻撃者は UUPS プロキシ モードのロジック コントラクトを悪意のあるコントラクトにアップグレードし、「自己破壊」関数呼び出しを実行する可能性があります。これにより、プロキシ コントラクト全体が役に立たなくなり、コントラクト内の資産が破壊される可能性があります。永久に破壊されて失われます。
場合
① パリティマルチシグフリーズ:ロジックコントラクトが初期化されません。攻撃者は、selfdestruct() を呼び出すことによって、多くのウォレットの初期化をトリガーし、コントラクト内のイーサをロックします。
② Harvest Finance、Teller、KeeperDAO、Rivermen はいずれも初期化されていないロジック コントラクトを使用しているため、攻撃者がコントラクトの初期化パラメータを任意に設定し、delegatecall() 中に selfdestruct() を実行してプロキシ コントラクトを破棄することができます。
ストレージの競合
アップグレード可能なコントラクト システムでは、プロキシ コントラクトは状態変数を宣言しませんが、疑似ランダム ストレージ スロットを使用して重要なデータを保存します。
プロキシ コントラクトは、宣言された場所に相対的な論理コントラクト状態変数の値を保存します。プロキシ コントラクトが独自の状態変数を宣言し、プロキシ コントラクトとロジック コントラクトの両方が同じストレージ スロットを使用しようとすると、ストレージの競合が発生します。
OpenZeppelin ライブラリによって提供されるプロキシ コントラクトは、コントラクト内で状態変数を宣言しませんが、EIP 1967 標準に基づいて、競合を防ぐために、保存する必要がある値 (管理アドレスなど) を特定のストレージ スロットに保存します。
場合
北京時間の2022年7月23日、分散型音楽プラットフォームAudiusがハッキングされ、プロキシ契約に新たなロジックが導入され、ストレージの競合が発生したことが原因であった。
プロキシ コントラクトは proxyAdmin アドレス状態変数を宣言しており、ロジック コントラクト コードが実行されると、その値が誤って読み取られます。
プロジェクト当事者によってカスタマイズされた proxyAdmin の値が、initialized およびInitialized の値と誤ってみなされたため、initializer 修飾子が間違った結果を返し、これにより攻撃者は、initialize() 関数を再度呼び出し、自分自身に initialize() 関数を管理する権限を与えることができました。契約。その後、攻撃者は Audius 資産を盗むために、投票パラメータを変更し、悪意のある提案を通過させました。
ロジック コントラクトまたは信頼できないコントラクトで delegatecall() を呼び出す
delegatecall() が論理コントラクトに存在し、そのコントラクトが呼び出しのターゲットを適切に検証しないとします。この場合、攻撃者はこの関数を悪用して、管理する悪意のあるコントラクトへの呼び出しを実行したり、ロジック実装を破壊したり、カスタム ロジックを実行したりする可能性があります。
同様に、ロジック コントラクトに無制限の address.call() 関数がある場合、攻撃者が悪意を持ってアドレス フィールドとデータ フィールドを提供すると、それがプロキシ コントラクトとして使用される可能性があります。
場合
Pickle Finance、Furucombo、dYdX の攻撃。
これらのインシデントでは、脆弱なコントラクトがユーザー トークンによって承認されており、コントラクトのアドレスとデータを呼び出すためにユーザーが提供した call()/delegatecall() がコントラクト内に存在し、攻撃者はtransferFrom() 関数は、ユーザー残高を引き出すためのコントラクトです。 dYdX 事件の際、dYdX は資金を保護するために独自のホワイトハット攻撃を実行しました。
ベストプラクティス
一般的
(1) 必要な場合にのみプロキシ モードを使用する
すべての契約をアップグレードする必要があるわけではありません。上で示したように、プロキシ パターンの使用には多くのリスクが伴います。プロキシ管理者はコミュニティの同意なしに契約をアップグレードできるため、「アップグレード可能」プロパティは信頼性の問題も引き起こします。必要な場合にのみ、プロキシ パターンをプロジェクトに統合することをお勧めします。
(2) プロキシライブラリは改変しないでください。
プロキシ コントラクト ライブラリは複雑で、特にストレージ管理とアップグレード メカニズムを扱う部分が複雑です。変更にエラーがあると、プロキシ コントラクトとロジック コントラクトの動作に影響します。監査中に発見された多数の重大度の高いエージェント関連のバグは、エージェント ライブラリの不適切な変更が原因でした。オーディウス事件は、代理店契約の不適切な変更がもたらす結果を示す代表的な例です。
代理店契約運営・管理のポイント
(1) ロジックコントラクトの初期化
攻撃者は初期化されていないロジック コントラクトを乗っ取り、プロキシ コントラクト システムを侵害する可能性があります。そのため、デプロイ後にロジック コントラクトを初期化するか、ロジック コントラクトのコンストラクターで _disableInitializers() を使用して初期化を自動的に無効にしてください。
(2) エージェント管理アカウントのセキュリティを確保する
アップグレード可能な契約システムでは通常、契約のアップグレードを管理するために「プロキシ管理者」という特権ロールが必要です。管理キーが漏洩すると、攻撃者は契約を悪意のある契約に自由にアップグレードでき、ユーザーの資産を盗むことができます。ハッキングの潜在的なリスクを回避するために、プロキシ管理者アカウントの秘密キーを慎重に管理することをお勧めします。マルチシグネチャウォレットを使用すると、単一ポイントの鍵管理の失敗を防ぐことができます。
(3) 透過的なプロキシ管理のために別のアカウントを使用する
プロキシ管理とロジック ガバナンスは、ロジック実装との対話の損失を防ぐために別個のアドレスである必要があります。プロキシ管理と論理ガバナンスが同じアドレスを参照する場合、特権機能を実行する呼び出しは転送されないため、ガバナンス機能の変更が禁止されます。
プロキシ契約ストレージ関連
(1) プロキシコントラクトで状態変数を宣言する場合は注意してください
Audius ハックで説明されているように、プロキシ コントラクトは独自の状態変数を宣言するときに注意する必要があります。プロキシ コントラクトで通常の方法で宣言された状態変数は、データの読み取りおよび書き込み時にデータの競合を引き起こす可能性があります。プロキシ コントラクトに状態変数が必要な場合は、ロジック コントラクト コードの実行時の競合を防ぐために、値を EIP1967 などのストレージ スロットに保存します。
(2) 変数の宣言順序とロジックコントラクトの型を維持する
ロジック コントラクトの各バージョンは、状態変数の同じ順序とタイプを維持する必要があり、新しい状態変数は既存の変数の最後に追加する必要があります。そうしないと、デリゲート呼び出しによってプロキシ コントラクトによって、保存されている不正な値が読み取られたり、上書きされたりする可能性があり、古いデータが新しく宣言された変数に関連付けられる可能性があり、アプリケーションに深刻な問題を引き起こす可能性があります。
(3) 基本契約にストレージギャップを含める
ロジック コントラクトでは、新しいロジック実装をデプロイするときに新しい状態変数を予測するために、コントラクト コードにストレージ ギャップを含める必要があります。新しい状態変数を追加した後、ギャップのサイズを適切に更新する必要があります。
(4) コンストラクタや宣言処理では状態変数の値を設定しないでください。
宣言中またはコンストラクター内で状態変数を割り当てると、ロジック コントラクトの値にのみ影響し、プロキシ コントラクトには影響しません。不変でないパラメータは、initialize() 関数を使用して割り当てる必要があります。
契約の継承
(1) アップグレード可能なコントラクトは、他のアップグレード可能なコントラクトからのみ継承できます。
アップグレード可能な契約は、アップグレード不可能な契約とは構造が異なります。たとえば、コンストラクターはエージェント状態の変更と互換性がなく、initialize() 関数を使用して状態変数を設定します。
別のコントラクトを継承するコントラクトは、継承されたコントラクトの initialize() 関数を使用して、それぞれの変数を割り当てる必要があります。 OpenZeppelin ライブラリを使用する場合、または独自のコードを作成する場合は、アップグレード可能なコントラクトが他のアップグレード可能なコントラクトのみを継承できることを確認してください。
(2) ロジック コントラクト内で新しいコントラクトをインスタンス化しないでください。
Solidity を通じて作成およびインスタンス化されたコントラクトはアップグレードできません。コントラクトは個別にデプロイし、そのアドレスをパラメータとしてアップグレード可能なロジック コントラクトに渡して、アップグレード可能な状態を実現する必要があります。
(3) 親契約の初期化リスク
親コントラクトを初期化するとき、__{ContractName}_init 関数はその親コントラクトを初期化します。 __{ContractName}_init を複数回呼び出すと、親コントラクトの 2 回目の初期化が発生する可能性があります。 __{ContractName}_init_unchained() は {ContractName} のパラメータを初期化するだけであり、その親コントラクトの初期化子は呼び出されないことに注意してください。
ただし、これは推奨されません。すべての親コントラクトを初期化する必要があり、必要なコントラクトを初期化しないと、将来の実行に問題が発生するためです。
ロジックコントラクトの実装
信頼できないコントラクトへの selfdestruct() または selegatecall()/call() を回避します
コントラクトに selfdestruct() または delegatecall() が含まれている場合、攻撃者がこれらの関数を使用してロジックの実装を破壊したり、カスタム ロジックを実行したりする可能性があります。開発者はユーザー入力を検証し、コントラクトによるデリゲートコール/信頼できないコントラクトへの呼び出しの実行を許可しないようにする必要があります。また、複数のコントラクトのデリゲート チェーンでストレージ レイアウトを管理するのが面倒になるため、ロジック コントラクトで delegatecall() を使用することはお勧めできません。
最後に書きました
プロキシ コントラクトは、展開後にプロトコルがコード ロジックを更新できるようにすることで、ブロックチェーンの不変の性質をバイパスします。ただし、プロキシ コントラクトの開発には依然として細心の注意が必要であり、不適切な実装はプロジェクトのセキュリティとロジックの問題を引き起こす可能性があります。
全体として、ベスト プラクティスは、トランスペアレント、UUPS、およびビーコン プロキシ モードのそれぞれが、それぞれのユースケースに合わせて実証済みのアップグレード メカニズムを備えているため、信頼できる広範にテストされたソリューションを使用することです。これに加えて、攻撃者がエージェントのロジックを変更できないように、エスカレーションするエージェントの特権ロールも安全に管理する必要があります。
ロジック実装コントラクトでは、攻撃者による selfdestruct() などの悪意のあるコードの実行を防ぐことができる delegatecall() を使用しないように注意する必要もあります。
ベスト プラクティスに従うことで、アップグレード可能な柔軟性を維持しながら安定したプロキシ コントラクトのデプロイメントが保証されますが、すべてのコードはプロジェクトを危険にさらす可能性のある新たなセキュリティまたはロジックの問題が発生する傾向があります。したがって、すべてのコードは、プロキシ契約プロトコルの監査とセキュリティ保護の経験を持つセキュリティ専門家のチームによって監査されるのが最適です。