ERC1167(Minimal Proxy Contract)で実装するERC20のFacrotyコントラクト

はじめに

ERC1167とは、最小限のコードで安価に簡単にコントラクトをコピーするための規格です。
全ての処理をコピー元コントラクトにdelegateCallする形のProxy Contractであるため、生成されるコントラクトは45バイトととても小さくなっています。
すでにファイナライズもされています。
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1167.md

このERC1167を利用し、独自トークンを発行するための規格であるERC20を生成して管理するFacrotyコントラクトを実装してみます。

実装ポイント

ERC1167を利用するには通常のコントラクトとは異なり実装時に気をつけるポイントが何点かあります。

1. constractorを使用できない

EIP1167のProxy Contractはバイトコードをコピーして新しくコントラクトを生成するため、コピーされた各コントラクトごとにconstractorで初期値を設定することができません。初期値の設定が必要な場合は、initializerを用意してそこで行います。

2. function外で変数に値をセットできない

上記1と同じ理由で、function外で変数に値をセットすることもできません。
対応策としては、下記の2つです。
1. constantを利用する。
2. initializerでセットする。

3. 消費gasが増加する

コピーで作られたコントラクトはコピー元のfunctionを全てdelegateCallで呼びだすため、通常より少し消費gasが増加します。
その代わりに、コントラクトを新たにデプロイするのと比べて、コントラクトコピー時の消費gasはとても小さくなっています。

4. コピー元コントラクトへの依存

コピー元のコントラクトをdelegateCallで呼びだすため、コピー元でselfdestructした場合やバグがあった場合などに全てのコントラクトが影響を受け、動作しなくなる可能性があります。

これらの注意点や危険性から、OpenZeppelinではプロジェクトの設計方針と異なるということで採用が見送られています。
https://github.com/OpenZeppelin/openzeppelin-contracts/issues/1162

実装手順

1. ワークディレクトリの作成

下記コマンドでtruffleを利用するためのワークディレクトリを作成します。

$ mkdir erc1167
$ cd erc1167/
$ npm i -g truffle
$ truffle init
$ npm init

2.ERC20規格のトークンコントラクトの作成

OpenZeppelinを利用するので、npm installします。

$ npm i @openzeppelin/contracts --save-dev

openzeppelinのコントラクトをimportしてERC20ベースのBaseToken.solを作成します。
通常であればトークン名やシンボルを管理するにはopenzeppelinのERC20Detailed.solを継承すれば良いのですが、constructorで各値を設定する仕様になっているため、下記のようにinitializeファンクションで設定するように変更します。

// BaseToken.sol
pragma solidity ^0.5.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20Mintable.sol";

contract BaseToken is ERC20, ERC20Mintable {
  string private _name;
  string private _symbol;
  uint8 private _decimals;
  bool public initialized;

  function initialize(string memory name, string memory symbol, uint8 decimals) public {
    require(!initialized,"already initialized");
    _name = name;
    _symbol = symbol;
    _decimals = decimals;
    initialized = true;
  }

  function name() public view returns (string memory) {
      return _name;
  }

  function symbol() public view returns (string memory) {
      return _symbol;
  }

  function decimals() public view returns (uint8) {
      return _decimals;
  }
}

3. ERC1167を用いたFacrotyコントラクトの作成

まずはERC1167のサンプル実装をコピーします。
https://github.com/optionality/clone-factory/blob/master/contracts/CloneFactory.sol

// CloneFactory.sol
pragma solidity ^0.5.0;

contract CloneFactory {

  function createClone(address target) internal returns (address result) {
    bytes20 targetBytes = bytes20(target);
    assembly {
      let clone := mload(0x40)
      mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
      mstore(add(clone, 0x14), targetBytes)
      mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
      result := create(0, clone, 0x37)
    }
  }

  function isClone(address target, address query) internal view returns (bool result) {
    bytes20 targetBytes = bytes20(target);
    assembly {
      let clone := mload(0x40)
      mstore(clone, 0x363d3d373d3d3d363d7300000000000000000000000000000000000000000000)
      mstore(add(clone, 0xa), targetBytes)
      mstore(add(clone, 0x1e), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)

      let other := add(clone, 0x40)
      extcodecopy(query, other, 0, 0x2d)
      result := and(
        eq(mload(clone), mload(other)),
        eq(mload(add(clone, 0xd)), mload(add(other, 0xd)))
      )
    }
  }
}

