第4回 SolidityのUpgradeabilityとOpenZeppelinSDKとは

第4回 SolidityのUpgradeabilityとOpenZeppelinSDKとは

4.OpenZeppelinSDKの応用

はじめに


本連載では、スマートコントラクトのUpgradeabilityに焦点を当てています。スマートコントラクトは、普通のアプリケーションとは違い、一度デプロイしたコントラクトのコードは修正できないという不変性を持っています。この不変性を特殊な方法を用いて、回避することで、Developer Experienceの向上を図ることができる、「Upgradeability」のコンセプトをわかりやすく解説するのが本連載の目的です。

今回は、「4.OpenZeppelinSDKの応用」です。コントラクトのアップグレードツールであるOpenZeppelinSDKを利用して、より高度なUpgradeable Contractついて解説していきます。

関連する記事

第1回 SolidityのUpgradeabilityとOpenZeppelinSDKとは
第2回 SolidityのUpgradeabilityとOpenZeppelinSDKとは
第3回 SolidityのUpgradeabilityとOpenZeppelinSDKとは

OpenZeppelin-contracts-ethereum-packageとは

アップグレーダブルなコントラクトのライブラリの紹介


OpenZeppelinでは、十分にテストされたスマートコントラクトのパッケージを無償で提供しており、多くの開発者から支持を得てきました。これらはバグバウンティにかけられ、毎日アップデートされています。

参考
https://github.com/OpenZeppelin/openzeppelin-contracts

同様にOpenZeppelinSDKでは、アップグレーダブルな機能をもつコントラクトのパッケージを提供しており、開発者はそれを利用することで、簡単にアップグレーダブルなコントラクトを利用することができます。現在このパッケージを利用することで、以下の4つのアップグレーダブルなコントラクトを実装することが可能です。

StandaloneERC20
StandaloneERC721
TokenVesting
PaymentSplitter

参考
https://github.com/OpenZeppelin/openzeppelin-contracts-ethereum-package

Use upgradeable packages
https://docs.openzeppelin.com/upgrades/2.7/writing-upgradeable#use-upgradeable-packages

initilize関数とは


ロジックコントラクトには、constructor()に相当するコントラクトのデータの初期化を行う初期化関数が必要になります。OpenZeppelinSDKでは、その関数として「initilize()」を用意しています。

Initializers
https://docs.openzeppelin.com/sdk/2.6/writing-contracts#initializers

本来コントラクトがデプロイされる時、constructor()が発動されコントラクトの変数が保持する初期値が割り当てられます。しかしそれはロジックコントラクトの保持する状態のみに適用され、ストレージコントラクトが保持する状態はまた別途に初期化する必要があります。

そこでストレージコントラクトの文脈で状態の初期化を行う関数としてinitilize()を用意します。以下が参考です。
import "@openzeppelin/upgrades/contracts/Initializable.sol";

function initialize(uint256 _rate, IERC20 _token) public initializer {
    rate = _rate;
    token = _token;
  }

initializer modifierとは

initilize()関数は誤って何度も実行できてしまうと、ストレージが利用不可能な状態になってしまうため、initilize()関数はconstrctor()同様に、一度しか実行できないようにinitializer modifierで制限がかけられています。ロジックコントラクトを作成する際は、必ずinitilize関数を実装し、initializer modifier 修飾子を利用するようにしてください。

Initializable.sol
https://github.com/OpenZeppelin/openzeppelin-sdk/blob/master/packages/lib/contracts/Initializable.sol

アップグレーダブルなコントラクトの運用

アップグレーダブルなERC20のデプロイ


前記で紹介したopenzeppelin-contracts-ethereum-packageを利用して、StandaloneERC20をデプロイしていきます。以下のドキュメントを参考にしています。

Linking OpenZeppelin contracts
https://docs.openzeppelin.com/sdk/2.6/linking

SDKのCLIをインストールします。
$ npm install --global @openzeppelin/cli

執筆中の実行時点では、最新の2.6.0バージョンを利用していきます。任意な作業ディレクトリを作成し、初期化を行います。
~$ mkdir token-exchange && cd token-exchange
token-exchange$npm init -y
Wrote to /Users/YukinoriHamada/token-exchange/package.json:

