仮想子猫経済を支える ERC721

この記事は ブロックチェーン Advent Calendar 2017 の 18 日目の記事です。この記事では、近頃 Ethereum 界隈で人気を博している Ðapp「CryptoKitties」が利用している ERC721(※まだドラフト段階なので注意)というトークンの仕様について紹介するとともに、実装・動作確認を通して ERC721 に対する理解を深めていこうと思います。

CryptoKitties について

この記事は CryptoKitties について説明するのが目的ではないので、ざっくりと紹介だけ。

まず、語弊を覚悟で一言でイメージを伝えると

Blockchain 上で育てるたまごっち

みたいなものです(え、たまごっちを知らないだって。。。?なんてこった。。。)。

これが、近頃 Ethereum 界隈で一大ブームを巻き起こしたというわけです。

ref. イーサリアム上で「仮想子猫」育成ゲームが人気、取引の4%占める

上記の記事にもさらっと目を通していただければ CryptoKitties のイメージはだいたい掴んでもらえるかと思います。一応、公式サイト に記載されている紹介文も引用しておきます。

CryptoKitties is a game centered around breedable, collectible, and oh-so-adorable creatures we call CryptoKitties! Each cat is one-of-a-kind and 100% owned by you; it cannot be replicated, taken away, or destroyed.

ざっくり訳すと、

CryptoKitties は、繁殖できて収集もできる、かわいくてかわいくてたまらない生き物、CryptoKitty を中心としたゲームです!CryptoKitty の所有権は 100% あなたのものです。複製されることも、奪取されることも、破壊されることもありません。

という感じでしょうか。

「所有権は 100% あなたのもの」とは言っていますが、コントラクトを見る限り、いくつかの管理操作権限(CryptoKitty の取引を停止する権利など)はゲーム開発側が保有しているようです。気になる方は 実際のコントラクト をご覧ください。

また、What’s the big deal? という問いに対する回答は以下のようになっています。

CryptoKitties is one of the world’s first games to be built on blockchain technology - the same breakthrough that makes things like Bitcoin and Ethereum possible. Bitcoin and ether are cryptocurrencies but CryptoKitties are cryptocollectibles. You can buy, sell, or trade your CryptoKitty like it was a traditional collectible, secure in the knowledge that blockchain will track ownership securely.

これもざっくり訳すと、

CryptoKitties は、Blockchain 技術を基に構築された世界初のゲームの 1 つです。このブレイクスルーによって、Bitcoin や Ethereum と同等のことが可能となります。Bitcoin や Ether は仮想通貨ですが、CryptoKitties は仮想収集品です。従来の収集品と同様、CryptoKitty は購入・売却・交換することができます。また、その所有権は Blockchain によって監視されるため、安全です。

という感じでしょうか。

もっと詳しく知りたい方は、公式の FAQ を参照したり、MetaMask をインストールしたブラウザで実際にゲームをプレイしてみるとよいかと思います。

仮想子猫の正体について

さて。この記事の本題、それは、

仮想子猫 CryptoKitty の正体

です。

はい。これ、実はトークンなんですね。ただ、現在最も利用されているであろう ERC20 準拠のトークンとは少し違う、

fungibility がないトークン(Non-Fungible Token:NFT)

となります

「fungibility とはなんぞや?」という方は以下の記事に目を通していただくのがよいかと思います(@indiv_0110 さん、いつも良質な記事をありがとうございます 🙏)。

ref. 匿名通貨と Fungibility(代替性)

CryptoKitties で利用されている NFT は、アドレスに対する保有量をベースに管理される ERC20 準拠のトークンと異なり、

1 つ 1 つのトークンにアイデンティティがあり、保有者が割り当てられている

ようなイメージです。CryptoKitty 1 匹 1 匹がアイデンティティを持っていて、その飼い主がいるというのは、イメージしやすいかと思います。

そして、この NFT の仕様が、この記事で着目したい ERC721 となります。

ERC721 について

前述した通り、ERC721 は CryptoKitties でも使われている NFT の 1 仕様です。現在、これを ERC20 のように標準化しようという動きがあり、以下の GitHub issue にて議論がなされています。

