Blockchain
Ethereum
solidity
truffle
ERC20

ERC-20 Token Standard に準拠した独自トークンを自前実装する

ERC-20 Token Standard の知識を深めるため、Solidity + Truffle を使って独自トークンを自前実装してみました。

コードだけ見たい方はこちらからどうぞ。
https://github.com/kyrieleison/otoshidama

ERC (Ethereum Request for Comment) とは

ERC (Ethereum Request for Comment) は、Ethereum における技術仕様を記した文書です。これは IETF における RFC と同じ立ち位置にあたります。

ERC として公開された文書は単なる問題提起から仕様変更要求として移行する段階で、EIP (Ethereum Improvement Proposal) として採択されます。
EIP は Draft(検討中), Accepted(承認済) ステータスを経た後に、正式に Ethereum の仕様として採用されます。
Ethereum に採用された EIP は Final として最終版の仕様がまとめられます。Final ステータスの EIP の一覧は下記にあります。
Finalized EIPS

ERC-20 Token Standard とは

ERC-20 Token Standard は、トークンの仕様について記された20番目の ERC です。
2017年9月11日に Ethereum の仕様として採用され、Final ステータスの EIP として下記に仕様がまとめられています。
EIP-20 Token Standard

ERC-20 に準拠したトークンについては、OpenZeppelin というスマートコントラクト開発ライブラリを利用することが一番の近道です。
セキュリティを考慮したライブラリ SafeMath を利用したトークン作成が、StandardToken を継承するだけで実装できます。

OpenZeppelin を用いたトークン作成については、下記の記事が分かりやすいので参照してください。(本記事を書く上でも非常に参考にさせていただきました。ありがとうございます!)
Truffle で始める Ethereum 入門 - ERC20 トークンを作ってみよう

今回は理解を深めるため、OpenZeppelin の実装を参考にして、自前実装することにします。

Truffle でプロジェクトを作成する

スマートコントラクト開発フレームワークとして、Truffle を使用します。
Truffle をインストールし、 新規プロジェクトを作成します。

% yarn global add truffle
% truffle init

ERC-20 に準拠したトークンを実装する

年末年始が近づいているので、Otoshidama トークンを発行することにしました。
Otoshidama コントラクトに実装する RPC-20 のインターフェースは、トークン発行主体からの付与に必要なメソッドアカウント同士のトークンの転送とその承認に必要なメソッド の2つに分かれます。

まずは、トークン発行主体からアカウントへのトークン付与に必要なメソッドを実装します。

トークン発行主体からトークンを付与する

実装するメソッドとイベントは下記のとおりです。

contract Otoshidama {
  function totalSupply() constant returns (uint totalSupply);
  function balanceOf(address _owner) constant returns (uint balance);
  function transfer(address _to, uint _value) returns (bool success);
  event Transfer(address indexed _from, address indexed _to, uint _value);
}

NOTE: constant は Solidity 0.4.18 では pure もしくは view に変更されているので、注意してください。

以下、コントラクトの実装になります。
Truffle プロジェクトの contracts/Otoshidama.sol に配置します。

contracts/Otoshidama.sol
pragma solidity ^0.4.18;

contract Otoshidama {

  // トークンのメタ情報を設定します (一部のウォレットが利用するそうです)
  string public name = 'Otoshidama';
  string public symbol = 'OTD';
  uint public decimals = 18;

  // トークンの合計供給量を保持します
  uint public totalSupply;

  // 各アカウントの残高を保持します
  mapping(address => uint) public balances;

  /**
  * コントラクトを初期化します
  */
  function Otoshidama() public {
    uint _initialSupply = 10000;

    balances[msg.sender] = _initialSupply;
    totalSupply = _initialSupply;
  }

  /**
   * トークンの合計供給量を取得します
   * @return トークンの合計供給量を表す uint 値
   */
  function totalSupply() public view returns (uint) {
    return (totalSupply);
  }

  /**
  * _owner アカウントの残高を取得します
  * @param _owner 残高を取得するアカウントのアドレス
  * @return 渡されたアドレスが所有する残高を表す uint 値
  */
  function balanceOf(address _owner) public view returns (uint) {
    return (balances[_owner]);
  }

  /**
  * msg.sender が _to アカウントに指定した量のトークンを転送します
  * @param _to トークンを転送するアカウントのアドレス
  * @param _value 転送するトークンの量
  * @return トークンの転送が成功したかどうかを表す bool 値
  */
  function transfer(address _to, uint256 _value) public returns (bool) {
    require(_value <= balances[msg.sender]);

    balances[msg.sender] -= _value;
    balances[_to] += _value;

    Transfer(msg.sender, _to, _value);
    return true;
  }

  event Transfer(address indexed from, address indexed to, uint value);
}

