ロジックとストレージの分離#
私たちは皆、スマートコントラクトがブロックチェーンにデプロイされた後、コントラクトのコードを改ざんすることはできないことを知っています。したがって、コントラクトにバグがある場合、プロジェクトの開発者は多くの場合手を打つことができません。
コントラクトは通常、ストレージ変数とロジック関数で構成されています。変数を新しいコントラクトに移行するのは非常にコストがかかりますし、主にアップグレードされるのはロジック関数ですので、ストレージ変数とロジック関数をコントラクトレベルで分離することができます。
したがって、完全な呼び出しチェーンは次の図のようになります:
- ユーザーは contract-A と対話し、A は関数のロジックを記録せずに変数のみを保存します。
- contract-A は delegatecall を使用して contract-B を呼び出し、B の関数のロジックを contract-A に適用します。
- 呼び出しが終了すると、ユーザーの視点では contract-A のみが表示され、contract-B の存在は感知されません。
基本的なアップグレード可能なコントラクトの実装#
上記の呼び出しチェーンを参考にして、contract-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 アップグレードコントラクトの例:
プロキシコントラクト#
変数、コンストラクタ、および 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;
}
}