OpenZeppelinアップグレードプラグインを使用してデプロイされたスマートコントラクトは、元のコントラクトアドレス、状態、およびバランスを維持しながら、コードを変更するためにアップグレードできます。これにより、プロジェクトに新しい機能を追加したり、本番環境で見つかったバグを修正したりできます。
このガイドでは、次のことを学びます。
-
アップグレードが重要な理由
-
アップグレードプラグインを使用して、ボックスをアップグレードします。
-
内部でアップグレードがどのように機能するかを学ぶ
-
アップグレード可能な契約を作成する方法を学ぶ
アップグレード可能な契約とは
イーサリアムのスマートコントラクトはデフォルトで不変です。一度作成されると、変更することはできず、契約参加者の不変の契約として効果的に機能します。
ただし、場合によっては、それらを変更できるようにしたいことがあります。従来の契約について考えてみてください。関係する両方の当事者がそれを変更することに同意した場合、調整を変更できます。また、イーサリアムでは、スマートコントラクトを変更して、発見したバグを修正したり(ハッカーが資金を盗んだりする可能性もあります)、機能を追加したり、適用するルールを変更したりできるようにしたいと考えています。
アップグレードできない契約のバグを修正するために必要なことは次のとおりです。
-
契約の新しいバージョンを展開します
-
すべての状態を古い契約から新しい契約に手動で移行します(これはガスで非常に高価になる可能性があります!)
-
新しい契約のアドレスを使用して、古い契約と相互作用するすべての契約を更新します
-
すべてのユーザーに連絡して、新しい展開の使用を開始するように説得します(ユーザーの移行が遅いため、2つのコントラクトを同時に使用する問題に対処します)
この混乱を避けるために、契約のアップグレードをプラグインに直接組み込みました。これにより、状態、残高、住所を保持しながら契約コードを変更できます。それを行う方法を見てみましょう。
アップグレードプラグインを使用して契約をアップグレードします
OpenZeppelinアップグレードプラグインで新しいコントラクトをデプロイする場合deployProxy
、コントラクトインスタンスはアップグレード可能な機能を実装できます。デフォルトでは、契約が最初に展開されたアドレスのみがアップグレード操作を実行する権限を持っています。
deployProxy
次のトランザクションが作成されます。
-
実行契約(当社の
Box
契約)を展開する -
契約の展開
ProxyAdmin
(エージェントの管理者) -
プロキシコントラクトをデプロイし、初期化機能を実行します
以前にデプロイしたときと同じ設定で、Box
アップグレード可能なバージョンのコントラクトをデプロイすることにより、それがどのように機能するかを見てみましょう。
// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Box {
uint256 private value;
// Emitted when the stored value changes
event ValueChanged(uint256 newValue);
// Stores a new value in the contract
function store(uint256 newValue) public {
value = newValue;
emit ValueChanged(newValue);
}
// Reads the last stored value
function retrieve() public view returns (uint256) {
return value;
}
}
まず、アップグレードプラグインをインストールする必要があります。
HardhatUpgradesプラグインをインストールします。
npm install --save-dev @openzeppelin/hardhat-upgrades
プラグインを使用するようにHardhatを構成する必要があり@openzeppelin/hardhat-upgrades
ます。プラグインは、hardhat.config.jsファイルに次のコードを追加することで追加できます。
// hardhat.config.js
require('@nomiclabs/hardhat-ethers');
require('@openzeppelin/hardhat-upgrades');
module.exports = {
...
};
このような契約をアップグレードBox
するには、前に見た展開プロセスとは異なり、最初にアップグレード可能な契約として展開する必要があります。値42で呼び出すことによりstore
、Boxコントラクトを初期化します。
Hardhatには現在ネイティブのデプロイメントシステムがないため、コントラクトをデプロイするにはスクリプトが必要です。
deployProxyを使用して、アップグレード可能なBoxコントラクトをデプロイするスクリプトを作成します。ファイルをとして保存しますscripts/deploy_upgradeable_box.js
。
// scripts/deploy_upgradeable_box.js
const { ethers, upgrades } = require("hardhat");
async function main() {
const Box = await ethers.getContractFactory("Box");
console.log("Deploying Box...");
const box = await upgrades.deployProxy(Box, [42], { initializer: 'store' });
await box.deployed();
console.log("Box deployed to:", box.address);
}
main();
これで、アップグレード可能なコントラクトをデプロイできます。
コマンドを使用して、コントラクトをネットワークrun
に展開できます。Box
development
$ npx hardhat run --network localhost scripts/deploy_upgradeable_box.js
All contracts have already been compiled, skipping compilation.
Deploying Box...
Box deployed to: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
Box
コントラクトを使用してretrieve
、初期化時に保存した値を取得できます。
Hardhatコンソールを使用して、アップグレード契約を操作しますBox
。
Box
コントラクトを展開するときに、プロキシコントラクトのアドレスを指定する必要があります。
$ npx hardhat console --network localhost
All contracts have already been compiled, skipping compilation.
> const Box = await ethers.getContractFactory("Box")
undefined
> const box = await Box.attach("0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0")
undefined
> (await box.retrieve()).toString()
'42'
例として、新しい機能を追加したいとします。Box
新しいバージョンで、保存された値に1を追加する自動インクリメント関数を作成しますvalue
。
// contracts/BoxV2.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract BoxV2 {
// ... code from Box.sol
// Increments the stored value by 1
function increment() public {
value = value + 1;
emit ValueChanged(value);
}
}
Solidityファイルを作成した後、このupgradeProxy
関数を使用して、以前にデプロイされたインスタンスをアップグレードします。
upgradeProxy
次のトランザクションが作成されます。
-
実行契約(当社の
BoxV2
契約)を展開する -
ProxyAdmin
新しい実装を適用するためにプロキシコントラクトを更新するために呼び出されます
upgradeProxy を使用して、使用するコントラクトを Box
アップグレード するスクリプトを作成 しますBoxV2
。このファイルをとして保存しますscripts/upgrade_box.js
。コントラクトを展開するBox
ときに、プロキシコントラクトアドレスを指定する必要があります。
// scripts/upgrade_box.js
const { ethers, upgrades } = require("hardhat");
async function main() {
const BoxV2 = await ethers.getContractFactory("BoxV2");
console.log("Upgrading Box...");
const box = await upgrades.upgradeProxy("0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", BoxV2);
console.log("Box upgraded");
}
main();
その後、アップグレード可能な契約を展開できます。
このrun
コマンドを使用して、アップグレードコントラクトをdevelopment
ネットワークに展開できます。Box
$ npx hardhat run --network localhost scripts/upgrade_box.js
All contracts have already been compiled, skipping compilation.
Box deployed to: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
終了!インスタンスは、その状態と以前のアドレスを維持しながらBox
、最新バージョンのコードにアップグレードされています。新しいアドレスに新しい契約を展開する必要はありません。また、古い契約を新しいボックスに手動でコピーする必要もありません。Box
value
新しい関数を呼び出して値を確認increment
してみてくださいvalue
。
コントラクトをデプロイするBox
ときに、プロキシコントラクトアドレスを指定する必要があります。
$ npx hardhat console --network localhost
All contracts have already been compiled, skipping compilation.
> const BoxV2 = await ethers.getContractFactory("BoxV2")
undefined
> const box = await BoxV2.attach("0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0")
undefined
> await box.increment()
...
> (await box.retrieve()).toString()
'43'
、およびそのアドレスは、アップグレードBox
プロセス全体を通じて保持されることに注意してくださいvalue
。また、ローカルブロックチェーン、テストネット、メインネットのいずれで作業していても、プロセスは同じです。
OpenZeppelinアップグレードプラグインがどのように実装されているか見てみましょう。
アップグレードの仕組み
このセクションは他のセクションよりも理論的です。興味がある場合は、スキップして戻ってください。
OpenZeppelinアップグレードプラグインは、新しいアップグレード可能なコントラクトインスタンスが作成されるときに、実際には3つのコントラクトをデプロイします。
-
作成するコントラクトは、ロジックを含むいわゆるコントラクト実装です。
-
1つ
ProxyAdmin
は、エージェントの管理者です。 -
実際にやり取りするコントラクトである実装コントラクトを指すプロキシ。
ここで、プロキシは、すべての呼び出しを実装コントラクトに委任するだけの単純なコントラクトです。*デリゲートコール*は通常のコールと似ていますが、すべてのコードが呼び出し先のコンテキストではなく、呼び出し元のコンテキストで実行される点が異なります。このためtransfer
、コントラクトのストレージへの読み取りまたは書き込みは、ブローカー自身のストレージから読み取りまたは書き込みされます。これは、コントラクトを実行するコードで実際に転送されます。
これにより、コントラクトの状態をコードから切り離すことができます。エージェントは状態を保持し、実装コントラクトはコードを提供します。また、プロキシを別の実装コントラクトに委任するだけでコードを変更することもできます。
アップグレードには、次の手順が含まれます。
-
新しい実装契約を展開する
-
トランザクションをブローカーに送信し、その実装アドレスを新しい実装アドレスに更新します。
複数のプロキシに同じ実装コントラクトを使用させることができるため、同じコントラクトの複数のコピーをデプロイする場合は、このモードを使用してガスを節約できることに注意してください。
スマートコントラクトのユーザーは常にプロキシと対話し、プロキシがそのアドレスを変更することはありません。これにより、ユーザーが自分の側で何も変更しなくても、アップグレードをプッシュしたり、バグを修正したりできます。ユーザーは、いつもと同じアドレスを操作するだけです。
注 OpenZeppelinプロキシがどのように機能するかについて詳しく知りたい場合は、プロキシを確認してください。
アップグレード可能な契約の制限
スマートコントラクトはアップグレードできますが、Solidity言語のいくつかの制限に対処する必要があります。これらの問題は、契約の初期バージョンを作成するときと、新しいバージョンをアップグレードするときの両方で発生します。
初期化
アップグレード可能なコントラクトにコンストラクターを含めることはできませんconstructor
。コードの初期化を支援するために、 OpenZeppelin ContractsはInitializable基本コントラクトを提供します。これにより、メソッドにinitializerタグを 追加することで、コードが1回だけ初期化されるようになります。
たとえば、初期化子を使用して新しいバージョンのBox
コントラクトを記述しadmin
、コンテンツを変更できる唯一のアドレスとしてアドレスを設定します。
// contracts/AdminBox.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/proxy/Initializable.sol";
contract AdminBox is Initializable {
uint256 private value;
address private admin;
// Emitted when the stored value changes
event ValueChanged(uint256 newValue);
function initialize(address _admin) public initializer {
admin = _admin;
}
// Stores a new value in the contract
function store(uint256 newValue) public {
require(msg.sender == admin, "AdminBox: not admin");
value = newValue;
emit ValueChanged(newValue);
}
// Reads the last stored value
function retrieve() public view returns (uint256) {
return value;
}
}
コントラクトをデプロイするときは、initializer
関数名を指定し(名前がそうでない場合のみinitialize
)、管理者アドレスを指定する必要があります。
// scripts/deploy_upgradeable_adminbox.js
const { ethers, upgrades } = require("hardhat");
async function main() {
const AdminBox = await ethers.getContractFactory("AdminBox");
console.log("Deploying AdminBox...");
const adminBox = await upgrades.deployProxy(AdminBox, ['0xACa94ef8bD5ffEE41947b4585a84BdA5a3d3DA6E'], { initializer: 'initialize' });
await adminBox.deployed();
console.log("AdminBox deployed to:", adminBox.address);
}
main();
実用的な目的では、初期化子はコンストラクターとして機能します。ただし、これは通常の関数であるため、すべての基本コントラクト(存在する場合)の初期化子を手動で呼び出す必要があることに注意してください。
アップグレード可能な契約を作成する際のこれとその他の考慮事項の詳細については、アップグレード可能な契約の作成 ガイドをご覧ください。
アップグレード
技術的な制限により、契約を新しいバージョンにアップグレードする場合、その契約のストレージレイアウトを変更することはできません。
つまり、コントラクトで状態変数を宣言した場合、その前に状態変数を削除したり、タイプを変更したり、他の変数を宣言したりすることはできません。このBox
場合、これは、value
後でのみ新しい状態変数を追加できることを意味します。
// contracts/Box.sol
contract Box {
uint256 private value;
// We can safely add a new variable after the ones we had declared
address private owner;
// ...
}
幸い、この制限は状態変数にのみ影響します。契約の機能やイベントは自由に変更できます。
誤ってコントラクトのストレージレイアウトを台無しにした場合、アップグレードプラグインはアップグレードしようとしたときに警告を発することに注意してください。
その他の制限については、契約の変更 ガイドをご覧ください。
テスト
アップグレード可能なコントラクトをテストするには、コントラクトを実装するための単体テストを作成すると同時に、プロキシとの相互作用をテストするためのより高いレベルのテストを作成する必要があります。deployProxy
デプロイするときと同じように、テストで使用できます。
アップグレードするときは、新しい実装コントラクトの単体テストを作成すると同時に upgradeProxy
、アップグレード後にプロキシを介した相互作用をテストするためのより高いレベルのテストを作成し、アップグレード中に状態が一貫していることを確認する必要があります。
次のステップ
スマートコントラクトをアップグレードする方法を理解し、プロジェクトを繰り返し開発できるようになったので、プロジェクトをテストネットに持ち込んでライブにします。バグが発生した場合でも、契約を変更して修正するためのツールが用意されているので安心できます。