このコントラクトをデプロイするためのマイグレーションファイルを migrations/2_deploy_otoshidama.js に配置します。

migrations/2_deploy_otoshidama.js
var Otoshidama = artifacts.require('./Otoshidama.sol');

module.exports = function(deployer) {
  deployer.deploy(Otoshidama);
};

コントラクトコードをコンパイルします。

% truffle compile
Compiling ./contracts/Otoshidama.sol...
Writing artifacts to ./build/contracts

truffle develop コマンドを実行して、ローカルマシンで Ethereum チェーンを起動します。

% truffle develop
Truffle Develop started at http://localhost:9545/

Accounts:
(0) 0x627306090abab3a6e1400e9345bc60c78a8bef57
(1) 0xf17f52151ebef6c7334fad080c5704d77216b732
(2) 0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef
(3) 0x821aea9a577a9b44299b9c15c88cf3087f3b5544
(4) 0x0d1d4e623d10f9fba5db95830f7d3839406c6af2
(5) 0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e
(6) 0x2191ef87e392377ec08e7c08eb105ef5448eced5
(7) 0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5
(8) 0x6330a553fc93768f612722bb8c2ec78ac90b3bbc
(9) 0x5aeda56215b167893e80b4fe645ba6d5bab767de

Mnemonic: candy maple cake sugar pudding cream honey rich smooth crumble sweet treat

truffle(develop)> 

コンソールが立ち上がるので、migrate コマンドでマイグレーションを実行し、コントラクトをデプロイします。

truffle(develop)> migrate
Compiling ./contracts/Otoshidama.sol...
Writing artifacts to ./build/contracts

Using network 'develop'.

Running migration: 1_initial_migration.js
  Replacing Migrations...
  ... 0x508d938371b3c4900b2eb993178ba29d4339ef38d3c832f3f5d32ce86dd5366a
  Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network...
  ... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts...
Running migration: 2_deploy_otoshidama.js
  Replacing Otoshidama...
  ... 0x8809af6c204f139586166bb05c545c0cefd1f00cb29fc9e5f4f8f382e76e3073
  Otoshidama: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
Saving successful migration to network...
  ... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0
Saving artifacts...

Otoshidama コントラクトがデプロイされました。
次に Otoshidama コントラクトを呼び出し、合計供給量や残高照会を行ってみます。

truffle(develop)> otoshidama = Otoshidama.at(Otoshidama.address)
truffle(develop)> otoshidama.totalSupply()
BigNumber { s: 1, e: 4, c: [ 10000 ] }
truffle(develop)> otoshidama.balanceOf(web3.eth.accounts[0])
BigNumber { s: 1, e: 4, c: [ 10000 ] }
truffle(develop)> otoshidama.balanceOf(web3.eth.accounts[1])
BigNumber { s: 1, e: 0, c: [ 0 ] }

マイグレーションは web3.eth.accounts[0] で行われるため、web3.eth.accounts[0] がトークン発行主体です。初期トークンとして発行した 10000 OTD を残高として持っています。もちろん他のアカウントの残高はゼロです。

では、トークン発行主体である web3.eth.accounts[0] から web3.eth.accounts[1] に 100 OTD
を付与してみます。

truffle(develop)> otoshidama.transfer(web3.eth.accounts[1], 100)
{ tx: '0xe388be17ba5efbc1cfd32d6f8c5d1f0df9bdbf05a68893fa6ee72af483b6d325',
  receipt:
   { transactionHash: '0xe388be17ba5efbc1cfd32d6f8c5d1f0df9bdbf05a68893fa6ee72af483b6d325',
     transactionIndex: 0,
     blockHash: '0x5ae676afa06fcd18f78dc70454fcb7b5a1c9f1097787ee5a319a2a8be7fb7f75',
     blockNumber: 5,
     gasUsed: 51097,
     cumulativeGasUsed: 51097,
     contractAddress: null,
     logs: [ [Object] ] },
  logs:
   [ { logIndex: 0,
       transactionIndex: 0,
       transactionHash: '0xe388be17ba5efbc1cfd32d6f8c5d1f0df9bdbf05a68893fa6ee72af483b6d325',
       blockHash: '0x5ae676afa06fcd18f78dc70454fcb7b5a1c9f1097787ee5a319a2a8be7fb7f75',
       blockNumber: 5,
       address: '0x345ca3e014aaf5dca488057592ee47305d9b3e10',
       type: 'mined',
       event: 'Transfer',
       args: [Object] } ] }
