ERC20トークンをバージョンアップ可能なコントラクトに移行する
こんにちは、次世代システム研究室のN.O.です。
前の記事でコントラクトのバージョンアップの仕組みについて解説しましたが、バージョンアップの仕組みを採用しなかった既存のプロジェクトでも、途中からバージョンアップが必要になるケースがあるかと思います。そのようなケースを具体的に進めていく例として、ZeppelinOSにてERC20トークンをバージョンアップ可能なコントラクトに移行するガイドが公開されていますので、ご紹介したいと思います。
前提
まず大前提として、ブロックチェーンにデプロイ済みのコントラクトは修正することはできませんので、バージョンアップを可能にするには新しいコントラクトを作成し、移行する形となります。本ガイドではトークン保有者が自分の意志で新しいトークンに移行する、オプトインの形を採用しています。
この方式ではトークン保有者がメリットを感じなければ、移行をせずにそのまま古いトークンを保有し続けることも可能です。
なお、ZeppelinOSではトークン発行者が主体的に移行を行うマネージド形式のプランも公開していますので、こちらもご参考ください。
デモ
ガイドではデモも用意されているので、試してみます。このデモでは移行元となる古いほうのコントラクトを
LegacyToken
移行先となる新しいコントラクトを
UpgradeableToken
とします。UpgradeableTokenについて
UpgradeableTokenがどのようにトークンを移行するか、コントラクトのコードを見ていきます。移行のための関数であるmigrateは
contracts/openzeppelin-zos/MigratableERC20.sol
に実装されています。※MigratableERC20はOpenZeppelin standard libraryで提供される予定ですが、まだ正式版がリリースされていないため、デモ用プロジェクトに直接配置されているこのコードを使用します。
function migrate() public { uint256 amount = legacyToken.balanceOf(msg.sender); migrateToken(amount); } function migrateToken(uint256 _amount) public { migrateTokenTo(msg.sender, _amount); } function migrateTokenTo(address _to, uint256 _amount) public { _mint(_to, _amount); legacyToken.safeTransferFrom(msg.sender, BURN_ADDRESS, _amount); } function _mint(address _to, uint256 _amount) internal;トークン保有者がmigrate関数をコールすることで、新しいコントラクトでトークンが発行され、古いコントラクトのトークンを使用することができないようにします。
詳細には、
migrateTokenTo
関数で_mint
により新しいトークンが発行され、legacyToken.safeTransferFrom
でLegacyTokenをburnアドレスに転送することで、LegacyTokenのトークンが使用できないようにしています。なお、burnの際にUpgradeableTokenが旧トークンをburnアドレスに転送できるようにするため、トークン保有者は事前にapproveする必要があります。
UpgradeableTokenのコントラクトコード
contracts/MyUpgradeableToken.sol
では、上記のMigratableERC20を継承しています。import "./openzeppelin-zos/MigratableERC20.sol"; import "openzeppelin-zos/contracts/token/ERC20/DetailedERC20.sol"; import "openzeppelin-zos/contracts/token/ERC20/StandardToken.sol"; contract MyUpgradeableToken is MigratableERC20, StandardToken, DetailedERC20 {またここでは_mint関数をオーバーライドして定義しています。
function _mint(address _to, uint256 _amount) internal { require(_to != address(0)); totalSupply_ = totalSupply_.add(_amount); balances[_to] = balances[_to].add(_amount); emit Transfer(address(0), _to, _amount); }
準備
デモ用のプロジェクトをcloneし、npm install
します。$ git clone https://github.com/zeppelinos/erc20-opt-in-onboarding $ cd erc20-opt-in-onboarding $ npm install
移行元のトークンコントラクトをデプロイする
truffle develop
でテスト環境上にLegacyTokenをデプロイします。$ npx truffle develop truffle(develop)> compile truffle(develop)> owner = web3.eth.accounts[1] truffle(develop)> MyLegacyToken.new('MyToken', 'MTK', 18, { from: owner }).then(i => legacyToken = i) truffle(develop)> legacyToken.address '0xb9a219631aed55ebc3d998f17c3840b7ec39c0cc'後の手順で利用するので、LegacyTokenのコントラクトアドレスをメモしておきます。
ownerの残高は以下の通りです
truffle(develop)> legacyToken.balanceOf(owner) BigNumber { s: 1, e: 2, c: [ 100 ] }
プロジェクトをZeppelinOSで初期化する
デモ用のプロジェクトでZeppelinOSを使えるようにします。$ npm install --global zos $ zos init my-token-migration 1.0.0 $ zos link [email protected] $ zos add MyUpgradeableToken
新しいトークンコントラクトをデプロイする
最初にstandard libraryをデプロイします。$ zos push -n local --deploy-stdlib
--args
にLegacyTokenのアドレスをいれてupgradeableTokenをデプロイします$ zos create MyUpgradeableToken --args 0xb9a219631aed55ebc3d998f17c3840b7ec39c0cc -n local ... MyUpgradeableToken proxy: 0xb8549998430fa7772dbb87c7b4a05892a1231dd8 Successfully written zos.local.jsonUpgradeableTokenのアドレスが出力されます。移行のための関数呼び出しの引数に使いますので、メモしておきます
新しいトークンに移行する
新しいトークンに移行するには、LegacyTokenの全残高をUpgaradableTokenのアドレスにapproveし、UpgaradableTokenのmigarteをコールします。truffle(develop)> upgradeableToken = MyUpgradeableToken.at('0xb8549998430fa7772dbb87c7b4a05892a1231dd8') truffle(develop)> legacyToken.balanceOf(owner).then(b => balance = b) truffle(develop)> legacyToken.approve(upgradeableToken.address, balance, { from: owner }) truffle(develop)> upgradeableToken.migrate({ from: owner })LegacyTokenの残高が0になります。
truffle(develop)> legacyToken.balanceOf(owner) BigNumber { s: 1, e: 0, c: [ 0 ] }LegacyTokenはburnアドレスに送られていることが確認できます。
truffle(develop)> legacyToken.balanceOf('0x000000000000000000000000000000000000dead') BigNumber { s: 1, e: 2, c: [ 100 ] }ownerにUpgradeableTokenが付与されました。
truffle(develop)> upgradeableToken.balanceOf(owner) BigNumber { s: 1, e: 2, c: [ 100 ] }
最後に
デモを通じて、コントラクトを移行するための一つのやり方を体験することができました。しかしながら実践ではこのほかにも、トークン保有者が実際に移行に踏み切ってもらえるよう、メリット、デメリットをしっかりと説明し、理解してもらうことが重要と考えます。次世代システム研究室では、アプリケーション開発や設計を行うアーキテクト、またはブロックチェーンのエンジニアを募集しています。次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。