ロジックとストレージの分離#
私たちは皆、スマートコントラクトがブロックチェーンにデプロイされた後、コントラクトのコードは改ざんできないことを知っています。したがって、バグが発生した場合、プロジェクト側は多くの場合手の打ちようがありません。
コントラクトは通常、ストレージ変数とロジック関数で構成されています。変数を新しいコントラクトに移行するコストが非常に高く、主にアップグレードされるのはロジック関数です。そのため、ストレージ変数とロジック関数をコントラクトレベルで分離することができます。
したがって、完全な呼び出しチェーンは次の図のようになります:
- ユーザーとコントラクト A のインタラクション、A は関数ロジックを記録せず、変数のみを保存します
- コントラクト A は delegatecall を使用してコントラクト B を呼び出し、B の関数ロジックをコントラクト A に適用します
- 呼び出しが終了すると、ユーザーの視点ではコントラクト A のみが存在し、コントラクト B の存在は感知されません
基本的なアップグレード可能なコントラクトの実装#
上記の呼び出しチェーンを参照して、コントラクト B を他のコントラクトに置き換えることで、ロジックコントラクトの置き換えを行い、元のコントラクトのストレージに影響を与えずに行うことができます。
プロキシコントラクト#
- implementation:ロジックコントラクトのアドレスを記録します
- fallback 関数を使用して、関数呼び出しをすべて delegatecall を介してロジックコントラクトに転送します
- アップグレード関数を提供し、ロジックコントラクトのアドレスを変更します
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
// シンプルなアップグレード可能なコントラクト、管理者はアップグレード関数を使用してロジックコントラクトのアドレスを変更し、コントラクトのロジックを変更することができます。
contract SimpleUpgrade {
address public implementation; // ロジックコントラクトのアドレス
address public admin; // 管理者アドレス
string public words; // 文字列、ロジックコントラクトの関数を介して変更できます
// コンストラクタ、管理者とロジックコントラクトのアドレスを初期化します
constructor(address _implementation){
admin = msg.sender;
implementation = _implementation;
}
// fallback関数、呼び出しをロジックコントラクトに委譲します
fallback() external payable {
(bool success, bytes memory data) = implementation.delegatecall(msg.data);
}
// アップグレード関数、ロジックコントラクトのアドレスを変更します。管理者のみが呼び出せます
function upgrade(address newImplementation) external {
require(msg.sender == admin);
implementation = newImplementation;
}
}
ロジックコントラクト#
- プロキシコントラクトと同じストレージレイアウト(変数の型、変数の順序)を維持する必要があります
- ロジックコントラクトの関数はプロキシコントラクトに変数の変更を反映します。したがって、
foo()
関数を呼び出すと、プロキシのwords
変数がold
に変更されます
contract Logic1 {
// ステート変数はプロキシコントラクトと同じでなければなりません(変数の型、変数の順序)
address public implementation;
address public admin;
string public words; // 文字列、ロジックコントラクトの関数を介して変更できます
// プロキシのステート変数を変更します、セレクタ:0xc2985578
function foo() public{
words = "old";
}
}
新しいロジックコントラクト:
- 同じストレージレイアウトを維持します
- 関数ロジックを調整してロジックの更新を完了します。
foo()
関数を呼び出すと、プロキシのwords
変数がnew
に変更されます
contract Logic2 {
// ステート変数はプロキシコントラクトと同じでなければなりません(変数の型、変数の順序)
address public implementation;
address public admin;
string public words; // 文字列、ロジックコントラクトの関数を介して変更できます
// プロキシのステート変数を変更します、セレクタ:0xc2985578
function foo() public{
words = "new";
}
}
コントラクトのアップグレード#
管理者アカウントはプロキシコントラクトのupgrade
関数を呼び出し、新しいロジックコントラクトのアドレスを渡すことでアップグレードを完了させることができます
問題点#
fallback 内での delegatecall 呼び出しでは、セレクタの競合が発生する可能性があります
関数のセレクタは、関数シグネチャのハッシュの最初の 4 バイトです。異なる関数には競合の可能性があるため、コントラクトに 2 つのセレクタの競合する関数が存在する場合、コントラクトはコンパイルできません。ただし、プロキシコントラクトとロジックコントラクトの間に競合が存在する可能性があり、コンパイル時に検出できない場合があります。
透明なプロキシ#
透明なプロキシは、セレクタの競合によるアップグレードの問題を解決するために存在します。透明なプロキシのアイデアは、アップグレード関数の権限と fallback での delegatecall の実行権限を分離することです。
- fallback 関数は admin 以外の呼び出しを要求します
- upgrade 関数は admin の呼び出しを要求します
これにより、関数セレクタの競合によるアップグレードの問題を回避できます。
fallback() external payable {
require(msg.sender != admin);
(bool success, bytes memory data) = implementation.delegatecall(msg.data);
}
// アップグレード関数、ロジックコントラクトのアドレスを変更します。管理者のみが呼び出せます
function upgrade(address newImplementation) external {
if (msg.sender != admin) revert();
implementation = newImplementation;
}
一般的なアップグレード可能なプロキシ UUPS#
透明なプロキシのモデルでは、管理者アドレスの検証に毎回ガスが必要です。
UUPS モデルでは、アップグレード関数をロジックコントラクト内に配置し、単一のコントラクト内でアップグレード関数のセレクタの競合問題をコンパイル時に回避できます。
以前のアップグレード関数はプロキシコントラクトにあり、admin 権限に制限されていましたが、今回はロジックコントラクトに移動します。delegatecall のプロセスでは msg.sender は変わらず、保存されている admin はプロキシコントラクトに記録されているため、この検証ロジックをロジックコントラクトに配置しても問題ありません。
UUPS アップグレード可能なコントラクトの例は次のとおりです。ロジックコントラクトに implementation アドレスを記録し、プロキシコントラクトに適用します。
プロキシコントラクト#
ストレージ変数、コンストラクタ、および fallback のみが含まれており、アップグレード関数は削除されています
contract UUPSProxy {
address public implementation; // ロジックコントラクトのアドレス
address public admin; // 管理者アドレス
string public words; // 文字列、ロジックコントラクトの関数を介して変更できます
// コンストラクタ、管理者とロジックコントラクトのアドレスを初期化します
constructor(address _implementation){
admin = msg.sender;
implementation = _implementation;
}
// fallback関数、呼び出しをロジックコントラクトに委譲します
fallback() external payable {
(bool success, bytes memory data) = implementation.delegatecall(msg.data);
}
}
ロジックコントラクト#
アップグレード関数が追加されています
// UUPSロジックコントラクト(アップグレード関数はロジックコントラクト内に記述されます)
contract UUPS1{
// ステート変数はプロキシコントラクトと同じでなければなりません(変数の型、変数の順序)
address public implementation;
address public admin;
string public words; // 文字列、ロジックコントラクトの関数を介して変更できます
// プロキシのステート変数を変更します、セレクタ:0xc2985578
function foo() public{
words = "old";
}
// アップグレード関数、ロジックコントラクトのアドレスを変更します。管理者のみが呼び出せます。セレクタ:0x0900f010
// UUPSでは、ロジック関数にアップグレード関数を含める必要があります。そうしないと、アップグレードができなくなります。
function upgrade(address newImplementation) external {
require(msg.sender == admin);
implementation = newImplementation;
}
}
// 新しいUUPSロジックコントラクト
contract UUPS2{
// ステート変数はプロキシコントラクトと同じでなければなりません(変数の型、変数の順序)
address public implementation;
address public admin;
string public words; // 文字列、ロジックコントラクトの関数を介して変更できます
// プロキシのステート変数を変更します、セレクタ:0xc2985578
function foo() public{
words = "new";
}
// アップグレード関数、ロジックコントラクトのアドレスを変更します。管理者のみが呼び出せます。セレクタ:0x0900f010
// UUPSでは、ロジック関数にアップグレード関数を含める必要があります。そうしないと、アップグレードができなくなります。
function upgrade(address newImplementation) external {
require(msg.sender == admin);
implementation = newImplementation;
}
}