ERC721xトークンやってみる~Multi-Fungible Token~
昨年に公開されていたCryptoZombies Season 2をやってみました。 Zombie Battleground上でカスタムゲームモードをユーザが作成できるようになるためのハンズオンとLoom Plasma Cashを使用してERC721トークンを効率よくやり取りするためのコースが出来ています。特にERC721トークンの拡張であるERC721xトークン(Multi-Fungible Token)は様々なユースケース(特にゲーム)で使えそうなので備忘録として残します。

記事の通り、昨年にCryptoZombies Season 2はリリースされました。但しカスタムゲームモードのチャプターは2つ、Plasma Cashはまだ1つしか公開されておらず公開時からアップデートは特にされていないようです。[1]
CryptoZombies Season 2では以下のカリキュラムが追加されています。Loom Plasma CashはERC721に対応したサイドチェーンを展開できます。

- カスタムゲームモードの展開
- ERC721xトークンとPlasma Cashの使用方法
Plasma Cashのコースでは、最初のチャプターであるマルチファンジブルトークンのみ閲覧可能です。リリースされる予定のチャプターには以下のような内容が含まれる予定のようです。
- マルチファンジブルトークン(Multi-Fungible Token)
- Truffle/Plasma Chainへのデプロイ
- Plasma CashコントラクトとTransfer gateway利用方法
ERC721とERC721x
CryptoKittiesなどの実装に使用されているERC721は、代替不可能なトークン基準としてアイテム収集などのゲームや不動産など固有のアセットを相互に代替不可能なものとして管理する目的に使用されてきました。[2]
ERC721の問題点として、マルチクラスの中で複数個の同一種類のアセットを表現できない点があります。
例えばカードゲームにおいて剣という種類がある場合、剣のカード同士は同じ種類として代替可能であるべきです。剣と盾という異なる種類は代替不可能のクラスとしたまま、ERC721トークンにマルチクラスによる複数種類で複数個のトークンという考え方とバッチトランスファーの機能を追加したものがERC721xという規格です。[3]
ERC721xのインターフェースではERC721との互換性が保たれています。マルチファンジブルトークンではコントラクトの中に複数のFT(Fungible Token)とNFT(Non-Fungible Token)が存在し、uint256 tokenIdで各トークンが識別できるようになっているようです。
contract ERC721X {
function implementsERC721X() public pure returns (bool);
function ownerOf(uint256 _tokenId) public view returns (address _owner);
function balanceOf(address owner) public view returns (uint256);
function balanceOf(address owner, uint256 tokenId) public view returns (uint256);
function tokensOwned(address owner) public view returns (uint256[], uint256[]); function transfer(address to, uint256 tokenId, uint256 quantity) public;
function transferFrom(address from, address to, uint256 tokenId, uint256 quantity) public; // Fungible Safe Transfer From
function safeTransferFrom(address from, address to, uint256 tokenId, uint256 _amount) public;
function safeTransferFrom(address from, address to, uint256 tokenId, uint256 _amount, bytes data) public; // Batch Safe Transfer From
function safeBatchTransferFrom(address _from, address _to, uint256[] tokenIds, uint256[] _amounts, bytes _data) public;
function name() external view returns (string);
function symbol() external view returns (string); // Required Events
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event TransferWithQuantity(address indexed from, address indexed to, uint256 indexed tokenId, uint256 quantity);
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
event BatchTransfer(address indexed from, address indexed to, uint256[] tokenTypes, uint256[] amounts);
}
マルチクラスを提案するEIPは他にもありますが、ウォレットやマーケットプレースでの使用を諦めているため、LoomではERC721とERC1178を融合させるような形をERC721xにおいて目指したとのことです。[4]
Our Approach: Extending ERC721 with ERC1178 Out of all the existing solutions to this problem, the one that best suited our needs was ERC1178 (https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1178.md).
バッチトランスファーの機能はマルチクラスに跨る多量のトークンをを1回の処理で送付できるためガスコストを安く抑えることができるようになっています。特にゲームにおいて複数種類、数百個のアイテムをバルクで同時に送付できることは大きなメリットでしょう。[5]
ERC721x実装
CryptoZombiesの内容をUbuntu上で再現してtruffle developで動作を実際に確認してみたいと思います。
ERC721xはnpmで提供されているのでnpm install erc721xで簡単にインストールできます。コードを直接持ってきてインポートでもOKです。
$ npm install erc721x
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN erc721x-token-standard@1.0.0 No description
npm WARN erc721x-token-standard@1.0.0 No repository field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.7 (node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})+ erc721x@1.0.1
added 265 packages from 402 contributors in 22.507s
コースのチャプターを進めていくと以下のような関数の実装がでてきます。各関数の実装は下で記載。
- function name() external view returns (string) — トークンの名前を返す関数
- function symbol() external view returns (string) — トークンのシンボルを返す関数
- function individualSupply(uint _tokenId) public view returns (uint) — トークンIDごとにトークン供給量を返す関数
- function mintToken(uint _tokenId, uint _supply) public onlyOwner — 新規に_tokenIdを持つFT(Fungible Token)を生成する関数、オーバーライドした_mintを呼び出します
- function awardToken(uint _tokenId, address _to, uint _amount) public onlyOwner — _トークンIDで与えられるトークンをコントラクトオーナーからユーザへ移転する関数
- function convertToNFT(uint _tokenId, uint _amount) public — FT(Fungible Token)をNFT(Non-Fungible Token)へと変換する関数
- function convertToFT(uint _tokenId) public — NFT(Non-Fungible Token)をFT(Fungible Token)へと変換する関数
- function batchMintTokens(uint[] _tokenIds, uint[] _tokenSupplies) external onlyOwner — FT(Fungible Token)のバッチ生成をする関数
トークンタイプを変換する関数にはtokenTypeというマッピングが必要になりますが、npmでインストールしたerc721x@1.0.1にtokenTypeが実装されていないため手動でコード追加します。以下、 ERC721XTokenNFT.solで修正した点のみ記載。
ERC721XTokenNFT.solではNFTとして生成するところをERC721XToken.solではデフォルトをFT(Fungible Token)として作成するように書き換えます。_mint(uint256 _tokenId, address _to)はNFT(Non-Fungible Token)の作成に引き続き呼び出しできます。