次に作成したCloneFactoryを継承したTokenFactoryを作成します。
TokenFactoryには、コピーしてtokenを新たに生成するcreateTokenファンクションと、生成されたtokenのアドレスを取得するtokenOfというファンクションを用意します。
コピー元となるトークンのアドレスはconstructorでセットします。

// TokenFactory.sol
pragma solidity ^0.5.0;

import "@openzeppelin/contracts/ownership/Ownable.sol";
import './BaseToken.sol';
import './CloneFactory.sol';

contract TokenFactory is CloneFactory, Ownable {
  address public baseToken;
  address[] public tokens;

  constructor (address _baseToken) public {
    baseToken = _baseToken;
  }

  function createToken(string memory name, string memory symbol, uint8 decimals) public onlyOwner {
    address clone = createClone(baseToken);
    BaseToken(clone).initialize(name, symbol, decimals);
    tokens.push(clone);
  }

  function tokenOf(uint256 tokenId) public view returns (address) {
      return tokens[tokenId];
  }
}

4.デプロイして動作確認をする。

マイグレーションファイルを作成します。

$ truffle create migration tokenFactory

作成されたマイグレーションファイルを下記のように動作するよう修正します。
1. コピー元となるBaseTokenをデプロイ
2. TokenFactoryをデプロイ
3. トークンを2つ作成して、動作確認

// xxxxxxxxx_token_factory.js
const BaseToken = artifacts.require("BaseToken");
const TokenFactory = artifacts.require("TokenFactory");

module.exports = async function(deployer) {
  deployer.then(function () {
    // 1. deploy original BaseToken
    return deployer.deploy(BaseToken);
  }).then(function () {
    // 2. deploy TokenFactory
    return deployer.deploy(TokenFactory, BaseToken.address);
  }).then(async function (tokenFactory) {
    // 3. check each functions
    await tokenFactory.createToken("Hoge Token1", "HT1", 0);
    await tokenFactory.createToken("Hoge Token2", "HT2", 4);
    const tokenAddress1 = await tokenFactory.tokenOf(0);
    const tokenAddress2 = await tokenFactory.tokenOf(1);
    const token1 = await BaseToken.at(tokenAddress1);
    const token2 = await BaseToken.at(tokenAddress2);

    console.log("[token1] address:", tokenAddress1);
    console.log("[token1] name:", await token1.name());
    console.log("[token1] symbol:", await token1.symbol());
    console.log("[token1] decimals:", await token1.decimals());
    console.log("[token2] address:", tokenAddress2);
    console.log("[token2] name:", await token2.name());
    console.log("[token2] symbol:", await token2.symbol());
    console.log("[token2] decimals:", await token2.decimals());
  });
};

下記コマンドを実行し、下記のようなログが出力され正常終了していれば成功です。

$ truffle develop
truffle(develop)> migrate

  :

[token1] address: 0xA08b9780F9b336fD6B34f5F9bBb53eC1a400341f
[token1] name: Hoge Token1
[token1] symbol: HT1
[token1] decimals: <BN: 0>
[token2] address: 0x930e2090e07F77C769312Ad2072F4245FFC4feE8
[token2] name: Hoge Token2
[token2] symbol: HT2
[token2] decimals: <BN: 4>

   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:           0.0573423 ETH

Summary
=======
> Total deployments:   3
> Final cost:          0.06261712 ETH

まとめ

これでコントラクトのコピーが簡単かつ安価にできるようになるため、アプリケーション内で複数のERC20トークンを発行するような実装を気軽に導入できるようになったと思います。

特にgas代行するアーキテクチャの場合には、Gasリミットの問題からサーバーサイドでコントラクトのデプロイ&初期設定を複数のトランザクションに分けて実行することがあるため、ERC1167を用いて1トランザクションで実行できるというのはシステムの安定性という観点からもとても有効な実装であると感じました。
今後は積極的にERC1167を導入していきたいです。

The following two tabs change content below.
Akihiro Tanaka

Akihiro Tanaka

Smart Contract Engineer
Since 2009, I have been a software engineer at Accenture for 9 years, managing, designing, and developing many services, mainly web and mobile apps.
In 2013, I met Bitcoin and started to work on blockchain-related development in 2018, developing an entertainment DApp for underground idols, a blockchain analysis tool, and an STO platform.
Currently, I am working as a Smart Contract Engineer at Secured Finance, developing a DeFi product.

WEB: https://tanakas.org/