ref. ERC: Non-fungible Token Standard

つまり、執筆時点ではまだ正式な仕様ではないということです。

ということで、この記事では CryptoKitties で実際に使われたバージョンの仕様を利用して説明していこうと思います。最終版のインターフェースは異なったものになるかと思いますので、今回は

NFT がどんな風に実現されているのかをざっくりと理解すること

に主眼を置きながら進めます。

まず、インターフェースを定義しているコントラクトを CryptoKitties で実際に使われているコントラクト から抽出してみます。ERC20 のインターフェースをご存知の方は、これを見るだけでどんなものなのかだいたい把握できるかと思います。

pragma solidity ^0.4.18;

/// @title Interface for contracts conforming to ERC-721: Non-Fungible Tokens
/// @author Dieter Shirley <dete@axiomzen.co> (https://github.com/dete)
contract ERC721 {
    // Required methods
    function totalSupply() public view returns (uint256 total);
    function balanceOf(address _owner) public view returns (uint256 balance);
    function ownerOf(uint256 _tokenId) external view returns (address owner);
    function approve(address _to, uint256 _tokenId) external;
    function transfer(address _to, uint256 _tokenId) external;
    function transferFrom(address _from, address _to, uint256 _tokenId) external;

    // Events
    event Transfer(address from, address to, uint256 tokenId);
    event Approval(address owner, address approved, uint256 tokenId);

    // Optional
    // function name() public view returns (string name);
    // function symbol() public view returns (string symbol);
    // function tokensOfOwner(address _owner) external view returns (uint256[] tokenIds);
    // function tokenMetadata(uint256 _tokenId, string _preferredTransport) public view returns (string infoUrl);

    // ERC-165 Compatibility (https://github.com/ethereum/EIPs/issues/165)
    function supportsInterface(bytes4 _interfaceID) external view returns (bool);
}

ERC20 っぽいとはいえ、見慣れない名前の function もいくつかあるようですので、上から 1 つ 1 つ見ていきましょう、、と言いたいところなのですが、インターフェースだけで説明するよりも実装が伴っていた方がイメージしやすそうだなと思ったので、以下、自分のサンプル実装をベースに説明していこうと思います。

なお、今回は tokenMetadata function は実装しません。この function は、IPFS 上などにある外部リソースをメタデータとしてトークンと紐付けるためのものなのですが、この仕様だけで結構ボリューミーな説明が必要になってしまうため、今回は省略することとします。

ERC721 トークンを実装してみる

CryptoKitties で実際に使われたバージョンの ERC721 のサンプル実装がこちらです。

CryptoKitties のソースコードをそのまま持ってきて説明してもよかったのですが、ERC721 の初歩的な理解には必要ないであろう CryptoKitties 特有の処理がそれなりに入ってきてしまうため、自分の思う最小限を MyNonFungibleToken として実装してみました。とはいえ、CryptoKitties の実装は大いに参考にしています。

ソースコードはそんなに長くないので、とりあえず全部貼ってしまおうと思います。

pragma solidity ^0.4.18;

import "./ERC721.sol";