コースの実装をカードゲームに見立て、CardGame.solという名前のコントラクトを作成してみました。コードの内容はCryptoZombies上で実装するものとほぼ同じです。
Ownable.solはいつものアレです。このコードをtruffle develop上でコンパイルしてテストしてみます。
ERC721xうごかす
truffle develop上で実装したCardGame.solを動かしてみます。基本的な関数をチェック。totalSupply()関数はトークンの種類の数を返します。まだ何もトークンを作成していないので0のままです。
truffle(develop)> CardGame.name()
'CardGame'
truffle(develop)> CardGame.symbol()
'CGM'
truffle(develop)>
undefined
truffle(develop)> CardGame.totalSupply()
BigNumber { s: 1, e: 0, c: [ 0 ] }
- 新規のトークンIDでFT(Fungible Token)を生成する
FTトークンのIDは1000000までと決めたので(CryptoZombies内でFTとNFTを変換する箇所でNFTのtokenIdを1000000からスタートするとしている)、トークンIDが1のトークンを1000個、2のトークンを5000個作成します。1は剣のカードを1000枚、盾のカードを5000枚作成するようなイメージです。
このカードゲームには供給量を定義しないリミテッドエディションと供給量を設定して発行するスタンダードエディションのカードがあり(あるとCryptoZombies内で想定している)、剣と盾のカードはスタンダードエディションのカードです。供給量を0とした場合に、そのカードはリミテッドエディションになります。
truffle(develop)> CardGame.mintToken(1, 1000, {from: web3.eth.accounts[0]})
truffle(develop)> CardGame.mintToken(2, 5000, {from: web3.eth.accounts[0]})
truffle(develop)> CardGame.totalSupply()
BigNumber { s: 1, e: 0, c: [ 2 ] }
...truffle(develop)> CardGame.totalSupply()
BigNumber { s: 1, e: 0, c: [ 2 ] }
truffle(develop)> CardGame.individualSupply(1)
BigNumber { s: 1, e: 3, c: [ 1000 ] }
truffle(develop)> CardGame.individualSupply(2)
BigNumber { s: 1, e: 3, c: [ 5000 ] }
truffle(develop)> CardGame.balanceOf(web3.eth.accounts[0], 1)
BigNumber { s: 1, e: 3, c: [ 1000 ] }
truffle(develop)> CardGame.balanceOf(web3.eth.accounts[0], 2)
BigNumber { s: 1, e: 3, c: [ 5000 ] }
- トークンの所有者を移転する
コントラクトオーナーからトークンを移転します。供給量の半分である500と2500をweb3.eth.accounts[1]のユーザへ移転してみます。
truffle(develop)> CardGame.awardToken(1, web3.eth.accounts[1], 500)
truffle(develop)> CardGame.awardToken(2, web3.eth.accounts[1], 2500)
truffle(develop)> CardGame.totalSupply()
BigNumber { s: 1, e: 0, c: [ 2 ] }
...truffle(develop)> CardGame.individualSupply(1)
BigNumber { s: 1, e: 3, c: [ 1000 ] }
truffle(develop)> CardGame.individualSupply(2)
BigNumber { s: 1, e: 3, c: [ 5000 ] }
truffle(develop)> CardGame.balanceOf(web3.eth.accounts[1], 1)
BigNumber { s: 1, e: 2, c: [ 500 ] }
truffle(develop)> CardGame.balanceOf(web3.eth.accounts[1], 2)
BigNumber { s: 1, e: 3, c: [ 2500 ] }
- トークンをNFT(Non-Fungible Token)へ変換する
ウォレットなどのERC721への後方互換性を持たせるためにはトークンはすべて代替不可能でユニークなERC721トークンである必要があります。ERC721xでのFT(Fungible Token)をNFT(Non-Fungible Token)へ変換する処理はLoom Plasma CashからEthereum Mainnetへ戻す際に使用されているようです。
とりあえずトークンIDが1のトークンを10個だけ変換してみます。トークンの種類は12種に増え、トークンIDが1のFT(Fungible Token)は490個に減少しました。また実装した通りFTトークンのIDが終わる1000000からNFT(Non-Fungible Token)のトークンIDが始まっています。
NFT(Non-Fungible Token)についてはownerOf()関数で所有者を確認することも可能です。
truffle(develop)> CardGame.convertToNFT(1, 10)
...truffle(develop)> CardGame.totalSupply()
BigNumber { s: 1, e: 1, c: [ 12 ] }
truffle(develop)> CardGame.balanceOf(web3.eth.accounts[0], 1)
BigNumber { s: 1, e: 2, c: [ 490 ] }
...truffle(develop)> CardGame.tokenByIndex(11)
BigNumber { s: 1, e: 6, c: [ 1000009 ] }
truffle(develop)> CardGame.balanceOf(web3.eth.accounts[0], 1000000)
BigNumber { s: 1, e: 0, c: [ 1 ] }
truffle(develop)> CardGame.balanceOf(web3.eth.accounts[0], 1000001)
BigNumber { s: 1, e: 0, c: [ 1 ] }
truffle(develop)> CardGame.balanceOf(web3.eth.accounts[0], 1000002)
BigNumber { s: 1, e: 0, c: [ 1 ] }
...truffle(develop)> CardGame.balanceOf(web3.eth.accounts[0], 1000009)
BigNumber { s: 1, e: 0, c: [ 1 ] }
truffle(develop)> CardGame.ownerOf(1000000)
'0x627306090abab3a6e1400e9345bc60c78a8bXXXX'
- トークンをバッチ生成する
実装したbatchMintTokens()をテストします。3種類のトークンを追加で発行できました。CardGame.totalSupply()は15種類で、FT(Fungible Token)として発行したものが5種類、NFT(Non-Fungible Token)へ変換したトークンが10個ある状態です。
truffle(develop)> CardGame.batchMintTokens([3, 4, 5], [30, 40, 50])
...truffle(develop)> CardGame.totalSupply()
BigNumber { s: 1, e: 1, c: [ 15 ] }
truffle(develop)> CardGame.individualSupply(3)
BigNumber { s: 1, e: 1, c: [ 30 ] }
truffle(develop)> CardGame.individualSupply(4)
BigNumber { s: 1, e: 1, c: [ 40 ] }
truffle(develop)> CardGame.individualSupply(5)
BigNumber { s: 1, e: 1, c: [ 50 ] }
まとめ
- ERC721の拡張でマルチクラスへ対応するERC721xがLoom Networkで使用されている(CryptoZombies Season 2)
- ERC721xはERC721互換のためウォレットや交換所などのマーケットプレースでも対応が可能
- ERC721xはnpmで提供されており簡単にインストールできる