truffle(develop)> otoshidama.balanceOf(web3.eth.accounts[0])
BigNumber { s: 1, e: 3, c: [ 9900 ] }
truffle(develop)> otoshidama.balanceOf(web3.eth.accounts[1])
BigNumber { s: 1, e: 2, c: [ 100 ] }

web3.eth.accounts[1] に 100 OTD を付与することに成功しました。

アカウント同士のトークンの転送とその承認を行う

トークン発行主体がトークンを付与するだけであれば、ここまでの実装で十分です。

しかし、実際には付与されたトークンをアカウント同士で転送したい場合があると思います。そこで、さらに下記のメソッドとイベントを実装します。

contract Otoshidama {
  function transferFrom(address _from, address _to, uint _value) returns (bool success);
  function approve(address _spender, uint _value) returns (bool success);
  function allowance(address _owner, address _spender) constant returns (uint remaining);
  event Approval(address indexed _owner, address indexed _spender, uint _value);
}

以下、コントラクトに追加する実装です。

contracts/Otoshidama.sol
pragma solidity ^0.4.18;

contract Otoshidama {

  ...

  // 各アカウントによる転送を許可したトークンの量を保持します
  mapping(address => mapping (address => uint)) internal allowed;

  /**
   * _from アカウントから _to アカウントに指定した量のトークンを転送します
   * @param _from トークンの転送元アドレス
   * @param _to address トークンの転送先アドレス
   * @param _value 転送するトークンの量
   * @return トークンの転送が成功したかどうかを表す bool 値
   */
  function transferFrom(address _from, address _to, uint _value) public returns (bool) {
    require(_value <= balances[_from]);
    require(_value <= allowed[_from][msg.sender]);

    balances[_from] -= _value;
    balances[_to] += _value;
    allowed[_from][msg.sender] -= _value;

    Transfer(_from, _to, _value);
    return true;
  }

  /**
   * msg.sender が _spender アカウントが指定した量のトークンを転送することを承認します
   * @param _spender トークンを転送したいアドレス
   * @param _value 転送を許可するトークンの量
   */
  function approve(address _spender, uint _value) public returns (bool) {
    allowed[msg.sender][_spender] = _value;

    Approval(msg.sender, _spender, _value);
    return true;
  }

  /**
   * _spender アカウントが _owner アカウントから転送できるトークンの量を取得します
   * @param _owner トークンを所持するアドレス
   * @param _spender トークンを転送したいアドレス
   * @return _spender アカウントが _owner アカウントから転送可能なトークンの量を表す uint の値
   */
  function allowance(address _owner, address _spender) public view returns (uint) {
    return allowed[_owner][_spender];
  }

  event Approval(address indexed owner, address indexed spender, uint value);
}

追加に実装したコードをローカルマシンに起動した Ethereum チェーンにデプロイして、動作を確認してみます。

% truffle develop
Truffle Develop started at http://localhost:9545/

Accounts:
(0) 0x...

truffle(develop)> migrate
Compiling ./contracts/Otoshidama.sol...
Writing artifacts to ./build/contracts

Using network 'develop'.

Running migration: ...

まずは、トークン発行者 web3.eth.accounts[0] からアカウント web3.eth.accounts[1] に 100 OTD を付与します。

truffle(develop)> otoshidama = Otoshidama.at(Otoshidama.address)
truffle(develop)> otoshidama.transfer(web3.eth.accounts[1], 100)
{ tx: '0xe388be17ba5efbc1cfd32d6f8c5d1f0df9bdbf05a68893fa6ee72af483b6d325',
  receipt:
   { transactionHash: '0xe388be17ba5efbc1cfd32d6f8c5d1f0df9bdbf05a68893fa6ee72af483b6d325',
     transactionIndex: 0,
     blockHash: '0x4d324479d85da560bcd683a530c0bd8c2e0b0cbefef8174cc366f05c3b321ce3',
     blockNumber: 5,
     gasUsed: 51119,
     cumulativeGasUsed: 51119,
     contractAddress: null,
     logs: [ [Object] ] },
  logs:
   [ { logIndex: 0,
       transactionIndex: 0,
       transactionHash: '0xe388be17ba5efbc1cfd32d6f8c5d1f0df9bdbf05a68893fa6ee72af483b6d325',
       blockHash: '0x4d324479d85da560bcd683a530c0bd8c2e0b0cbefef8174cc366f05c3b321ce3',
       blockNumber: 5,
       address: '0x345ca3e014aaf5dca488057592ee47305d9b3e10',
       type: 'mined',
       event: 'Transfer',
       args: [Object] } ] }
truffle(develop)> otoshidama.balanceOf(web3.eth.accounts[1])
BigNumber { s: 1, e: 2, c: [ 100 ] }