contract MyNonFungibleToken is ERC721 {
  /*** CONSTANTS ***/

  string public constant name = "MyNonFungibleToken";
  string public constant symbol = "MNFT";

  bytes4 constant InterfaceID_ERC165 =
    bytes4(keccak256('supportsInterface(bytes4)'));

  bytes4 constant InterfaceID_ERC721 =
    bytes4(keccak256('name()')) ^
    bytes4(keccak256('symbol()')) ^
    bytes4(keccak256('totalSupply()')) ^
    bytes4(keccak256('balanceOf(address)')) ^
    bytes4(keccak256('ownerOf(uint256)')) ^
    bytes4(keccak256('approve(address,uint256)')) ^
    bytes4(keccak256('transfer(address,uint256)')) ^
    bytes4(keccak256('transferFrom(address,address,uint256)')) ^
    bytes4(keccak256('tokensOfOwner(address)'));


  /*** DATA TYPES ***/

  struct Token {
    address mintedBy;
    uint64 mintedAt;
  }


  /*** STORAGE ***/

  Token[] tokens;

  mapping (uint256 => address) public tokenIndexToOwner;
  mapping (address => uint256) ownershipTokenCount;
  mapping (uint256 => address) public tokenIndexToApproved;


  /*** EVENTS ***/

  event Mint(address owner, uint256 tokenId);


  /*** INTERNAL FUNCTIONS ***/

  function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) {
    return tokenIndexToOwner[_tokenId] == _claimant;
  }

  function _approvedFor(address _claimant, uint256 _tokenId) internal view returns (bool) {
    return tokenIndexToApproved[_tokenId] == _claimant;
  }

  function _approve(address _to, uint256 _tokenId) internal {
    tokenIndexToApproved[_tokenId] = _to;

    Approval(tokenIndexToOwner[_tokenId], tokenIndexToApproved[_tokenId], _tokenId);
  }

  function _transfer(address _from, address _to, uint256 _tokenId) internal {
    ownershipTokenCount[_to]++;
    tokenIndexToOwner[_tokenId] = _to;

    if (_from != address(0)) {
      ownershipTokenCount[_from]--;
      delete tokenIndexToApproved[_tokenId];
    }

    Transfer(_from, _to, _tokenId);
  }

  function _mint(address _owner) internal returns (uint256 tokenId) {
    Token memory token = Token({
      mintedBy: _owner,
      mintedAt: uint64(now)
    });
    tokenId = tokens.push(token) - 1;

    Mint(_owner, tokenId);

    _transfer(0, _owner, tokenId);
  }


  /*** ERC721 IMPLEMENTATION ***/

  function supportsInterface(bytes4 _interfaceID) external view returns (bool) {
    return ((_interfaceID == InterfaceID_ERC165) || (_interfaceID == InterfaceID_ERC721));
  }

  function totalSupply() public view returns (uint256) {
    return tokens.length;
  }

  function balanceOf(address _owner) public view returns (uint256) {
    return ownershipTokenCount[_owner];
  }

  function ownerOf(uint256 _tokenId) external view returns (address owner) {
    owner = tokenIndexToOwner[_tokenId];

    require(owner != address(0));
  }

  function approve(address _to, uint256 _tokenId) external {
    require(_owns(msg.sender, _tokenId));

    _approve(_to, _tokenId);
  }

  function transfer(address _to, uint256 _tokenId) external {
    require(_to != address(0));
    require(_to != address(this));
    require(_owns(msg.sender, _tokenId));

    _transfer(msg.sender, _to, _tokenId);
  }

  function transferFrom(address _from, address _to, uint256 _tokenId) external {
    require(_to != address(0));
    require(_to != address(this));
    require(_approvedFor(msg.sender, _tokenId));
    require(_owns(_from, _tokenId));

    _transfer(_from, _to, _tokenId);
  }

  function tokensOfOwner(address _owner) external view returns (uint256[]) {
    uint256 balance = balanceOf(_owner);

    if (balance == 0) {
      return new uint256[](0);
    } else {
      uint256[] memory result = new uint256[](balance);
      uint256 maxTokenId = totalSupply();
      uint256 idx = 0;

      uint256 tokenId;
      for (tokenId = 1; tokenId <= maxTokenId; tokenId++) {
        if (tokenIndexToOwner[tokenId] == _owner) {
          result[idx] = tokenId;
          idx++;
        }
      }
    }

    return result;
  }


  /*** OTHER EXTERNAL FUNCTIONS ***/

  function mint() external returns (uint256) {
    return _mint(msg.sender);
  }

  function getToken(uint256 _tokenId) external view returns (address mintedBy, uint64 mintedAt) {
    Token memory token = tokens[_tokenId];

    mintedBy = token.mintedBy;
    mintedAt = token.mintedAt;
  }
}

さて、ここからは上記のコードを追いながら説明していこうと思うのですが、1 つ注意点として、

以降の説明において、単に「ERC721」と表記した場合、「CryptoKitties で実際に使われたバージョンの ERC721」を指す

こととします。ご承知ください。

まず、以下のようにいくつか constant を定義しています。