{
  "name": "token-exchange",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

初期化を行います。
token-exchange$ openzeppelin init


? Welcome to the OpenZeppelin SDK! Choose a name for your project token-exchange
? Initial project version 1.0.0
Project initialized. Write a new contract in the contracts folder and run 'openzeppelin create' to deploy it.

以下の作業で「openzeppelin/contracts-ethereum-package」をインストールします。
token-exchange$openzeppelin link @openzeppelin/contracts-ethereum-package

✓ Dependency @openzeppelin/contracts-ethereum-package installed
Dependency linked to the project. Run 'openzeppelin create' to deploy one of its contracts.

「oz create」を実行し、「enter」を押して質問に答えながらコントラクトのデプロイを行なっていきます。一部引数の入力が求められますが、任意の文字や数字を入力してください。(以下の実行の値のコピーで構いません。)
token-exchange$ oz create
No contracts found to compile.
? Pick a contract to instantiate @openzeppelin/contracts-ethereum-package/StandaloneERC20
? Pick a network development
✓ Deploying @openzeppelin/contracts-ethereum-package dependency to network dev-1574386470880
All contracts are up to date
? Call a function to initialize the instance after creating it? Yes
? Select which function * initialize(name: string, symbol: string, decimals: uint8, initialSupply: uint256, initialHolder: address, minters: address[], pauser
s: address[])
? name (string): MyToken
? symbol (string): MYT
? decimals (uint8): 18
? initialSupply (uint256): 100e18
? initialHolder (address): 0x19648B26968Ef0ce385a99412dDBAF3B2F7C0Ea4
? minters (address[]): 0x19648B26968Ef0ce385a99412dDBAF3B2F7C0Ea4
? pausers (address[]): 0x19648B26968Ef0ce385a99412dDBAF3B2F7C0Ea4
✓ Setting everything up to create contract instances
✓ Instance created at 0xd414E2273eA489dD5454b67BCa42D1432Ea2dDE0
0xd414E2273eA489dD5454b67BCa42D1432Ea2dDE0

デプロイできたコントラクトのアドレスを別に控えておいてください。デプロイしたコントラクトのアドレスを引数として以下を実行し、初期発行を割り当てたアカウントの保有数を確認してみます。
token-exchange$ oz balance --erc20 0x19648B26968Ef0ce385a99412dDBAF3B2F7C0Ea4
? Enter an address to query its balance 
token-exchange$oz balance --erc20 0xd414E2273eA489dD5454b67BCa42D1432Ea2dDE0
? Enter an address to query its balance 0x19648B26968Ef0ce385a99412dDBAF3B2F7C0Ea4
? Pick a network development
Balance: 100 MYT
100000000000000000000

「oz call」を実行し、トランザクションの伴わない関数の実行をします。
token-exchange$ oz call
? Pick a network development
? Pick an instance StandaloneERC20 at 0xd414E2273eA489dD5454b67BCa42D1432Ea2dDE0
? Select which function name()
✓ Method 'name()' returned: MyToken
MyToken
token-exchange$oz call
? Pick a network development
? Pick an instance StandaloneERC20 at 0xd414E2273eA489dD5454b67BCa42D1432Ea2dDE0
? Select which function totalSupply()
✓ Method 'totalSupply()' returned: 100000000000000000000
100000000000000000000

トークン名や総発行量を確認できました。
「oz send-tx」を実行し、トランザクションの伴う処理を行います。
token-exchange$ oz send-tx
? Pick a network development
? Pick an instance StandaloneERC20 at 0xd414E2273eA489dD5454b67BCa42D1432Ea2dDE0
? Select which function transfer(to: address, value: uint256)
? to (address): 0xfD19F073E0177fEE72CDDf02eE60e5B5e7C75e0c
? value (uint256): 1e18
✓ Transaction successful. Transaction hash: 0xebcbe8c8f50877249a365e97dbab4049c60b0ec3aa6c0cb89b25e95b1ad3bcc1
Events emitted: 
 - Transfer(0x19648B26968Ef0ce385a99412dDBAF3B2F7C0Ea4, 0xfD19F073E0177fEE72CDDf02eE60e5B5e7C75e0c, 1000000000000000000)


token-exchange$ oz send-tx
? Pick a network development
? Pick an instance StandaloneERC20 at 0xd414E2273eA489dD5454b67BCa42D1432Ea2dDE0
? Select which function approve(spender: address, value: uint256)
? spender (address): 0xfD19F073E0177fEE72CDDf02eE60e5B5e7C75e0c
? value (uint256): 1e18
✓ Transaction successful. Transaction hash: 0x68206ff3f18f1b44460a02cda69afa8b514711f2d8ab4e2ef5cd222490ed0158
Events emitted: 
 - Approval(0x19648B26968Ef0ce385a99412dDBAF3B2F7C0Ea4, 0xfD19F073E0177fEE72CDDf02eE60e5B5e7C75e0c, 1000000000000000000)

トークンの移転や承認が行えました。
実行していない機能は複数あるため、試してみてください。

アップグレーダブルなexchange contractの作成


次にアップグレード可能なexchange機能を持つコントラクトをデプロイします。コントラクトはtoken-exchange/contractsディレクトリ配下に作成します。
$ touch contracts/TokenExchange.sol

以下のコードを新しいファイルにコピー・ペーストします。このコントラクトはfallback関数を実装しており、このコントラクトにETHを送金するとその関数が発動され、送金された分のETHと_rateと積(ETH×レート)の分のトークンが送金者に送り返されるようになっています。
pragma solidity ^0.5.0;

// Import base Initializable contract
import "@openzeppelin/upgrades/contracts/Initializable.sol";

// Import interface and library from OpenZeppelin contracts
import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";

contract TokenExchange is Initializable {
  using SafeMath for uint256;

  // Contract state: exchange rate and token
  uint256 public rate;
  IERC20 public token;

  // Initializer function (replaces constructor)
  function initialize(uint256 _rate, IERC20 _token) public initializer {
    rate = _rate;
    token = _token;
  }

  // Send tokens back to the sender using predefined exchange rate
  function() external payable {
    uint256 tokens = msg.value.mul(rate);
    token.transfer(msg.sender, tokens);
  }
}

用意したコントラクトをデプロイします。渡す引数に2番目はトークンのアドレスです。コントラクトのアドレスを控えておいてください。
token-exchange4$ oz create
✓ Compiled contracts with solc 0.5.13 (commit.5b0b510c)
? Pick a contract to instantiate TokenExchange
? Pick a network development
✓ Added contract TokenExchange
✓ Contract TokenExchange deployed
All contracts have been deployed
? Call a function to initialize the instance after creating it? Yes
? Select which function initialize(_rate: uint256, _token: address)
? _rate (uint256): 10
? _token (address): 0x02aDbeF4e8C167e18A005Cd7749dc7615d0138c2
✓ Instance created at 0xEdE0aB69fe6c4Ca6Df767ad19190eF9Fe465c405
0xEdE0aB69fe6c4Ca6Df767ad19190eF9Fe465c405

先ほど作成したER20トークンを以下の手順に従って、アカウント(0)からTokenExchangeコントラクトに移転(transfer)します。
token-exchange4$ oz send-tx
? Pick a network development
? Pick an instance StandaloneERC20 at 0x02aDbeF4e8C167e18A005Cd7749dc7615d0138c2
? Select which function transfer(to: address, value: uint256)
? to (address): 0xEdE0aB69fe6c4Ca6Df767ad19190eF9Fe465c405
? value (uint256): 10e18
✓ Transaction successful. Transaction hash: 0x1dd0005905e4cb2b94c23dc27555f56e2a806db6aa1cefbd7b0b5313225abef5
Events emitted: 
 - Transfer(0xF987BFe4396FEf1f75488467c6845b74A496e381, 0xEdE0aB69fe6c4Ca6Df767ad19190eF9Fe465c405, 10000000000000000000)

TokenExchangeコントラクトに0.1ETHをアカウント(1)から送金します。コントラクト側では、fallback関数が実行され、ETHの送金と同時に同等量のトークンがアカウント(1)に送金されます。
token-exchange4$ oz transfer
? Pick a network development
? Choose the account to send transactions from (1) 0x004925a18aDee88246BE28f020d12E11A1412b5F
? Enter the receiver account 0xEdE0aB69fe6c4Ca6Df767ad19190eF9Fe465c405
? Enter an amount to transfer 0.1 ether
✓ Funds sent. Transaction hash: 0x2b9013a16be4ba3d357d8aa159971a93d5d640d1c7e35dbddf4f0bdd3c26ef9b

以下の操作でアカウント(0)とアカウント(1)のトークン保有量を確かめてください。ganacheを起動しているタブで、該当するアカウントのアドレスのコピーし、コマンド操作のときにペーストしてください。
token-exchange4$ oz call
? Pick a network development
? Pick an instance StandaloneERC20 at 0x02aDbeF4e8C167e18A005Cd7749dc7615d0138c2
? Select which function balanceOf(account: address)
? account (address): 0xF987BFe4396FEf1f75488467c6845b74A496e381
✓ Method 'balanceOf(address)' returned: 90000000000000000000
90000000000000000000

token-exchange4$ oz call
? Pick a network development
? Pick an instance StandaloneERC20 at 0x02aDbeF4e8C167e18A005Cd7749dc7615d0138c2
? Select which function balanceOf(account: address)
? account (address): 0x004925a18aDee88246BE28f020d12E11A1412b5F
✓ Method 'balanceOf(address)' returned: 1000000000000000000
1000000000000000000

TokenExchange.solのアップグレード


TokenExchangeコントラクトは、ETHの入金はできますが引き出しができません。デポジットが永遠に引き出せないという問題をアップグレードで解決したいと思います。

まず以下のようにTokenExchange.solのファイルの中身を変更したいと思います。新しい状態変数「address public owner;」は、既存の変数の最後尾に追加します。また新しい関数withdraw()も下記のように追加します。
contract TokenExchange is Initializable {
  uint256 public rate;
  IERC20 public token;
  address public owner;

  function withdraw() public {
    //require(msg.sender == owner, "Address not allowed to call this function");
    msg.sender.transfer(address(this).balance);
  }

 // To be run during upgrade, ensuring it can never be called again
   function setOwner(address _owner) public {
     require(owner == address(0), "Owner already set, cannot modify!");
     owner = _owner;
   }

  // (existing functions not shown here for brevity)
}

お使いのエディタでアップグレードするコントラクトのファイルの保存を完了させてください。

コントラクトのアップグレードを実行します。対象のコントラクトをコントラクト名から選択してください。新しいownerには、ganacheのアカウント(0)を選択してください。
token-exchange4$ oz upgrade
? Pick a network development
Nothing to compile, all contracts are up to date.
All contracts are up to date
? Which instances would you like to upgrade? Choose by name
? Pick an instance to upgrade TokenExchange
? Call a function on the instance after upgrading it? Yes
? Select which function setOwner(_owner: address)
? _owner (address): 0xF987BFe4396FEf1f75488467c6845b74A496e381
Contract TokenExchange at 0xEdE0aB69fe6c4Ca6Df767ad19190eF9Fe465c405 is up to date.

新しく追加したwithdraw機能を実行してみます。
token-exchange4$ oz send-tx
? Pick a network development
? Pick an instance TokenExchange at 0xEdE0aB69fe6c4Ca6Df767ad19190eF9Fe465c405
? Select which function withdraw()
✓ Transaction successful. Transaction hash: 0x22db891168bb0b72a350bbac1260a3277173828a096daffd162d4ac5c78c2f86

これでTokenExchange.solのアップグレードに成功しました。ご関心のある方は、openzeppelin-contracts-ethereum-packageのStandaloneERC20もアップグレードしてみてください。

既存のERC20コントラクトをアップグレーダブルなERC20コントラクトにアップグレードする。


OpenZeppelinSDKでは、既存のERC20コントラクトをアップグレーダブルなコ
ントラクトにアップグレードすることが可能です。この技術を利用することで多くのERC20トークンをアップグレードすることができます。ただしこの機能のコントラクトは、まだ推奨されておらず最新のバージョンが対応していないのが現状です。なので、以降では、その戦略と要件の紹介に留めます。ご関心がある方は、最新のバージョンに対応していませんが、以下のドキュメントが参考になります。

Onboarding ERC20 tokens
https://docs.openzeppelin.com/sdk/2.6/erc20-onboarding

戦略

古いトークンコントラクトに対して、新しくアップグレーダブルなトークンコントラクトを用意して、古いトークンの保有者のみ新しいトークンの発行を許可することで、トークンのアップグレードを行います。

2つのコントラクトの間に入るコントラクトとして、SDKが用意する特別な機能のついたERC20Migrator.solというコントラクトを利用します。このコントラクトがトークン保有者の古いトークンのバーンと新しいトークンの発行を自動執行します。

ERC20Migrator.sol
https://github.com/OpenZeppelin/openzeppelin-contracts-ethereum-package/blob/master/contracts/drafts/ERC20Migrator.sol

要件

既に古いERC20コントラクトをデプロイ済みである必要があります。古いコントラクトは、frozenやpausedされておらず、トークン保有者は自由に移転が行える必要があります。

アップグレードガバナンスについて

概要


Proxy Contractは通常誰でもアクセスしてアップグレードを実行できては困るため、adminを設置します。しかしadminが自由にロジックコントラクトをアップグレードできると、そのコントラクトの信用性は著しく損なわれます。そこでProxy Contractのadminをマルチシグウォレットにし、複数人でアップグレードするかどうかについて、署名を行なって決めることで、adminの集権性を緩和することが可能です。

マルチシグウォレットの作成


概要

マルチシグウォレットのん作成方法のみ紹介します。今回利用する実装はConsenSys社がコントラクト部分を作成し、Gnosis社が完成させた最も安全と言われるマルチシグウォレットです。AragonやBancor、Golem、MysteriumDev、District0xなどが実際に利用しています。

参考
https://github.com/gnosis/MultiSigWallet

マルチシグウォレットのデプロイまで

実装をインストールします。
~$ git clone https://github.com/gnosis/MultiSigWallet.git

カレントディレクトを移動し、初期化を行います。
~$ cd MultiSigWallet
MultiSigWallet$ npm install
MultiSigWallet$ git submodule update --init --force --recursive --remote

サーバーを起動します。
MultiSigWallet$ npm start
Starting up http-server, serving src
Available on:
  http://127.0.0.1:5000
  http://192.168.30.52:5000
Hit CTRL-C to stop the server

Chromeなどのブラウザを開き、「http://127.0.0.1:5000」を検索して開きます。


MetaMaskを用意して、Ropstenテストネットワークに接続してください。ETHを以下のサイトよりFaucetで用意してください。

Ropsten Ethereum Faucet
https://faucet.ropsten.be/

ウォレットを作成します。「Wallets」の右側にある「Add」をクリックします。


「Create new wallet」がチェックして、「Next」をクリックします。


「Name」・「Required confirmations」・「Daily limit (ETH)」・Ownersをそれぞれ以下を参考にして埋めてください。アカウントはMetaMaskのもの利用してください。「Deploy with factory」をクリックします。


「Gas limit」・「Gas price (GWei)」・「Tx fees (ETH)」は自動で記入されるので、「Send transaction」をクリックします。


MetaMaskの画面が表示されるので、「確認」をクリックします。


トランザクションを含むブロックが承認されると、新しいウォレットが表示されます。


マルチシグウォレットを利用して、OpenZeppelinSDKのProxy ContractのAdminアドレスをマルチシグウォレットのコントラクトアドレスにすることによって、集権性の低いアップグレードアプローチをすることが可能になります。最新のSDKのバージョンのドキュメントでは、まだこの部分に完全に対応していないため、今回ご案内できるのは以上までとなります。ご関心がある方は以下の古いバージョンのドキュメントになりますが、チャレンジしてみてください。

Upgrades governance
https://docs.openzeppelin.com/sdk/2.6/upgrades-governance

Deploy your contracts to a public network
https://docs.openzeppelin.com/sdk/2.6/public-deploy

まとめ


4回の連載では、スマートコントラクトの「Upgradeability」について、解説してきました。Upgradeabilityは、確かにブロックチェーンの分散性を損なう手法ではありますが、長期的なDApps開発を行う上では、大事な開発手法です。実際に、有力なSTOプラットフォームである、PolymathやSecuritizeもProxy Contractを利用して、セキュリティトークンを発行しています。分散性を損なわないように工夫をしながら、これらの開発パターンを導入していくことで、Developer Experienceは改善され、より良いDAppsが開発できます。今回は、簡単な紹介しかできませんでしたが、Aragon OSもOpenZeppelin OS同様に有力なツールとなっていますので、ご関心のある方は、調べてみてください。
     

免責事項

本記事に掲載されている記事の内容につきましては、正しい情報を提供することに務めてはおりますが、提供している記事の内容及び参考資料からいかなる損失や損害などの被害が発生したとしても、弊社では責任を負いかねます。実施される際には、法律事務所にご相談ください。

技術・サービス・実装方法等のレビュー、その他解説・分析・意見につきましてはblock-chani.jp運営者の個人的見解です。正確性・正当性を保証するものではありません。本記事掲載の記事内容のご利用は読者様個人の判断により自己責任でお願いいたします。

     

コンセンサス・ベイス(株)とブロックチェーン事業を行なってみませんか?

当サイトを運営するコンセンサス・ベイス株式会社は、2015年設立の国内で最も古いブロックチェーン専門企業です。これまでに、大手企業の顧客を中心に、日本トップクラスのブロックチェーンの開発・コンサルティング実績があります。

ブロックチェーンに関わるビジネスコンサル・システム開発・教育・講演などご希望でしたら、お気軽にお問い合わせください。

     
     

ブロックチェーン学習に最適の書籍の紹介

図解即戦力 ブロックチェーンのしくみと開発がこれ1冊でしっかりわかる教科書

ブロックチェーン イーサリアムへの入り口 第二版 (ブロックチェーン技術書籍)

本書は、ブロックチェーン技術に興味を持ったエンジニアや、その仕組みを学び、自分の仕事に活かしたいビジネスパーソンを対象にして、ブロックチェーンのコア技術とネットワーク維持の仕組みを平易な言葉で解説しています。この本を読んだうえで、実際にコードを書くような専門書、ブロックチェーンビジネスの解説書を読むことで、理解度が飛躍的に高まるでしょう。(はじめにより)

イーサリアム(Ethereum)カテゴリの最新記事