インストール
~ % npm install --global @openzeppelin/cli
Upgradableに使えるパッケージをいれる
proxyコントラクトを作成するときにconstructerの代わりにinitializeを使用するため
npm install @openzeppelin/upgrades --save
Networkの設定
初期だと以下になっている
module.exports = {
networks: {
development: {
protocol: 'http',
host: 'localhost',
port: 8545,
gas: 5000000,
gasPrice: 5e9,
networkId: '*',
},
},
};
Ganacheやテストネットの設定にしておこう
本記事ではGanacheを使用
コントラクトの作成
今回は単純な計算機を作成。
割った時の小数点以下は表現できない。簡単のためこのコントラクトを作成
一応OwnarbleとSafeMathを実装。(Openzeppelinのパッケージにあるが、インポートせずになんとなく書いた)
// "SPDX-License-Identifier: UNLICENSED"
pragma solidity ^0.7.0;
import "@openzeppelin/upgrades/contracts/initializable.sol";
// Openzeppelin SafeMath
library SafeMath {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: addition overflow");
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
return sub(a, b, "SafeMath: subtraction overflow");
}
function sub(
uint256 a,
uint256 b,
string memory errorMessage
) internal pure returns (uint256) {
require(b <= a, errorMessage);
uint256 c = a - b;
return c;
}
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
if (a == 0) {
return 0;
}
uint256 c = a * b;
require(c / a == b, "SafeMath: multiplication overflow");
return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256) {
return div(a, b, "SafeMath: division by zero");
}
function div(
uint256 a,
uint256 b,
string memory errorMessage
) internal pure returns (uint256) {
require(b > 0, errorMessage);
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
}
contract Ownarble is Initializable {
address private owner;
// constructor(){
// owner = msg.sender;
// }
function initialize() public virtual initializer {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "This function can run only owner!");
_;
}
function getOwner() public view returns (address) {
return owner;
}
}
contract Calculater is Ownarble {
using SafeMath for uint256;
uint256 private counter;
// constructor(){
// counter = 0;
// }
function initialize() public override initializer {
Ownarble.initialize();
counter = 0;
}
function addCounter(uint256 num) public onlyOwner {
counter = counter.add(num);
}
function subCounter(uint256 num) public onlyOwner {
counter = counter.sub(num);
}
function mulCounter(uint256 num) public onlyOwner {
counter = counter.mul(num);
}
function divCounter(uint256 num) public onlyOwner {
counter = counter.div(num);
}
function getCounter() public view returns (uint256) {
return counter;
}
}
親コントラクトの初期化はオーバーライドを宣言して親のinitializeを呼び出す。
呼び出さないと初期化されないから注意
initialize
upgradableなコントラクトはコンストラクタが使用できない。
そのためフツーの関数をコンストラクタのように初期化させる。
Openzeppelinではinitialize関数をinitializer関数修飾子で定義している。
initializer関数修飾子を定義した関数は一度しか呼ばれない。
以下が簡略化したInitializableコントラクトである。
contract Initializable{
bool private initialized; // default is "false"
modifier initializer() {
require(!initialized)
initialized = true;
_;
}
}
デプロイ
以下でデプロイできる
~ % npx oz deploy
~ % npx oz deploy
✓ Compiled contracts with solc 0.7.0 (commit.9e61f92b)
? Choose the kind of deployment upgradeable // 今回はupgradableがいいのでupgradableを選択
? Pick a network development // ネットワークは`networks.js`でGanacheを指定したいのでdevelop
? Pick a contract to deploy Calculater // デプロイするコントラクト
✓ Contract Calculater deployed
All implementations have been deployed
? Call a function to initialize the instance after creating it? Yes // initializeしてからコントラクトは作成
? Select which function * initialize() // Calculaterのinitializeを指定
✓ Setting everything up to create contract instances
✓ Instance created at 0x2c7823cD749bfa8159c5eE1e5623d083Ec297133
コントラクトが0x2c7823cD749bfa8159c5eE1e5623d083Ec297133
のアドレスでデプロイされた。
デプロイしたコントラクトの呼び出し
Openzeppelin-cliのここが嬉しい
簡単に関数を呼び出したり、トランザクションを作成することが可能
Call
viewやpureで指定した関数が指定できる
~ % npx oz call
? Pick a network development
? Pick an instance Calculater at 0x2c7823cD749bfa8159c5eE1e5623d083Ec297133
? Select which function getCounter()
✓ Method 'getCounter()' returned: 100
100
Send Transaction
~ % npx oz send-tx
? Pick a network development
? Pick an instance Calculater at 0x2c7823cD749bfa8159c5eE1e5623d083Ec297133
? Select which function addCounter(num: uint256)
? num: uint256: 100
✓ Transaction successful. Transaction hash: 0x7bd5cfb8c57412c2646e703534ad783e9c99d298c50d6aec6c85de3b95fff04c
どうやらガスの設定などもできるみたい
APIリファレンスを参照してね
(https://docs.openzeppelin.com/openzeppelin/)
Transfer
コントラクトからイーサを送る
~ % npx oz transfer
? Pick a network development
? Choose the account to send transactions from (0) 0x96290189d7fA46eBf93ddd3EeAb1c52C8dD87e5a
? Enter the receiver account
? Enter an amount to transfer
Upgradeする
先ほどのContractにメモの機能をつける。
Calculaterコントラクトの最後にv2の部分を追加
function getCounter() public view returns (uint256) {
return counter;
}
//////v2///////
string private memo;
function setMemo(string memory _memo) public onlyOwner {
memo = _memo;
}
function getMemo() public view returns (string memory) {
return memo;
}
//////v2///////
}
その後upgradeするコマンドを打つ
? Pick a network development
? Which instances would you like to upgrade? All instances
✓ Compiled contracts with solc 0.7.0 (commit.9e61f92b)
Compilation warnings:
@openzeppelin/upgrades/contracts/initializable.sol: Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.
There is more than one contract named Ownarble. The compiled artifact for only one of them will be generated.
There is more than one contract named SafeMath. The compiled artifact for only one of them will be generated.
- New variable 'string memo' was added in contract Calculater in contracts/Calculater.sol:1 at the end of the contract.
See https://docs.openzeppelin.com/upgrades/2.6//writing-upgradeable#modifying-your-contracts for more info.
✓ Contract Calculater deployed
All implementations have been deployed
✓ Instance upgraded at 0x1bd96e1A780181f5eEea1f5Aa088DBb2Dff5891F. Transaction receipt: 0xc6cba979048a0676af085845c08122946ba520b06561a61f6533b6f3bb7243e8
✓ Instance at 0x1bd96e1A780181f5eEea1f5Aa088DBb2Dff5891F upgraded
この後npx oz call
などして実際にupgradeできたことを確認。
アンチパターン ~Upgradeできないコントラクト設計~
コントラクト内でインスタンスを作成している場合
例
以下の例ではMyContract
はアップグレード可能だが、ERC20の設計は変えることはできない。
proxyのstorageとして格納できないからである。
pragma solidity ^0.5.0;
import "@openzeppelin/upgrades/contracts/Initializable.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts-ethereum-package/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
}
}
回避策
コントラクト内でインスタンスを作成しないようにし、引数からtoken情報を入れる。
pragma solidity ^0.5.0;
import "@openzeppelin/upgrades/contracts/Initializable.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20.sol";
contract MyContract is Initializable {
ERC20 public token;
function initialize(ERC20 _token) initializer public {
// 'token' will be upgradeable if it was created via the OpenZeppelin CLI
token = _token;
}
}
$ TOKEN=$(npx oz create TokenContract)
$ npx oz create MyContract --init --args $TOKEN
変数の型の変更
変数の宣言時にメモリが取られるがそのメモリのとる量は型によって違う
変数部分のメモリ設計を変更することはできない
例
このようなコントラクトがあった場合
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 x1;
uint256 private x2;
string private y;
}
間違えやすいパターン
変数の削除後に追加
以下のようにx,y,zを宣言。
contract MyContract {
uint256 private x;
string private y;
bytes private z;
}
その後、ver2 ver3にupgradeする。ver2ではy,zの宣言が削除している。
しかし、メモリに変数は入ったままなので、ver3でzを宣言した場合元々yに格納されていたデータが入ってしまう。
// ver2
contract MyContract {
uint256 private x;
}
// ver3
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 { }
継承順番を変えてしまうと割り当てるメモリの順番が変更されてしまうため以下のように変更できない。
contract MyContract is B, A { }
親コントラクトの変数追加
以下のようなコントラクトを作成する。
contract Base {
uint256 base1;
}
contract Child is Base {
uint256 child;
}
base2を追加すると子コントラクトのchildのメモリが割り当てられてしまう。
contract Base {
uint256 base1;
uint256 base2;
}
まとめ
Openzeppelin cliは使いやすい!!
だが、全ていいようにやってくれるようではなく、upgradaできないコントラクトの設計(アンチパターン)を知っていたり、
EVMについてをある程度知っている必要があるので、初学者が使うには少しハードルが高い
まずcliでupgradeする前にProxy Contract Patternを知っておこう