概要
ブロックチェーンの生態系は、最近、新しいプロトコルの爆発を見せています。これらのプロトコルは、伝統的な価値移転から分散型ファイルストレージまで、あらゆるものを提供することを約束します。ブロックチェーン業界は、従来のインターネットインフラストラクチャの多くを再設計して再構築できるので、これはエキサイティングな時間です。分散環境で複雑なアプリケーションをオンラインで展開するために、迅速かつ簡単に、安全に行う機会があります。
しかし、業界はセキュリティ違反に悩まされており、ブロックチェイン技術の可能性は驚異的ですが、この技術には慎重にアプローチする必要があることがますます明らかになっています。同時に、この技術を可能な限り利用しやすくし、イノベーションを促進し、分散型のオープンな経済への移行を加速したいと考えています。
これらの問題と機会に対処するために、我々は、分散アプリケーションのためのブロックチェーン・オペレーティング・システムであるZeppelin OSを提案します。 Zeppelin OSを使用すると、開発者は既存のプロトコルを使用して組み合わせるセキュアなアプリケーションを簡単に構築できます。同時に、Zeppelinの永続的開発のインセンティブを生み出します。
Zeppelin OSは、カーネル、マーケットプレイス、SDKの3つのコアコンポーネントで構成されています。また、Zeppelinの生態系に燃料を供給するためのZEPトークンを提案します。トークンは、プロトコル市場でサービスを消費し、提供するために使用されます。また、OpenZeppelinほど頻繁に更新されるカーネルのアップグレードのための主要なガバナンスメカニズムとしても機能し、提案の重要な部分になります。
Overview
ZeppelinOS
ZeppelinOSは、Ethereumと他のすべてのEVMおよびeWASM搭載のブロックチェーン上でスマートコントラクトプロジェクトを開発、展開、運用するためのプラットフォームです。
特徴
- アップグレード:パッケージの新しいバージョンをブロックチェーンに展開します。
- EVMパッケージを公開する:ブロックチェーンにパッケージを展開して、他のプロジェクトがそれらを再利用できるようにする。
- EVMパッケージをリンクする:既にブロックチェーンに配置されているEVMパッケージにプロジェクトをリンクします。
- 保証:ZEPトークンを使用して、EVMパッケージの品質を元に戻します。
クイックスタート
最初のプロジェクトの展開
以下の手順でZeppelinOSを使い始めることができます。
インストール
まず、Node.jsとnpmをインストールしてみましょう。 それぞれのWebサイトには、マシンの具体的な手順が記載されています。
次に、実行中のZeppelinOSをインストールします。
npm install --global zos
zos --help
を実行すると、すべてのZeppelinOSコマンドのリストが表示されます。このガイドでは、新しいコマンドを導入するときはいつでも、zos<command-name> --help
を実行して、そのコマンドとその引数に関する詳細情報を取得します。
プロジェクトの設定
プロジェクトのディレクトリを作成してアクセスする必要があります。
mkdir my-project
cd my-project
npmを使用してpackage.jsonファイルを作成します。
npm init
このコマンドはプロジェクトの詳細を尋ねます。 この非常に基本的なガイドでは、enterを押して各フィールドのデフォルト値を受け入れることができます。
これで、ZeppelinOSプロジェクトを初期化することができます
zos init -my-project
このコマンドは、プロジェクトに関するすべての情報を含むzos.jsonファイルを作成します。 このファイル形式の詳細については、構成ファイルのページを参照してください。
コマンドはTruffleも初期化しますので、今はmy-projectディレクトリの中にpackage.jsonファイル(npmで作成)、contractsとmigrateという名前の空の2つのディレクトリ、truffle-config.jsファイル(zos Truffle用)、zos.jsonファイル(ZeppelinOS用にzosで作成)があります。
コントラクトの追加
プロジェクトに追加する例として非常に単純なコントラクトを作成しましょう。 名前をMyContract.solとし、それを以下のソリッディティコードでcontractsフォルダに配置します。
pragma solidity ^0.4.24;
import "zos-lib/contracts/Initializable.sol";
contract MyContract is Initializable {
uint256 public x;
string public s;
function initialize(uint256 _x, string _s) initializer public {
x = _x;
s = _s;
}
}
サンプルのコントラクトには、標準のSolidityコンストラクタの代わりにinitialize関数があることに注意してください。これは、ZeppelinOSでアップグレード可能なコントラクトのコンストラクタ機能を定義する方法です。これについては、次のガイドで詳しく説明します。
しかし、その前にまだ2つのステップが必要です。 このコントラクトは、zos-libパッケージから別のコントラクトをインポートするので、インストールする必要があります。
npm install zos-lib
zos-lib
はZeppelinOSライブラリであり、スマートコントラクトの開発を支援するコントラクトを含んでいます。この場合、初期化子修飾子を使用するためにInitializable.solをインポートしています。これにより、initialize
関数が複数回呼び出されることがなくなります。
次の方法でプロジェクトにコントラクトを追加できます
zos add MyContract
このコマンドは、まずMyContractをコンパイルし、zos.json設定ファイルにプロジェクトを追加します。
注:現在のバージョンのZeppelinOSはSolidity 0.4.24を使用しています。
別のバージョンを使用する場合は、-skip-compileオプションを使用して独自の
ビルドファイルを用意する必要があります。
プロジェクトの展開
これでプロジェクトの初期展開を行う準備が整いました。 ブロックチェーンネットワークがありませんが、この例では、スマートコントラクトを開発するために使用できるEthereum開発用の個人用ブロックチェーンであるganacheを使用します。
npm install -g ganache-cli
作業を開始するには、別のターミナルを開いて以下を実行してください
ganache-cli --port 9545 --deterministic
これを終えたら、元のターミナルに戻って次のコマンドを実行してみましょう
zos session --network local --from<<送信者アドレス>> --expires 3600
session
コマンドは、目的のネットワークで動作するセッションを開始します。
この場合、--network
オプションを指定してローカルネットワークと連携し、--fromオ
プションを指定して実行するトランザクションのデフォルトの送信者アドレスを設定するように指示しています。 さらに、expires
フラグを使用すると、セッションの有効期限を秒単位で指定できます。
注:ganache-cliが使用するデフォルトのアドレスとは異なる--fromオプションに特定のアドレスを
使用していることに注意してください。これは、アップグレード可能なコントラクトを作成して
照会するために、異なるアドレスを使用する必要があるためです。
すべての設定が完了したら、プロジェクトを展開する準備が整いました。 そのためには、次を実行します。
zos push
このコマンドは、MyContractを指定されたネットワークにデプロイし、そのアドレスを出力します。プロジェクトが(addコマンドを使って)他のコントラクトを追加した場合、それらも展開されます。
また、pushコマンドは、contracts["MyContract"].addressで展開されたコントラクト実装のアドレスと、この特定のネットワーク内のプロジェクトに関するすべての情報を含むzos.dev-<network_id>.jsonファイルを作成します。このファイル形式の詳細については、設定ファイルのセクションを参照してください。
以前のzos.dev-<network_id>.jsonがすでに作成されている場合、pushコマンドは最後の変更でファイルを更新します。たとえば、MyContractのソースを変更してzosを再度呼び出すと、ZeppelinOSはMyContractの新しいバージョンをデプロイし、jsonファイルのcontracts["MyContract"].addressのエントリを置き換えます。
理解しておくべき重要なことは、pushコマンドによってデプロイされたコントラクトが論理コントラクトであり、アップグレード可能なインスタンスで使用されるのではなく、直接使用されることを意図していないことです。
truffle-config.jsファイルのネットワーク名をlocalから置き換えるだけで、同じ手順に従ってメインネットや他のテストネットワークにプロジェクトを展開することができます。 これについては、「メインネットへのデプロイガイド」でさらに説明します。
プロジェクトの更新
ここまでで1つのコントラクトでZeppelinOSプロジェクトを展開しました。それは私以下のコードです
pragma solidity ^0.4.24;
import "zos-lib/contracts/Initializable.sol";
contract MyContract is Initializable {
uint256 public x;
string public s;
function initialize(uint256 _x, string _s) initializer public {
x = _x;
s = _s;
}
}
これは、制限された機能ですべてがブロックチェーン上で永遠に不変のコントラクトです。ZeppelinOSを使用すると、コントラクトのアップグレードを許可することができ、プロジェクトを開発するためのより持続可能なプロセスへの扉を開くことができます。アップグレードでは、ユーザーの常に変化する目標に従って調整できる小さな機能をすばやく追加して、反復リリースを行うことができます。 もちろん、以前の反復で導入したバグの修正プログラムを追加することもできます。また、ブロックチェーン上の他のコントラクトと同様に、コントラクトのアップグレード時期と方法を手動で、自動化、またはユーザーの信頼を得るために組み合わせることができます。
次に、このコントラクトのアップグレード可能なインスタンスを作成して、これが何であるかを試してみましょう。
zos create MyContract --init initialize --args 42,hitchhiker
zos creat
eコマンドは、コントラクトを作成した後に初期化関数を呼び出すためのオプションの--init [関数名]
パラメータを受け取り、--args
パラメータを使用して引数を渡すことができます。 このようにして、x状態変数の値として42を、s状態変数の値としてhitchhikerを使ってコントラクトを初期化しています。
このコマンドはコントラクトのアドレスを表示し、zos.dev-<network_id>.jsonファイルを更新します。
注:多くの変数を持つイニシャライザを呼び出すときは、カンマで区切られたリストとして渡され、
その間にスペースは入れられません。
コントラクトと対話するためのコンソールを起動し、それが適切に初期化されている確認できます。
npx truffle console --network local
Truffleコンソールに入ったら、次の手順を実行して、インスタンスが正常に動作していることをテストします。
注:<your-contract-address>を上記で実行したcreateコマンドで返されたアドレスに置き換えてください。
truffle(local)> myContract = MyContract.at('<your-contract-address>')
truffle(local)> myContract.x()
42
truffle(local)> myContract.s()
"hitchhiker"
Truffleコンソールを終了して、次の手順に進みます。
コントラクトのアップグレード
このガイドでは、ganacheローカル開発ネットワークを使用しています。 前に実行したganache-cliコマンドを停止しないでください。そうしないと、以前の展開が失われます。
さて、私たちのコントラクト上の問題を発見したとしましょう、あるいは単にその機能を拡張したいとしましょう。
contracts/MyContract.solを開き、新しい関数を追加します。
pragma solidity ^0.4.24;
import "zos-lib/contracts/Initializable.sol";
contract MyContract is Initializable {
uint256 public x;
string public s;
function initialize(uint256 _x, string _s) initializer public {
x = _x;
s = _s;
}
function increment() public {
x += 1;
}
}
注:ZeppelinOSは機能性に関する任意の変更をサポートしていますが、コントラクトの以前のバージョン
にあったすべての変数を保存し、既存の変数の下に新しい変数を宣言する必要があります。
アップグレードに関するすべての考慮事項および推奨事項については、
「アップグレード可能なコントラクトの作成」ページで説明しています。
これらの変更を保存したら、新しいコードをネットワークにpushします。
zos push
最後に、すでにデプロイされているコントラクトを新しいコードで更新しましょう。
npx truffle console --network local
Truffleコンソールに入ったら、次の手順を実行して、追加した新機能を試してください。
truffle(local)> myContract = MyContract.at('<your-contract-address>')
truffle(local)> myContract.increment()
truffle(local)> myContract.x()
43
EVMパッケージへのリンク
ZeppelinOSを使用すると、すでにデプロイされているパッケージを、ブロックチェーンにリンクすることができます。 もちろん、開発者はパッケージをアップグレードすることができ、新しいバージョンを指すようにリンクを更新することができるので、最終的にコミュニティのすべての開発を効率的に活用しています。
EVMパッケージを使用するには、最初にZeppelinOSプロジェクトを初期化する必要があります。今回は最初のプロジェクトの導入で行っています。
続行するには、MyLinkedContract.solという新しいコントラクトを作成し、contractsフォルダに配置します。次に、OpenZeppelinパッケージから非常に一般的なコントラクトをインポートしましょう。
pragma solidity ^0.4.24;
import "openzeppelin-eth/contracts/token/ERC721/ERC721Mintable.sol";
contract MyLinkedContract {
ERC721Mintable private _token;
function setToken(ERC721Mintable token) external {
require(token != address(0));
_token = token;
}
}
注目すべきは、openzeppelin-solidity
をインポートする代わりに、openzeppelin-eth
をインポートしていることです。 これはOpenZeppelin EVMパッケージの名前です。すでに展開されているパッケージを再利用する場合に使用する必要があります。openzeppelin-solidity
との違いはアップグレード可能で使用可能なことです。つまり、他のEVMパッケージと同じように、すでにEthereumネットワークに導入されてインストールされているので、コードをダウンロードしてカスタマイズして展開する必要はありません。
さて、openzeppelin-eth
パッケージにプロジェクトをリンクさせましょう
zos link openzeppelin-eth
このコマンドは、openzeppelin-eth
をローカルにインストールします。これは、それをインポートするコントラクトをコンパイルするために必要です。リンクされたパッケージへの参照でzos.jsonファイルを更新し、プロジェクトをデプロイするときにすでにブロックチェーンに存在するEVMパッケージを使用するようにします。
以前と同じように、プロジェクトにコントラクトを追加する必要があります。
zos add MyLinkedContract
変更をブロックチェーンにプッシュしましょう。
zos push --deploy-dependencies
ここには--deploy-dependencies
フラグが付いています。 この機能はすでにデプロイされているパッケージを再利用することに関するもので、openzeppelin-eth
や他の多くのパッケージをmainnet、ropsten、rinkeby、kovanに既に配備しています。しかし、このガイドではこれらのネットワークを使用していませんが、Truffleが空のローカル開発ネットワークを開始しました。 したがって、--deploy-dependenciesはZeppelinOSに依存しているEVMパッケージをネットワークに展開するように指示します。これは一度だけ行う必要があるため、上記の実際のネットワークのいずれかを使用している場合、またはローカルネットワーク上でコマンドを再度実行している場合、このフラグは必要ありません。
コントラクトのアップグレード可能なインスタンスを作ってみましょう。
zos create MyLinkedContract
EVMパッケージからのERC721トークンのインスタンスも必要です。
zos create openzeppelin-eth/StandaloneERC721 --init initialize --args MyToken,TKN,[<address>],[<address>]
<address>はトークンのminterとpauserになります。ローカル開発では、ganache-cliがデフォルトで作成した10のアドレスのうちの1つを使用できます。
最後に、新しいTruffleコンソールを起動して、コントラクトを動かします
npx truffle console --network local
Truffleコンソールに移動して、2つの展開されたアップグレード可能なコントラクトを結びましょう。
<my-linked-contract-address>
および<my-erc721-address>
を、それぞれMyLinkedContractおよびStandaloneERC721で作成したアップグレード可能なインスタンスのアドレスに置き換えてください。両方のアドレスは、上で実行したcreateコマンドによって返されました。
truffle(local)> MyLinkedContract.at('<my-linked-contract-address>').setToken('<my-erc721-address>')
コントラクトとトークンの両方のアドレスは、zos.dev-<network_id>.json設定ファイルにもあります。
これは、開発者が知識とクールなアイデアをEVMパッケージで共有する、より優れたブロックチェーンエコシステムの始まりに過ぎず、すべてのパッケージを使用して改善することで貢献します。これはまもなく、社会が働くための狂気の新しい方法を実装するパッケージの群れとなり、それらはすべてあなたのプロジェクトにリンクしてそれらの上に構築することができます。
EVMパッケージの公開
以前のガイドでは、ZeppelinOSプロジェクトを初期化する方法、コントラクトを追加する方法、それらのコントラクトをアップグレードする方法、既存のEVMパッケージにリンクする方法について説明しました。この時点では、さまざまなZeppelinOS機能を使用するシンプルなプロジェクトがあります。このガイドでは、プラットフォームに新しい機能を追加し、他の人が使用できるようにEVMパッケージとして素早くラップします。
プロジェクトのディレクトリで、初期化します。
mkdir<project-name>
cd<project-name>
npm init
zos init<project-name>
次に、contractsディレクトリにあるすべてのコントラクトをプロジェクトに追加することができます。
zos add<contract-name-1><contract-name-2> ...<contract-name-n>
次にプロジェクトをネットワークにpushします。
zos push --network<network>
EVMパッケージを他の人が使用するには、ローカルの開発ネットワークの代わりに実際のネットワークを使用する必要があります。 私たちは便利ですmainnetへのデプロイについてのガイドを持っています。
すべては、これまでのコマンドを知られています。 ここでは、新機能です。 ZeppelinOSでパッケージを共有したい場合は、publishコマンドを実行してください。
zos publish --network<network>
これでEVMパッケージの開発者になれました。しかし、これはまだ半分に過ぎません。なぜなら、パッケージが他のプロジェクトによって発見され、リンクされるためには、パッケージをnpmに公開する必要があるからです。
以前にパッケージを公開していない場合は、npmアカウントにサインアップする必要があります。
npmパッケージには、すべてのソースとコンパイル済みのコントラクトとZeppelinOS設定ファイルが含まれている必要があります。したがって、package.jsonファイルに次の最上位フィールドを追加してください。
{
...,
"files": [
"build",
"contracts",
"test",
"zos.json",
"zos.*.json"
]
}
zos設定ファイルは、ZeppelinOSがコントラクト、作成したインスタンスのアドレス(この場合は公開したEVMパッケージのアドレス)を追跡するものです。 これはZeppelinOSがあなたのものに依存するプロジェクトのリンクを解決する方法です。
残りのフィールドでパッケージが正確に記述されていることを確認してください。 EVMパッケージには意味がないため、mainフィールドが存在する場合は削除することをお勧めします。 また、zos.dev-<network_id>.jsonファイルがある場合は、ローカルのテスト環境に固有のため、これを削除することもできます。
次のコマンドでnpmにログインします。
npm login
npmに公開します。
npm publish
他の開発者は、以下を使用してパッケージにリンクすることができます。
zos link<your-project-name>
EVMパッケージの保証
ZeppelinOSのネイティブZEPトークンは、ZeppelinOSを使用する必要はありませんが、健全なEVMパッケージのエコシステムの作成と保守にインセンティブを与えるために使用されます。これは、この記事の以下のセクションで詳述される一連のオプトインメカニズムを介して行われます。
EVMパッケージ保証
誰でも無料でZeppelinOS EVMパッケージを作成できます。EVMパッケージを作成するためにZEPを保持または使用する必要はありません。
しかし、ZeppelinOSは、ZEPを使用するためにEVMパッケージを登録して保証する仕組みを提供しています。評価のためにEVMパッケージに欠陥がある場合はいつでも、EVMパッケージに保証されたトークンを他のZEP保持者がチャレンジすることができます。このような状況では、パッケージの保証されたトークンをチャレンジした人に有利に削減することができました。
最終的な目標は、この単純なメカニズムにより、EVMパッケージが表すZEPが保証されることです。
- EVMパッケージのコード品質の指標。 大量のトークンが保証されたEVMパッケージは、挑戦者を引きつけ、コードをピアレビューする必要があります。パッケージの保証されたトークン隠蔽がそのような精査に耐えられるならば、コードが重要な脆弱性または欠点を有さないことを、署名されていないZEPの存在が示す。
- EVMパッケージがコミュニティから得たサポートの尺度。採用が多いEVMパッケージは、パッケージのガバナンスだけでなく収入モデルにも参加したいZEP保有者からの保証を受けます。
- EVMパッケージの新機能の開発のための財務バッファ。貢献者は、パッケージの開発者が投稿を受け入れるたびに、パッケージの保証トークンからZEPで報酬を受け取ることができます。
- EVMパッケージのコードを監査するための金融バッファ。審査員は、パッケージに問題が発生したときはいつでもZEPを受け取ることができます。
このコントラクトの現在のバージョンでは、現在次の機能がサポートされています。
- 新しいEVMパッケージの登録
- EVMパッケージの保証(パッケージ所有者のみ)
- EVMパッケージから保証されたトークンを削除する
- レジストリからEVMパッケージを削除する
ZEPトークンプライベートBeta
ZEPはERC20トークンであり、TPL(Transaction Permission Layer)管轄区域によって制御されます。これにより、地域の管轄区域では、KYCを使用してZEPを保持または交換できるアドレス、または地域に適した他の検証プロセスがあればそれを管理できます。
Zeppelinは現在、ZeppelinOSの採用に関心のあるプロジェクトにmainnet ZEPを配布しています。 これは販売ではなく、前述のトークンダイナミクスのベータテストです。 プライベートベータ版の配布に適用するには、プロジェクトの詳細をZEPトークンのプライベートベータの送信フォームに提出してください。
ガイド
アップグレード可能なコントラクトの作成
ZeppelinOSでアップグレード可能なコントラクトを扱う場合、あなたのSolinityコードを書く際に注意すべき点がいくつかあります。
これらの制限は、Ethereum VMの仕組みにルーツがあり、ZeppelinOSだけでなくアップグレード可能なコントラクトで動作するすべてのプロジェクトに適用されることに言及することは重要です。
初期化
ZeppelinOSでは、コンストラクタを除き、変更なしでSolidityコントラクトを使用することができます。プロキシベースのアップグレード可能性システムの要件により、アップグレード可能なコントラクトではコンストラクタを使用できません。ZeppelinOS Upgrades Pattern(ZeppelinOSアップグレードパターン)ページで、この制限の理由を詳細に読むことができます。
ZeppelinOS内でコントラクトを作る際、そのコンストラクタを通常はinitializeという名前の通常の関数に変更する必要があります。ここでは、すべてのセットアップロジックを実行します。
contract MyContract {
uint256 public x;
function initialize(uint256 _x) public {
x = _x;
}
}
しかし、Solidityはコンストラクタが一度だけ呼び出されることを保証しますが、通常の関数は何度も呼び出すことができます。コントラクトが複数回初期化されないようにするには、initialize関数が1回だけ呼び出されるようにチェックを追加する必要があります。
contract MyContract {
uint256 public x;
bool private initialized;
function initialize(uint256 _x) public {
require(!initialized);
initialized = true;
x = _x;
}
}
このパターンはアップグレード可能なコントラクトを書くときに非常に一般的なものなので、ZeppelinOSはinitializer modifier持つ初期化可能なベースコントラクトを提供します。
import "zos-lib/contracts/Initializable.sol";
contract MyContract is Initializable {
uint256 public x;
function initialize(uint256 _x) initializer public {
x = _x;
}
}
コンストラクタと通常の関数の別の違いは、Solidityが自動的にコンストラクタのすべての祖先のコンストラクタを呼び出すことです。イニシャライザを書くときには、すべての親コントラクトのイニシャライザを手作業で呼び出すことが必要です。
import "zos-lib/contracts/Initializable.sol";
contract BaseContract is Initializable {
uint256 public y;
function initialize() initializer public {
y = 42;
}
}
contract MyContract is BaseContract {
uint256 public x;
function initialize(uint256 _x) initializer public {
BaseContract.initialize(); // Do not forget this call!
x = _x;
}
}
アップグレード可能なパッケージを使用する
この制限は作成したコントラクトだけでなく、ライブラリからインポートするコントラクトにも影響することに注意してください。たとえば、OpenZeppelinのERC20Detailedトークン実装を使用すると、コントラクトはトークンの名前、シンボル、および小数を初期化します。
Contract ERC20Detailed is IERC20 {
string private _name;
string private _symbol;
uint8 private _decimals;
constructor(string name, string symbol, uint8 decimals) public {
_name = name;
_symbol = symbol;
_decimals = decimals;
}
}
つまり、ZeppelinOSプロジェクトではこれらのコントラクトを使用すべきではありません。代わりにOpenzeppelin-eth(OpenZeppelinの公式フォーク)を使用してください。これはコンストラクタではなくイニシャライザを使用するように変更されています。例えば、openzeppelin-ethが提供するERC20の実装は、ERC20Mintableです。
contract ERC20Mintable is Initializable, ERC20, MinterRole {
function initialize(address sender) public initializer {
MinterRole.initialize(sender);
}
[...]
}
OpenZeppelinであろうと他のEVMパッケージであろうと、アップグレード可能なコントラクトを処理するようにパッケージが設定されていることを常に確認してください。
フィールド宣言の初期値を避ける
Solidityではコントラクトで宣言するときにフィールドの初期値を定義できます。
contract MyContract {
uint256 public hasInitialValue = 42;
}
これはコンストラクタでこれらの値を設定するのと同じで、アップグレード可能なコントラクトでは機能しません。 上記のようにすべての初期値がイニシャライザ機能で設定されていることを確認してください。 そうしないと、アップグレード可能なインスタンスにはこれらのフィールドが設定されません。
contract MyContract is Initializable {
uint256 public hasInitialValue;
function initialize() initializer public {
hasInitialValue = 42;
}
}
コントラクトコードから新しいインスタンスを作成する
コントラクトのコードからコントラクトの新しいインスタンスを作成する場合、これらの作成はZeppelinOSではなくSolidityによって直接処理されます。つまり、これらのコントラクトはアップグレードできません。
たとえば、次の例では、MyContractがアップグレード可能であっても(zos create MyContractを使用して作成された場合)、作成されるトークンコントラクトはそうではありません。
import "zos-lib/contracts/Initializable.sol";
import "openzeppelin-eth/contracts/token/ERC20/ERC20.sol";
import "openzeppelin-eth/contracts/token/ERC20/RC20Detailed.sol";
contract MyContract is Initializable {
ERC20 public token;
function initialize() initializer public {
token = new ERC20Detailed("Test", "TST", 18); // This contract will not be upgradeable
}
}
この問題を回避する最も簡単な方法は、コントラクトを自分で作成することを避けることです。initialize関数で契約を作成するのではなく、そのコントラクトのインスタンスをパラメータとして受け入れ、ZeppelinOSからインスタンスを作成した後に注入するだけです。
import "zos-lib/contracts/Initializable.sol";
import "openzeppelin-eth/contracts/token/ERC20/ERC20.sol";
contract MyContract is Initializable {
ERC20 public token;
function initialize(ERC20 _token) initializer public {
// This contract will be upgradeable if it was created via ZeppelinOS
token = _token;
}
}
$ TOKEN=$(zos create TokenContract)
$ zos create MyContract --init --args $TOKEN
先進的な方法として、アップグレード可能なコントラクトを即時に作成する必要がある場合は、ZeppelinOS Appのインスタンスをコントラクトに保存することです。 このアプリケーションは、ロジック実装への参照を持つZeppelinOSプロジェクトのエントリーポイントとして機能する契約であり、新しいコントラクトインスタンスを作成することができます。
import "zos-lib/contracts/Initializable.sol";
import "zos-lib/contracts/application/BaseApp.sol";
contract MyContract is Initializable {
BaseApp private app;
function initialize(BaseApp _app) initializer public {
app = _app;
}
function createNewToken() public returns(address) {
return app.create("openzeppelin-eth", "StandaloneERC20");
}
}
潜在的に危険な操作
アップグレード可能なスマートコントラクトを使用して作業する場合、常にコントラクトインスタンスとやりとりし、基盤となるロジックコントラクトは使用しません。 しかし、悪意のあるアクタが取引を直接ロジックコントラクトに送ることを妨げるものは何もありません。これは、ロジックコントラクトの記憶域がプロジェクトで使用されることはないため、ロジックコントラクトの状態の変更がコントラクトインスタンスに影響を与えないため、脅威にはなりません。
ただし、例外があります。ロジックコントラクトへの直接呼び出しによってselfdestructがトリガーされると、ロジックコントラクトが破棄され、すべてのコントラクトインスタンスがコードなしですべてのコールをアドレスに委任されます。これにより、プロジェクト内のすべてのコントラクトインスタンスが効果的に破棄されます。
ロジックコントラクトがdelegatecall操作を含む場合、同様の効果を達成することができる。selfdestructを含む悪意のあるコントラクトに委任するようにコントラクトを結ぶことができる場合、呼び出し元のコントラクトは破棄されます。
したがって、あなたのコントラクトでは、selfdestructまたはdelegatecallの使用を避けることを強く推奨します。それらを含める必要がある場合は、初期化されていないロジックコントラクトで攻撃者が呼び出すことができないことを絶対に確認してください。
コントラクトの変更
新機能やバグ修正のためにコントラクトの新しいバージョンを書くときには、コントラクトの状態変数が宣言されている順序やタイプを変更することはできません。
つまり、次のような初期コントラクトがある場合
contract MyContract {
uint256 private x;
string private y;
}
次のように変数のタイプを変更することはできません。
contract MyContract {
string private x;
string private y;
}
または宣言されている順序を変更する
contract MyContract {
string private y;
uint256 private x;
}
あるいは既存の変数の前に新しい変数を導入する
contract MyContract {
bytes private a;
uint256 private x;
string private y;
}
既存の変数を削除する
contract MyContract {
string private y;
}
新しい変数を導入する必要がある場合は、必ず最後に必ず行ってください。
contract MyContract {
uint256 private x;
string private y;
bytes private z;
}
変数の名前を変更すると、アップグレード後も以前と同じ値が保持されることに注意してください。新しい変数が古い変数と意味的に同じ場合、これは望ましい動作です。
contract MyContract {
uint256 private x;
string private z; // starts with the value from `y`
}
コントラクト終了時に変数を削除した場合は、ストレージがクリアされないことに注意してください。新しい変数を追加する後続の更新では、その変数が削除された値から残りの値を読み取るようになります。
contract MyContract {
uint256 private x;
}
// Then upgraded to...
contract MyContract {
uint256 private x;
string private z; // starts with the value from `y`
}
親コントラクトを変更してコントラクトのストレージ変数を誤って変更することもあります。たとえば、次のコントラクトがあるとします。
contract A {
uint256 a;
}
contract B {
uint256 b;
}
contract MyContract is A, B { }
ベースコントラクトが宣言されている注文を交換するか、新しいベースコントラクトを導入することでMyContractを変更すると、変数の実際の格納方法が変更されます。
contract MyContract is B, A { }
子コントラクトが独自の変数を持っている場合は、ベースコントラクトに新しい変数を追加することもできません。以下のシナリオを考えます。
contract Base {
uint256 base1;
}
contract Child is Base {
uint256 child;
}
追加の変数を追加するようにBaseが変更された場合
contract Base {
uint256 base1;
uint256 base2;
}
次に、変数base2は、その子が以前のバージョンで持っていたスロットに割り当てられます。これを回避するには、未使用の変数を、今後拡張する可能性のあるベースコントラクトに、そのスロットを「予約する」手段として宣言します。このトリックはガス使用量の増加を伴わないことに注意してください。
これらのストレージレイアウトの制限に違反すると、アップグレードされたコントラクトバージョンでストレージ値が混在し、アプリケーションに重大なエラーが発生する可能性があります。
アップグレード可能なプロジェクトのテスト
ZeppelinOSを使って作業するときは、通常どおりにコントラクトをテストしたり、ZeppelinOSでテスト環境でプロジェクト全体を自動的に設定したりすることができます。 これにより、実行するテストごとにプロジェクトを管理するのと同じ一連のコントラクトを複製することができます。
zosパッケージには、プロジェクト構造をzos.jsonファイルから取得し、現在のテストネットワークにすべてをデプロイするためのTestHelper()関数が用意されています。 zos addを介して登録したすべての契約と、リンクしたEVMパッケージが提供するすべてのコントラクトが利用可能になります。返されたプロジェクトオブジェクト(SimpleProjectまたはAppProjectのいずれか)は、コントラクトのアップグレード可能なインスタンスを作成するための便利なメソッドを提供します。これをテストに使用できます。
重要:テスト環境でTestHelperを正しく動作させるには、テスト実行時にテストするために
NODE_ENV環境変数を設定する必要があります。たとえば、truffleを使用している場合は、
`NODE_ENV = test truffle test`を実行します。
例
ERC20トークンの実装を提供するEVMパッケージにリンクされたサンプルコントラクトを持つ小規模なプロジェクトがあるとします。
{
"zosversion": "2",
"name": "my-project",
"version": "0.1.0",
"contracts": {
"Sample": "Sample"
},
"dependencies": {
"openzeppelin-eth": "2.0.2"
}
}
テストファイルにプロジェクト全体を設定するには、まずzosからTestHelperをインポートします
import { TestHelper } from 'zos';
次に、テストスイートのセットアップでそれを呼び出します。オプションとして、コントラクトを展開するときに使用するオプションのセット(from、gas、およびgasPriceなど)も含まれます。
beforeEach(async function () {
this.project = await TestHelper({ from: owner })
});
テストでは、プロジェクトのcreateProxy関数を使用してコントラクトのインスタンスを生成できます。
const Sample = artifacts.require('Sample')
it('should create a proxy', async function () {
const proxy = await this.project.createProxy(Sample);
// Use proxy ...
})
サンプルテストファイルの完全なコードは次のとおりです。
import { TestHelper } from 'zos';
const Sample = artifacts.require('Sample')
const ERC20 = artifacts.require('ERC20')
contract('Sample', function ([_, owner]) {
beforeEach(async function () {
this.project = await TestHelper({ from: owner })
})
it('should create a proxy', async function () {
const proxy = await this.project.createProxy(Sample);
const result = await proxy.greet();
result.should.eq('A sample')
})
it('should create a proxy for the EVM package', async function () {
const proxy = await this.project.createProxy(ERC20, { contractName: 'StandaloneERC20', packageName: 'openzeppelin-eth' });
const result = await proxy.totalSupply();
result.toNumber().should.eq(0);
})
})
実稼働環境で実行する前に、この便利なツールを使用してコードとストレージの移行テストを作成します。
ユニットテストで手動でinitialize関数を呼び出す
Truffleは、コントラクトに名前は一致していても機能が異なる状況を解決する方法を知りません。次に、Crowdsaleから継承したTimedCrowdsaleコントラクトの例を示します。このコントラクトでは、さまざまなアリティを持つ2つのinitialize関数があります。
contract TimedCrowdsale is Crowdsale {
initialize(uint256 _openingTime, uint256 _closingTime) public initializer {
Crowdsale.initialize(_rate, _wallet, _token);
}
}
contract Crowdsale {
initialize(uint256 _rate, address _wallet, ERC20 _token) public initializer {
// does something
}
}
これは、Truffleから直接initializeを呼び出すと、OpenZeppelin(例えば、TimedCrowdsale)からのいくつかの契約の場合のように、initializeという名前の複数の関数を持つコントラクトの呼び出しが元に戻ってしまう可能性があることを意味します。zos createは、パラメータをエンコードするときにこれを正しく処理します。ただし、単体テストでは、手動でinitializeを呼び出す必要があります。
この問題に対する現在の解決策は、 npm install zos-lib で、 zos createコマンドで使用されているのと同じヘルパー関数encodeCallを使用することです。このヘルパーは、引数と型だけでなく、initialize関数のシグネチャも受け取ります。encodeCallは、生の呼び出しで送信できる呼び出しデータを作成します。たとえば、次のようにTimedCrowdsale#initialize
を呼び出すことができます。
data = encodeCall(
'initialize',
['uint256', 'uint256'],
[openingTime, closingTime]
)
const timeCrowdsale = await TimeCrowdsale.new()
await timeCrowdsale.sendTransaction({ data, from: owner })
ERC20トークンのオンボーディング
前書き
このガイドでは、通常のERC20トークンをアップグレード可能なバージョンに移行する方法について説明します。 このプロセスの間に、元のコントラクト(「レガシー」と呼ばれる)およびアップグレードされる能力を有する新たなコントラクトが共存することになります。
新しいアップグレード可能なコントラクトは、従来のコントラクトで提供されているのと同じ機能を持ちますが、アップグレード可能です。これは、将来的に新しい移行を実行する必要なく、新しい機能を追加したり、新しいデータを保存したり、バグを修正したり、必要なだけ新しい基準をサポートしたりできることを意味します。
戦略
この戦略は、トークン残高のオプションの移行に基づいています。この移行はトークンホルダーによって実行され、支払われます。新しいアップグレード可能なトークンコントラクトは、最初の供給がなく、残高がない状態から開始されます。新しいトークンを「作成」する唯一の方法は、ユーザーが古いトークンを「入れ替える」ことです。これは、まずマイグレーションする量を承認してから、アップグレード可能なトークンの機能を呼び出してマイグレーションを実行することによって行われます。古いトークンはバーンアドレスに送られ、所有者は新しいトークンコントラクトで同額を受け取ります。
要件
この搭乗計画では、以下の前提を考慮しています。
- ERC20標準に準拠した、すでに展開されているトークンコントラクトがあります。
- レガシートークンコントラクトは固定されていないか、一時停止されていないため、トークン所有者はそれを交換できます。
オンボーディング計画のデモ
この提案の中心的なアイデアは、ZeppelinOSコマンドラインツールを使用して、トークンのアップグレード可能なバージョンを展開することです。さらに、ZeppelinOSのOpenZeppelin EVMパッケージであるopenzeppelin-ethが提供するERC20Migratorコントラクトを使用します。
オンボーディング計画をデモするためのローカル環境をセットアップします。これを行うために、サンプルレガシートークンコントラクトを展開し、いくつかの残高を作成します。
サンプルのリポジトリには、現実のシナリオをローカルでシミュレートするために使用するMyLegacyTokenというコントラクトがあります。 ご覧のとおり、このトークンは、テスト目的のためだけに初期化されたトークンを所有者に100トークン化します。
開始する前に、npm installを実行している依存関係をインストールすることを忘れないでください。さらに、npm testでテストファイルを実行することで、すべてが期待どおりに動作していることを確認する必要があります。
さて、レガシートークンをデプロイしましょう。私たちは、あなたのコントラクトを展開してアプリケーションを開発するために使うことができるEthereum開発用の個人的なブロックチェーンであるganache-cliを使用します。使用を開始するには、次のコマンドを実行します。
npx ganache-cli --port 9545
その後、truffle-config.js内の既に設定されているネットワークに、以下のコマンドを実行することで、truffleコンソールを接続することができます
npx truffle console --network local
次に、次のコマンドを実行します。
truffle(local)> compile
truffle(local)> owner = web3.eth.accounts[1]
truffle(local)> MyLegacyToken.new({ from: owner }).then(i => legacyToken = i)
truffle(local)> legacyToken.address
'0x...'
所有者とlegacyTokenのアドレスを把握しておくには、次の手順で必要になります。
所有者の残高を確認するには、以下を実行します。
truffle(local)> legacyToken.balanceOf(owner)
BigNumber { s: 1, e: 22, c: [ 100000000 ] }
後でそれを使用するので、このコンソールを閉じないように注意してください。
1. ZeppelinOSで移行プロジェクトを初期化する
ZeppelinOS CLIをインストールしていない場合は、ターミナルで次のコマンドを実行します。
npm install --global zos
このプロジェクトをZeppelinOSで初期化するには、ターミナルを開いて次の行を実行します。
zos init my-token-migration 1.0.0
新しいZeppelinOSプロジェクトを初期化しました。 新しいzos.jsonファイルが作成されているはずです。
次に、既存のトークンコントラクトを変更して、現在の残高を移行する新しいアップグレード可能なバージョンを取得する必要があります。
サンプルプロジェクトでは、MyUpgradeableTokenと呼ばれる別のコントラクトがあります。これはサンプルレガシートークンコントラクトMyLegacyTokenのアップグレード可能なバージョンです。
import "zos-lib/contracts/Initializable.sol";
import "openzeppelin-eth/contracts/token/ERC20/IERC20.sol";
import "openzeppelin-eth/contracts/drafts/ERC20Migrator.sol";
import "openzeppelin-eth/contracts/token/ERC20/ERC20Mintable.sol";
import "openzeppelin-eth/contracts/token/ERC20/ERC20Detailed.sol";
contract MyUpgradeableToken is Initializable, ERC20, ERC20Detailed, ERC20Mintable {
function initialize(ERC20Detailed _legacyToken, ERC20Migrator _migrator) initializer public {
ERC20Mintable.initialize(_migrator);
ERC20Detailed.initialize(_legacyToken.name(), _legacyToken.symbol(), _legacyToken.decimals());
_migrator.beginMigration(this);
}
}
一方で、レガシートークンによって提供されたすべての情報と機能を複製することは非常に重要です。この場合、私たちはOpenZeppelin EVMパッケージが提供するERC20とERC20Detailedコントラクトを継承し、ERC20トークンのすべての機能を複製します。
一方、必要なすべての移行機能を処理するERC20Migratorインスタンスを受け取るinitializeメソッドを定義しています。ERC20Migratorを使用するには、アップグレード可能なトークンを作成する必要があります。そのため、OpenZeppelin EVMパッケージで提供されるERC20Mintableコントラクトも継承しています。
イニシャライザは、ZeppelinOSでアップグレード可能なコントラクトのコンストラクタ機能を定義する方法です。 Initializer modifierは、あなたのinitializeメソッドがコントラクトの生涯にわたって一度しか呼び出されないようにします。
openzeppelin-ethからのすべてのコントラクトは、ZeppelinOSとの互換性に適合しており、アップグレード可能なコントラクトを扱う際に使用されるものでなければならないことに注意してください。
ZeppelinOSは、アップグレード可能なアプリケーションを構築できるほか、EVMパッケージを提供します。プロジェクトでEVMパッケージを使用するには、使用するEVMパッケージのnpmパッケージの名前を指定するlinkコマンドを使用するだけです。この場合、プロジェクトで提供されているコントラクトを使用できるように、OpenZeppelin EVMパッケージをリンクします。
zos link openzeppelin-eth@^2.0.0
最後に、アップグレード可能なトークンコントラクトをプロジェクトに追加することができます
zos add MyUpgradeableToken
このプロジェクトはOpenZeppelin EVMパッケージにリンクされており、MyUpgradeableTokenが追加されました。
2. アップグレード可能なトークンを展開する
まず、コントラクトソースコードを配備する必要があります。 また、OpenZeppelin EVMパッケージのコピーを展開する必要があります。これは、ローカル環境で作業するためです。 これを行うには、次のコマンドを実行します。
zos push -n local --deploy-dependencies
OpenZeppelin EVMパッケージはローカルのブロックチェーンにまだデプロイされていないため、--deploy-dependencies
を使用してOpenZeppelin EVMパッケージをローカルにデプロイすることに注意してください。
MyUpgradeableTokenソースコードとOpenZeppelin EVMパッケージをローカルネットワークにデプロイしました。新しいzos.dev-<network_id>.jsonファイルが作成されているはずです。
MyUpgradeableTokenソースコードとOpenZeppelin EVMパッケージをローカルネットワークにデプロイしました。新しいzos.dev-<network_id>.jsonファイルが作成されているはずです。
次に、ZeppelinOSを使用してアップグレード可能なトークンの新しいインスタンスを作成しましょう。これを行うには、最初にERC20Migratorのインスタンスを作成する必要がありますが、まだOpenZeppelin EVMパッケージでは提供されていないため、手動で追加する必要があります。次に、次のコマンドを実行して、LEGACY_TOKEN_ADDRESSを従来のトークンコントラクトのアドレスに置き換えます。
zos add ERC20Migrator
zos push -n local --deploy-dependencies
zos create ERC20Migrator --args LEGACY_TOKEN_ADDRESS -n local
OpenZeppelin EVMパッケージで提供されるERC20Migratorコントラクトを使用して、アップグレード可能な新しいインスタンスを作成しました。次に、次のコマンドを実行してMyUpgradeableTokenの新しいアップグレード可能なインスタンスを作成できます。
LEGACY_TOKEN_ADDRESSをレガシートークンコントラクトのアドレスとERC20_MIGRATOR_ADDRESSを上記で作成したインスタンスのアドレスに置き換えてください。
zos create MyUpgradeableToken --args LEGACY_TOKEN_ADDRESS,ERC20_MIGRATOR_ADDRESS -n local
このコマンドで出力されたアップグレード可能なトークンアドレスを保存します。後で必要になります。
ZeppelinOSが今作成したアップグレード可能なインスタンスを追跡しているので、zos.dev-<network_id>.json
のプロキシセクションに以下が含まれているはずです。
{
...,
"proxies": {
"erc20-onboarding/ERC20Migrator": [
{
"address": "0x...",
"version": "1.0.0",
"implementation": "0x..."
}
],
"erc20-onboarding/MyUpgradeableToken": [
{
"address": "0x...",
"version": "1.0.0",
"implementation": "0x..."
}
]
},
...
}