string public constant name = "MyNonFungibleToken";
string public constant symbol = "MNFT";

bytes4 constant InterfaceID_ERC165 =
  bytes4(keccak256('supportsInterface(bytes4)'));

bytes4 constant InterfaceID_ERC721 =
  bytes4(keccak256('name()')) ^
  bytes4(keccak256('symbol()')) ^
  bytes4(keccak256('totalSupply()')) ^
  bytes4(keccak256('balanceOf(address)')) ^
  bytes4(keccak256('ownerOf(uint256)')) ^
  bytes4(keccak256('approve(address,uint256)')) ^
  bytes4(keccak256('transfer(address,uint256)')) ^
  bytes4(keccak256('transferFrom(address,address,uint256)')) ^
  bytes4(keccak256('tokensOfOwner(address)'));

namesymbol は ERC20 でもお馴染みですね。「Bitcoin」と「BTC」みたいなものです。なお、ERC721 では optional なインターフェースとなっているので、これらの定義は必須ではありません。

また、ERC721 の要件には ERC165 に準拠することも含まれているため、ERC165 と ERC721 のインターフェース ID も定義しています。ERC165 は、ざっくり言うと、コントラクトがどんなインターフェースを実装しているのかを確認できるようにするためのインターフェースです。ただ、ERC165 もまだ正式な仕様として認められているわけではないので、ここでは深入りしません。

次は、Token struct の定義です。

struct Token {
  address mintedBy;
  uint64 mintedAt;
}

これがトークンの実体となります。CryptoKitties においては、これにあたる存在が Kitty として以下のように定義されています(コメントなどは省きました)。これが、仮想子猫 CryptoKitty の正体です。

struct Kitty {
  uint256 genes;
  uint64 birthTime;
  uint64 cooldownEndBlock;
  uint32 matronId;
  uint32 sireId;
  uint32 siringWithId;
  uint16 cooldownIndex;
  uint16 generation;
}

今回の実装では、誰でもトークンを発行できるようにしたので、シンプルに、トークン発行者のアドレスとそのトークンが発行されたブロックのタイムスタンプを保持するようにしています。どんなトークンにするかは ERC721 で制限されてはいないので、required なインターフェースを満たせるのであれば自由に実装することができます。

次は、ストレージの役割を担う array や mapping の定義です。

Token[] tokens;

mapping (uint256 => address) public tokenIndexToOwner;
mapping (address => uint256) ownershipTokenCount;
mapping (uint256 => address) public tokenIndexToApproved;

tokens は、その名の通り、全てのトークンを保持する array であり、そのインデックスをトークンの ID として扱います。これは CtyptoKitties においても同様です。

そして、このトークン ID に対応するトークンの保有者(アドレス)を管理するのが tokenIndexToOwner であり、各アドレスが保有するトークンの合計数を管理するのが ownershipTokenCount です。

また、ERC721 準拠のトークンも認可(保有者以外にトークンの譲渡を許可する行為)が可能なため、誰が認可されているのかをトークンごとに管理する必要があります。この管理を行っているのが tokenIndexToApproved となります。今回は簡単なサンプル実装ということで、1 トークンに対して 1 アドレスしか認可できないようにしています。

なお、これらストレージの実装も ERC721 で制限されてはいないので、required なインターフェースを満たせるのであれば自由に実装することができます(今回は CryptoKitties と同じように実装しています)。

次は、event の定義です。

event Mint(address owner, uint256 tokenId);

Mint event は ERC721 とは関係ありませんが、トークン発行時に発行する event として追加で定義しています。

Transfer event と Approval event は ERC721 における定義をそのまま使うので、ここでは定義していません。それぞれ、トークンの譲渡時と認可時に発行されます。

さて。ここからは function の実装部分に入っていくのですが、1 つずつ説明していくのも冗長ですし、上述したストレージの仕様を加味すれば理解に苦しむ function はないと思いますので、全ての function の概要をざっとまとめるに留めようかと思います。

internal functions

