はじめに
初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
代表的なゲームはクリプトスペルズというブロックチェーンゲームです。
今回は、プロキシコントラクト内にアップグレードロジックを実装している、アップグレーダブルなコントラクトの仕組みを提案しているERC1822についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
他にも様々なEIPについてまとめています。
概要
この標準プロキシコントラクトは、すべてのロジックコントラクトと完全に互換性があるように設計されています。
プロキシとロジックコントラクトの間に互換性の問題が生じないように、プロキシコントラクト内の特定のストレージ内にロジックコントラクトのアドレスを保存します。
この方法により、アップグレードの時には互換性チェックが行われ、アップグレードの成功が保証されます。
また、アップグレードは無制限に行うことが可能であり、特定のカスタムロジックによって制限することもできます。
さらに、バイトコードの検証に影響を与えずにデプロイ時に複数のコンストラクタから実行する処理を選択することができます。
アップグレーダブルなコントラクトでは、「プロキシコントラクト」(以降「Proxy」)と「インプリメンテーションコントラクト(ロジック)」(以降「implementation」)の2つに分かれています。
implementationはコントラクトのロジック(実装)を管理し、Proxyはimplementationに実装されている機能の呼び出しとデータの保持を担当します。
これにより、「データ」をProxyが管理し、「実装」をimplementationが管理できるため、実装をアップグレードしたい時は新しいimplementationをデプロイし、Proxyから呼び出すimplementationを新しいコントラクトにすることができ、これによりコントラクトのアップグレードが可能になります。
より詳しくは以下の記事を参考にしてください。
動機
この標準プロキシコントラクトは、既存のプロキシ実装を改良し、プロキシコントラクトとロジックコントラクトのデプロイとメンテナンスにおける開発者体験を向上させることを目的としています。
また、プロキシコントラクトで使用されるバイトコードの検証方法の標準化と改善もしています。
用語
delegatecall()
ユーザーがコントラクトAに特定の関数の実行をリクエストし、コントラクトAに該当の関数がないときに実行されるfallback()
関数が実行されます。
このfallback()
関数ではあらかじめ、コントラクトBを呼び出すように設定されており、コントラクトB内の該当の関数を実行します。
この時、データはコントラクトAに保存されているものが使用されるような特殊な実行方法です。
プロキシ(Proxy)コントラクト
データを保存するコントラクトで、delegatecall()
を通じて外部コントラクトBのロジックを使用します。
ロジック(Logic)コントラクト
実装したいコントラクトのロジックを管理するコントラクトです。
Proxiableコントラクト
ロジックコントラクトBに継承されてアップグレード機能を提供します。
仕様
この規格で提案されているProxyコントラクトは、そのままデプロイされて既存コントラクトのライフサイクル管理に使用されます。
さらに、このプロキシコントラクトに加えて、アップグレードのパターンを確立するための「Proxiableコントラクト」インターフェース/ベースを提案しています。
このインターフェースは、既存のロジックに干渉することなくコントラクトのアップグレードを可能にします。
また、アップグレードを可能にするロジックは柔軟に実装することができます。
Proxyコントラクト
関数
fallback
実行したい関数をコントラクトにリクエストしたとき、該当の関数がない時に実行される関数。
ロジックコントラクトのアドレスは、keccak256("PROXIABLE")
で計算された特定のストレージ位置に格納されています。
これにより、プロキシコントラクトとロジックコントラクトの変数が衝突されなくなり、全てのロジックコントラクトとの互換性が保たれます。
fallback
関数はアセンブリコードで記述されています。
アセンブリコードについては以下の記事を参考にしてください。
function() external payable {
assembly { // solium-disable-line
// ロジックコントラクトのアドレスをロード
let contractLogic := sload(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7)
// コールデータをメモリの0x0番地にコピー
calldatacopy(0x0, 0x0, calldatasize)
// delegatecallを実行
let success := delegatecall(sub(gas, 10000), contractLogic, 0x0, calldatasize, 0, 0)
// リターンデータサイズを取得
let retSz := returndatasize
// リターンデータをメモリの0番地にコピー
returndatacopy(0, 0, retSz)
// 成功した場合はリターン、失敗した場合はリバート
switch success
case 0 {
revert(0, retSz)
}
default {
return(0, retSz)
}
}
}
-
sload(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7)
- 事前に定義されたストレージ位置からロジックコントラクトのアドレスをロードします。
-
calldatacopy(0x0, 0x0, calldatasize)
- 受け取ったコールデータをメモリの先頭にコピーします。
-
delegatecall(sub(gas, 10000), contractLogic, 0x0, calldatasize, 0, 0)
- ロジックコントラクトに対して
delegatecall
を行います。 - 現在のコントラクトのコンテキストを保持したまま、ロジックコントラクトの関数を実行します。
-
sub(gas, 10000)
は、呼び出しに使用するガス量を指定しています。- ここでは、多少のガスマージンを残しています。
- ロジックコントラクトに対して
-
returndatasize
とreturndatacopy
- ロジックコントラクトから返されたデータのサイズを取得し、それをメモリにコピーします。
-
switch success
-
delegatecall
が成功したかどうかを確認します。 - 成功した場合は
return
、失敗した場合はrevert
でエラーメッセージを返します。
-
constructor
プロキシコントラクトのconstructor
関数は、複数のロジックコントラクト内のconstructor
関数から選択して実行でき、任意の数や型の引数を受け取れます。
ロジックコントラクトに複数のconstructor
が含まれている場合、初期化後に再度constructor
が呼び出され内容することが重要です。
さらに、複数のconstructor
をサポートする追加機能があっても、プロキシコントラクトのバイトコードの検証に影響を及びしません。
理由としては、初期化(デプロイ)トランザクションのコールデータ(入力)は、最初にプロキシコントラクトのABIを使用してデコードしてからロジックコントラクトのABIを使用してデコードするためです。
誤って片方のコントラクトの実装が影響することがないということです。
contract Proxy {
// ストレージ内のコード位置は keccak256("PROXIABLE") = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7"
// コンストラクタは任意の型の引数を受け入れる
constructor(bytes memory constructData, address contractLogic) public {
// ロジックコントラクトのアドレスを保存
assembly { // solium-disable-line
sstore(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7, contractLogic)
}
// ロジックコントラクトのコンストラクタを呼び出す
(bool success, bytes memory _ ) = contractLogic.delegatecall(constructData); // solium-disable-line
require(success, "Construction failed");
}
// フォールバック関数
function() external payable {
assembly { // solium-disable-line
let contractLogic := sload(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7)
calldatacopy(0x0, 0x0, calldatasize)
let success := delegatecall(sub(gas, 10000), contractLogic, 0x0, calldatasize, 0, 0)
let retSz := returndatasize
returndatacopy(0, 0, retSz)
switch success
case 0 {
revert(0, retSz)
}
default {
return(0, retSz)
}
}
}
}
-
`constructor
-
constructor(bytes memory constructData, address contractLogic)
- コンストラクタは任意の型の引数を受け入れ、
constructData
とcontractLogic
を引数として受け取ります。
- コンストラクタは任意の型の引数を受け入れ、
-
assembly { sstore(...) }
- ロジックコントラクトのアドレスを特定のストレージ位置に保存します。
-
contractLogic.delegatecall(constructData)
- ロジックコントラクトのコンストラクタを呼び出します。
- これにより、任意の初期化データを使用してロジックコントラクトを初期化できます。
-
require(success, "Construction failed")
- 初期化が成功しなかった場合、エラーメッセージを返します。
-
-
fallback
- 受け取った全ての呼び出しをロジックコントラクトに委任します。
Proxiableコントラクト
ロジックコントラクトに含まれており、アップグレードを実行するために必要な機能を提供します。
互換性チェックを行うproxiable
関数は、アップグレード中に不正な更新などが行われるのを防ぎます。
Warning: updateCodeAddress and proxiable must be present in the Logic Contract. Failure to include these may prevent upgrades, and could allow the Proxy Contract to become entirely unusable. See below Restricting dangerous functions
警告: ロジックコントラクトには必ず
updateCodeAddress
関数とproxiable
関数を含めてください。これらが含まれていないと、アップグレードができなくなり、プロキシコントラクトが完全に使用不能になる可能性があります。詳細は、危険な関数の制限に関するセクションを参照してください。
proxiable
新しいロジックコントラクトがユニバーサルアップグレーダブルプロキシ標準(UUPS)に準拠していることを確認するための互換性チェックを行います。
この関数は、プロキシコントラクトが新しいロジックコントラクトに正しくアップグレードされることを保証するために重要です。
また、この関数では新しいロジックコントラクトがkeccak256("PROXIABLE")
のような特定の値と一致するかどうかをチェックします。
この互換性チェックは、将来的な実装変更をサポートするために柔軟に変更することができます。
例えば、keccak256("PROXIABLE-ERC1822-v1")
のように、バージョン情報を含めた比較に変更することも可能です。
これにより、プロキシコントラクトは常に最新の標準に対応できるようになります。
updateCodeAddress
プロキシコントラクト内の特定のストレージ位置に新しいロジックコントラクトのアドレスを保存する関数です。
例)keccak256("PROXIABLE")
上記の場合は以下に保存されます。
0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7
これにより、プロキシコントラクトが新しいロジックコントラクトのコードを使用するように更新されます。
また、proxiableUUID
関数は、ロジックコントラクトが特定の標準に準拠しているかどうかを確認するために使用されます。
重要なポイント
contract Proxiable {
// ストレージ位置の定義 (keccak256("PROXIABLE") = 0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7)
function updateCodeAddress(address newAddress) internal {
// 新しいアドレスのロジックコントラクトが互換性を持つか確認
require(
bytes32(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7) == Proxiable(newAddress).proxiableUUID(),
"Not compatible"
);
// 新しいアドレスをストレージに保存
assembly { // solium-disable-line
sstore(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7, newAddress)
}
}
// UUIDを返す関数
function proxiableUUID() public pure returns (bytes32) {
return 0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7;
}
}
-
**
updateCodeAddress
- 新しいロジックコントラクトのアドレスをプロキシコントラクトに保存します。
- まず、新しいロジックコントラクトのアドレスが正しい
proxiableUUID
を返すかを確認します。 - このチェックにより、新しいロジックコントラクトが互換性を持つことが保証されます。
- 次に、アセンブリコードを使用して、新しいロジックコントラクトのアドレスをストレージに保存します。
-
**
proxiableUUID
- ロジックコントラクトが特定の標準に準拠していることを示す固定のUUID値を返します。
- これにより、互換性チェックが可能になります。
プロキシ使用時の注意点
プロキシコントラクトを使用する時の注意点として、以下のベストプラクティスが推奨されます。
変数とロジックの分離
新しいロジックコントラクトを設計する時には、既存のプロキシコントラクトのストレージとの互換性を維持する必要があります。
具体的には、変数の初期化順序を変更せず、以前のロジックコントラクトからのすべての変数の後に新しい変数を追加するようにします。
このため、全ての変数を保持する単一の「ベース」コントラクトを利用し、新しいロジックコントラクトで継承することが推奨されます。
この方法により、変数の順序を誤って変更したり、ストレージを上書きしたりするリスクが大幅に減少します。
継承については以下のようなイメージです。
ロジックコントラクトA
→ ロジックコントラクトB(コントラクトAを継承)
→ ロジックコントラクトC(コントラクトBを継承)
危険な関数の制限
Proxiableブルコントラクトの互換性チェックは、標準を実装していないロジックコントラクトへのアップグレードを防ぎますが、ロジックコントラクト自体が不正な変更を実行してしまう可能性は残ります。
そのため、コントラクトの実装に大きく影響を与える関数の権限をonlyOwner
に制限し、デプロイ後すぐにロジックコントラクトの所有権を無効なアドレス(例:address(1)
)に移譲することが推奨されます。
これにより、コントラクトに損害を与える関数(SELFDESTRUCT
、CALLCODE
、delegatecall()
など)による不正な変更を防ぐことができます。
実装例
Owned
updateCodeAddress
の実行権限をowner
にのみ絞っています。
contract Owned is Proxiable {
// ensures no one can manipulate this contract once it is deployed
address public owner = address(1);
function constructor1() public{
// ensures this can be called only once per *proxy* contract deployed
require(owner == address(0));
owner = msg.sender;
}
function updateCode(address newCode) onlyOwner public {
updateCodeAddress(newCode);
}
modifier onlyOwner() {
require(msg.sender == owner, "Only owner is allowed to perform this action");
_;
}
}
ERC20
Proxyコントラクト
pragma solidity ^0.5.1;
contract Proxy {
// Code position in storage is keccak256("PROXIABLE") = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7"
constructor(bytes memory constructData, address contractLogic) public {
// save the code address
assembly { // solium-disable-line
sstore(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7, contractLogic)
}
(bool success, bytes memory _ ) = contractLogic.delegatecall(constructData); // solium-disable-line
require(success, "Construction failed");
}
function() external payable {
assembly { // solium-disable-line
let contractLogic := sload(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7)
calldatacopy(0x0, 0x0, calldatasize)
let success := delegatecall(sub(gas, 10000), contractLogic, 0x0, calldatasize, 0, 0)
let retSz := returndatasize
returndatacopy(0, 0, retSz)
switch success
case 0 {
revert(0, retSz)
}
default {
return(0, retSz)
}
}
}
}
Logicコントラクト
contract Proxiable {
// Code position in storage is keccak256("PROXIABLE") = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7"
function updateCodeAddress(address newAddress) internal {
require(
bytes32(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7) == Proxiable(newAddress).proxiableUUID(),
"Not compatible"
);
assembly { // solium-disable-line
sstore(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7, newAddress)
}
}
function proxiableUUID() public pure returns (bytes32) {
return 0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7;
}
}
contract Owned {
address owner;
function setOwner(address _owner) internal {
owner = _owner;
}
modifier onlyOwner() {
require(msg.sender == owner, "Only owner is allowed to perform this action");
_;
}
}
contract LibraryLockDataLayout {
bool public initialized = false;
}
contract LibraryLock is LibraryLockDataLayout {
// Ensures no one can manipulate the Logic Contract once it is deployed.
// PARITY WALLET HACK PREVENTION
modifier delegatedOnly() {
require(initialized == true, "The library is locked. No direct 'call' is allowed");
_;
}
function initialize() internal {
initialized = true;
}
}
contract ERC20DataLayout is LibraryLockDataLayout {
uint256 public totalSupply;
mapping(address=>uint256) public tokens;
}
contract ERC20 {
// ...
function transfer(address to, uint256 amount) public {
require(tokens[msg.sender] >= amount, "Not enough funds for transfer");
tokens[to] += amount;
tokens[msg.sender] -= amount;
}
}
contract MyToken is ERC20DataLayout, ERC20, Owned, Proxiable, LibraryLock {
function constructor1(uint256 _initialSupply) public {
totalSupply = _initialSupply;
tokens[msg.sender] = _initialSupply;
initialize();
setOwner(msg.sender);
}
function updateCode(address newCode) public onlyOwner delegatedOnly {
updateCodeAddress(newCode);
}
function transfer(address to, uint256 amount) public delegatedOnly {
ERC20.transfer(to, amount);
}
}
引用
Gabriel Barros gabriel@terminal.co, Patrick Gallagher blockchainbuddha@gmail.com, "ERC-1822: Universal Upgradeable Proxy Standard (UUPS) [DRAFT]," Ethereum Improvement Proposals, no. 1822, March 2019. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1822.
最後に
今回は「プロキシコントラクト内にアップグレードロジックを実装している、アップグレーダブルなコントラクトの仕組みを提案しているERC1822」についてまとめてきました!
いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!