次に、web3.eth.accounts[3] として、web3.eth.accounts[1] から web3.eth.accounts[2] に 10 OTD を転送したいとします。
メソッドの引数にオプションとして { from: <ADDRESS>} を指定すると、コンソールでそのアカウントとしてコマンドを実行できます。

web3.eth.accounts[3] として transferFrom メソッドを実行しても、権限が付与されていないためエラーになります。

truffle(develop)> otoshidama.transferFrom(web3.eth.accounts[1], web3.eth.accounts[2], 10, { from: web3.eth.accounts[3] })
Error: VM Exception while processing transaction: invalid opcode
    at Object.InvalidResponse 
    at ...

そこで approve メソッドで、トークン所有者である web3.eth.accounts[1] から web3.eth.accounts[3] に 10 OTD の転送を承認します。
承認結果は allowance メソッドで確認できます。

truffle(develop)> otoshidama.approve(web3.eth.accounts[3], 10, { from: web3.eth.accounts[1] })
{ tx: '0xd5577f7acee96c81020f989a205adf02ee14d1d74f689c7acf2a04c20cbdc256',
  receipt:
   { transactionHash: '0xd5577f7acee96c81020f989a205adf02ee14d1d74f689c7acf2a04c20cbdc256',
     transactionIndex: 0,
     blockHash: '0x2585dcb2423185f052614f11b14627f807c6d7f2239523490a2da91f69e74840',
     blockNumber: 8,
     gasUsed: 45192,
     cumulativeGasUsed: 45192,
     contractAddress: null,
     logs: [ [Object] ] },
  logs:
   [ { logIndex: 0,
       transactionIndex: 0,
       transactionHash: '0xd5577f7acee96c81020f989a205adf02ee14d1d74f689c7acf2a04c20cbdc256',
       blockHash: '0x2585dcb2423185f052614f11b14627f807c6d7f2239523490a2da91f69e74840',
       blockNumber: 8,
       address: '0x345ca3e014aaf5dca488057592ee47305d9b3e10',
       type: 'mined',
       event: 'Approval',
       args: [Object] } ] }
truffle(develop)> otoshidama.allowance(web3.eth.accounts[1], web3.eth.accounts[3])
BigNumber { s: 1, e: 1, c: [ 10 ] }

web3.eth.accounts[3] に 10 OTD の転送が承認されたので、再度 web3.eth.accounts[1] から web3.eth.accounts[2] へのトークンの転送を試みます。

truffle(develop)> otoshidama.transferFrom(web3.eth.accounts[1], web3.eth.accounts[2], 10, { from: web3.eth.accounts[3] })
{ tx: '0x7eb17984018f3341efa866f74f9efcf4a8a9cf4b30e6d0a99ac2a2a3914afa85',
  receipt:
   { transactionHash: '0x7eb17984018f3341efa866f74f9efcf4a8a9cf4b30e6d0a99ac2a2a3914afa85',
     transactionIndex: 0,
     blockHash: '0x274d0fdd774f825cf96cf344491027fe1921083ef39ed6a8770b1c06b36a5c90',
     blockNumber: 9,
     gasUsed: 43275,
     cumulativeGasUsed: 43275,
     contractAddress: null,
     logs: [ [Object] ] },
  logs:
   [ { logIndex: 0,
       transactionIndex: 0,
       transactionHash: '0x7eb17984018f3341efa866f74f9efcf4a8a9cf4b30e6d0a99ac2a2a3914afa85',
       blockHash: '0x274d0fdd774f825cf96cf344491027fe1921083ef39ed6a8770b1c06b36a5c90',
       blockNumber: 9,
       address: '0x345ca3e014aaf5dca488057592ee47305d9b3e10',
       type: 'mined',
       event: 'Transfer',
       args: [Object] } ] }
truffle(develop)> otoshidama.balanceOf(web3.eth.accounts[1])
BigNumber { s: 1, e: 1, c: [ 90 ] }
truffle(develop)> otoshidama.balanceOf(web3.eth.accounts[2])
BigNumber { s: 1, e: 1, c: [ 10 ] }
truffle(develop)> otoshidama.allowance(web3.eth.accounts[1], web3.eth.accounts[3])
BigNumber { s: 1, e: 0, c: [ 0 ] }

10 OTD が転送され、残高が更新されました。また、web3.eth.accounts[3] に渡された転送権限は転送したトークン量だけ減算され、ゼロに戻りました。

approveallowance というこれらのメソッドが標準仕様として実装されているのは、恐らくトークンの転送を第三者 (取引所やウォレット) が行うケースがあるからだと思われます。

参考文献