definition return description
_owns(address _claimant, uint256 _tokenId) internal view bool _claimant で指定したアドレスが、_tokenId に対応するトークンを保有しているかどうかを確認する
_approvedFor(address _claimant, uint256 _tokenId) internal view bool _claimant で指定したアドレスが、_tokenId に対応するトークンについて認可されているかどうかを確認する
_approve(address _to, uint256 _tokenId) internal - _tokenId に対応するトークンついて、_to で指定したアドレスを認可する
_transfer(address _from, address _to, uint256 _tokenId) internal - _tokenId に対応するトークンを、_from で指定したアドレスから _to で指定したアドレスに譲渡する
_mint(address _owner) internal uint256 新しくトークンを発行し、_owner で指定したアドレスに保有させる(返り値はトークン ID)

ERC721 implementation

definition return description
supportsInterface(bytes4 _interfaceID) external view bool 指定された _interfaceID に対応するインターフェースが実装されているかどうかを確認する(今回は ERC165 と ERC721 を実装)
totalSupply() public view uint256 トークンの総数を取得する
balanceOf(address _owner) public view uint256 _owner で指定したアドレスが保有するトークンの合計数を取得する
ownerOf(uint256 _tokenId) external view address _tokenId に対応するトークンを保有するアドレスを取得する
approve(address _to, uint256 _tokenId) external - msg.sender が保有する _tokenId に対応するトークンについて、_to で指定したアドレスを認可(_approve)する
transfer(address _to, uint256 _tokenId) external - msg.sender が保有する _tokenId に対応するトークンを、_to で指定したアドレスに譲渡(_transfer)する
transferFrom(address _from, address _to, uint256 _tokenId) external - msg.sender が認可された _tokenId に対応するトークンを、_from で指定したアドレスから _to で指定したアドレスに譲渡(_transfer)する
tokensOfOwner(address _owner) external view uint256[] _owner で指定したアドレスが保有するトークンのトークン ID 一覧を取得する

other external functions

