2018.09.05

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 openzeppelin-zos@1.9.1
  $ 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.json

UpgradeableTokenのアドレスが出力されます。移行のための関数呼び出しの引数に使いますので、メモしておきます

新しいトークンに移行する

新しいトークンに移行するには、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 ] }

最後に

デモを通じて、コントラクトを移行するための一つのやり方を体験することができました。しかしながら実践ではこのほかにも、トークン保有者が実際に移行に踏み切ってもらえるよう、メリット、デメリットをしっかりと説明し、理解してもらうことが重要と考えます。

次世代システム研究室では、アプリケーション開発や設計を行うアーキテクト、またはブロックチェーンのエンジニアを募集しています。次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。