definition return description
mint() external uint256 新しくトークンを発行(_mint)し、msg.sender に保有させる
getToken(uint256 _tokenId) external view address, uint64 _tokenId に対応するトークンの情報を取得する(返り値は、mintedBymintedAt

ERC20 との大きな違いは、

各トークンがアイデンティティを持っているため、各トークンに対して操作を行う際にトークン ID を指定してあげる必要がある

という点となります。この点を理解すれば、特に難しいことはないかと思います。

なお、今回は ERC721 と関係のない external function として、mintgetToken を実装しています。mint については、これがないとトークンを発行する術がないため、実装しています。getToken は必須ではないのですが、利便性を加味して実装しています。

その他の注意点として、tokensOfOwner という非常に危険な香りのする function があります。なぜなら、CryptoKitties の実装に習い、トークンの総数分ループをぶん回すという漢気溢れる仕様になっているからです。これについては、CryptoKitties のソースコードでも以下のようにコメントされています。結果を格納する配列は memory を指定して定義していますので、その点についても注意が必要となります。

This method MUST NEVER be called by smart contract code. First, it’s fairly expensive (it walks the entire Kitty array looking for cats belonging to owner), but it also returns a dynamic array, which is only supported for web3 calls, and not contract-to-contract calls.

これに関する仕様をどうするかは、ERC721 に対応する GitHub issue でも議論されており、このコメント などから、その様子を伺うことができます。

実装したコントラクトを動かしてみる

ここでは、紹介した ERC721 のサンプル実装について、truffle を用いて簡単な動作確認を行ってみようと思います。なお、コントラクトがデプロイ済みの状態を前提とします。

まずはコントラクトオブジェクトをつくります。

truffle(development)> token = MyNonFungibleToken.at(MyNonFungibleToken.address)
...

namesymbol を確認してみます。

truffle(development)> token.name()
'MyNonFungibleToken'
truffle(development)> token.symbol()
'MNFT'

デプロイしたてなので、totalSupply もまだ 0 です。

truffle(development)> token.totalSupply()
BigNumber { s: 1, e: 0, c: [ 0 ] }

試しに 3 つくらいトークンを発行してみます。

truffle(development)> token.mint()
...
truffle(development)> token.mint()
...
truffle(development)> token.mint()
...

想定通り、totalSupply は 3 になっています。

truffle(development)> token.totalSupply()
BigNumber { s: 1, e: 0, c: [ 3 ] }

mint を実行したアカウントが保有するトークン一覧は以下のようになっており、ID が 0、1、2 のトークン 3 つが保有されていることがわかります。

truffle(development)> token.tokensOfOwner(web3.eth.accounts[0])
[ BigNumber { s: 1, e: 0, c: [ 1 ] },
  BigNumber { s: 1, e: 0, c: [ 2 ] },
  BigNumber { s: 1, e: 0, c: [ 0 ] } ]

当然、保有するトークンの合計数は 3 です。

truffle(development)> token.balanceOf(web3.eth.accounts[0])
BigNumber { s: 1, e: 0, c: [ 3 ] }

各トークンの情報も確認してみます。発行者のアドレスと、発行されたブロックのタイムスタンプが閲覧できます。

truffle(development)> token.getToken(0)
[ '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  BigNumber { s: 1, e: 9, c: [ 1513623273 ] } ]
truffle(development)> token.getToken(1)
[ '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  BigNumber { s: 1, e: 9, c: [ 1513623279 ] } ]
truffle(development)> token.getToken(2)
[ '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  BigNumber { s: 1, e: 9, c: [ 1513623280 ] } ]

なお、0x627306090abab3a6e1400e9345bc60c78a8bef57 は、web3.eth.accounts[0] です。

次に、ID:1 のトークンを web3.eth.accounts[1] に譲渡してみます。

truffle(development)> token.transfer(web3.eth.accounts[1], 1)
...

結果は後ほど確認するとして、次に ID:2 のトークンを web3.eth.accounts[2] に譲渡してみます。今回は approve を利用します。

truffle(development)> token.approve(web3.eth.accounts[2], 2)
...
truffle(development)> token.transferFrom(web3.eth.accounts[0], web3.eth.accounts[2], 2, {from: web3.eth.accounts[2]})
...

transferFrom は認可された側が実行する必要がありますので、{from: web3.eth.accounts[2]} を指定しています。

譲渡結果を確認してみます。まずは各アカウントが保有するトークンの合計数です。想定通り、1 つずつトークンを保有しています。

truffle(development)> token.balanceOf(web3.eth.accounts[0])
BigNumber { s: 1, e: 0, c: [ 1 ] }
truffle(development)> token.balanceOf(web3.eth.accounts[1])
BigNumber { s: 1, e: 0, c: [ 1 ] }
truffle(development)> token.balanceOf(web3.eth.accounts[2])
BigNumber { s: 1, e: 0, c: [ 1 ] }

次に、各アカウントが保有するトークン一覧です。これも想定通りです。

truffle(development)> token.tokensOfOwner(web3.eth.accounts[0])
[ BigNumber { s: 1, e: 0, c: [ 0 ] } ]
truffle(development)> token.tokensOfOwner(web3.eth.accounts[1])
[ BigNumber { s: 1, e: 0, c: [ 1 ] } ]
truffle(development)> token.tokensOfOwner(web3.eth.accounts[2])
[ BigNumber { s: 1, e: 0, c: [ 2 ] } ]

念のため、ownerOf で各トークンを保有するアカウントを確認してみます。

truffle(development)> token.ownerOf(0).then(function(owner) { console.log(owner == web3.eth.accounts[0]) })
true
undefined
truffle(development)> token.ownerOf(1).then(function(owner) { console.log(owner == web3.eth.accounts[1]) })
true
undefined
truffle(development)> token.ownerOf(2).then(function(owner) { console.log(owner == web3.eth.accounts[2]) })
true
undefined

こちらも想定通りの結果となりました。問題なさそうです。

まとめ

  • 標準化に向けて議論が進んでいる NFT(Non-Fungible Token)の仕様である ERC721 について、それが利用されている Ðapp である CryptoKittiies を踏まえながら紹介しました
  • CryptoKitties で実際に使われたバージョンの ERC721 について、サンプル実装やその動作確認を通じて理解を深めました
comments powered by Disqus