Edited at

ERC-721トークンをOpenZeppelinで作ってみる

More than 1 year has passed since last update.

ERC721に対応したトークンをOpenZeppelinで作り、テストネットにデプロイしてみました。フロントエンドも実装したので、Dappsブラウザを使える方なら誰でもERC721トークンの譲渡を試すことができます。

開発したアプリには上記のURLからアクセスできます。Ropstenネットワークにのみデプロイしており、閲覧にはMistやMetaMaskなどのDappsブラウザが必要です。常識的な範囲内ならコントラクトを自由に動かしてもらって構いません。ブロックチェーンが更新されてもアプリは自動更新されずページの再読み込みが必要なのであしからず…

https://arosh.github.io/erc721-sandbox/


ERC721とは

ERC20のような、Ethereumブロックチェーン上のトークンの標準規格のひとつです。仕様のドラフトは以下のページに記載されています。

ERC-721 Non-Fungible Token Standard | Ethereum Improvement Proposals

ERC721の目的はNon-Fungible Token (NFT) の規格を標準化することです。NFTの概念の説明や利用用途については以下のページが詳しいです。


ERC721トークンを作り方

仕様書 に書いてあるとおりのメソッドを実装すればトークンを作ることができるわけですが、実装しなければならないメソッドがたくさんあって脆弱性なく実装できる気がしないのでライブラリを利用することを推奨します。ERC20トークンを作るときにも利用されている zeppelin-solidity には、ERC721トークンのベースとなるコードが追加されています。

↓ERC721関連のコードはここにあります。

https://github.com/OpenZeppelin/openzeppelin-solidity/tree/master/contracts/token/ERC721

このディレクトリにはファイルがたくさんあって混乱するのでひとつずつ解説します。


ERC721Basic

transferapprove, ownerOf といったトークンの譲渡や所有に関するメソッドのインターフェースが定義されています。インターフェースの定義だけで実装はありません。

https://github.com/OpenZeppelin/openzeppelin-solidity/blob/4a10f727c4756a8f6433f272a49e7a15db5e4b8f/contracts/token/ERC721/ERC721Basic.sol


ERC721BasicToken

ERC721Basicで定義されたインターフェースを実装したものです。modifierとして onlyOwnerOf(_tokenId)(トークンのオーナーだけこのメソッドを呼べる)や canTransfer(_tokenId)(トークンのオーナー or approveされた人だけこのメソッドを呼べる)といったものが定義されているので、トークンに独自の機能を追加するときに活用できると思います。

https://github.com/OpenZeppelin/openzeppelin-solidity/blob/4a10f727c4756a8f6433f272a49e7a15db5e4b8f/contracts/token/ERC721/ERC721BasicToken.sol


ERC721

ERC721Basicに追加のインターフェースとしてERC721EnumerableとERC721Metadataを追加したものです。インターフェースの定義だけで実装はありません。

ERC721EnumerableはERC721の中で enumeration extension と呼ばれている追加機能のインターフェースです。これは発行されたトークンを列挙するための機能で、totalSupply()tokenByIndex(_index) を組み合わせることで、コントラクトから発行された有効なトークンを列挙することができます。また tokenOfOwnerByIndex(_index) をERC721Basicの balanceOf() と組み合わせれば、あるユーザーが所有するトークンを列挙することもできます。

ERC721MetadataはERC721の中で metadata extension と呼ばれている追加機能のインターフェースです。ERC20にも同様の機能がある name(), symbol() に加えて、発行された個別のトークンに紐付けられたメタデータの場所を返すメソッドである tokenURI(_tokenId) のインターフェースが定義されています。トークンがNFTとして機能するためには tokenURI などを使ってメタデータを管理する必要があります。

これらの機能は必須なように思えますが、実装するためにはトークンのリストやメタデータをブロックチェーン上に格納する必要があるため、コントラクトの実行が高価になってしまう恐れがあります。そのためこれらの機能はoptionalとなっています。

https://github.com/OpenZeppelin/openzeppelin-solidity/blob/4a10f727c4756a8f6433f272a49e7a15db5e4b8f/contracts/token/ERC721/ERC721.sol


ERC721Token

ERC721で定義されたインターフェースを実装したものです。さらに、ERC721BasicToken(ERC721Basicで定義されたインターフェースを実装したもの)も継承しています。

このコントラクトをデプロイすればERC721トークンが完成するかというとそういうわけではなく、トークンを生成する _mint(_to, _tokenId)、トークンを償却する _burn(_owner, _tokenId)、トークンのメタデータを書き換える _setTokenURI(_tokenId, _uri) はinternalなメソッドとなっているため、このままコントラクトをデプロイしてもトークンを生成することができません。ERC721Tokenを継承したコントラクトを作成し、_mint, _burn, _setTokenURI を用いて、作成したいトークンの特性に合わせた mint, burn, setTokenURI を実装する必要があります。

https://github.com/OpenZeppelin/openzeppelin-solidity/blob/4a10f727c4756a8f6433f272a49e7a15db5e4b8f/contracts/token/ERC721/ERC721Token.sol


ERC721Receiver

ERC721ではトークンが行方不明になってしまうことを防ぐため、コントラクトがトークンを受け取る場合にはコントラクトが onERC721Received(_from, _tokenId, _data) を呼びだされた時に受け取りを許可するならば戻り値としてマジックナンバー 0xf0b9e5ba を返すことを要求しています。このコントラクトでは onERC721Received のインターフェースとマジックナンバー 0xf0b9e5ba の定義のみを行っています。

https://github.com/OpenZeppelin/openzeppelin-solidity/blob/4a10f727c4756a8f6433f272a49e7a15db5e4b8f/contracts/token/ERC721/ERC721Receiver.sol


ERC721Holder

ERC721Receiverで定義された onERC721Received の実装例として、何も判定を行わずにマジックナンバー 0xf0b9e5ba を返す処理を定義しています。

https://github.com/OpenZeppelin/openzeppelin-solidity/blob/4a10f727c4756a8f6433f272a49e7a15db5e4b8f/contracts/token/ERC721/ERC721Holder.sol


DeprecatedERC721

ERC721の議論中に名前が変更されたり削除されたメソッドのインターフェースを定義しています。

https://github.com/OpenZeppelin/openzeppelin-solidity/blob/4a10f727c4756a8f6433f272a49e7a15db5e4b8f/contracts/token/ERC721/DeprecatedERC721.sol


実際にERC721トークンを作ってみる

今回はサンプルとして、誰でもトークンを発行できて、所有者はいつでも自分のトークンのメタデータを書き換えたりトークンを償却したりすることができるコントラクトを作ってみました。

トークンはERC721Tokenを継承して mint, setTokenURI, burn を実装することによって行います。ERC721BasicTokenで定義されている onlyOwnerOf も使ってみました。


MyToken.sol

pragma solidity ^0.4.23;

import "zeppelin-solidity/contracts/token/ERC721/ERC721Token.sol";

contract MyToken is ERC721Token {
uint256 internal nextTokenId = 0;

constructor() public ERC721Token("MyToken", "MTKN") {}

function mint() external {
uint256 tokenId = nextTokenId;
nextTokenId = nextTokenId.add(1);
super._mint(msg.sender, tokenId);
}

function setTokenURI(uint256 _tokenId, string _message) external onlyOwnerOf(_tokenId) {
super._setTokenURI(_tokenId, _message);
}

function burn(uint256 _tokenId) external onlyOwnerOf(_tokenId) {
super._burn(msg.sender, _tokenId);
}
}


再掲しますが、このコントラクトをRopstenにデプロイしてフロントエンドを実装したものが以下のものになります。

https://arosh.github.io/erc721-sandbox/

mintのボタンを押すと、押した人にトークンが発行されます。burnはトークンの償却を行うボタンです。setTokenURIはメタデータのURIを設定するボタンです。参照先のURIにはERC721 Metadata JSON Schemaとして以下のスキーマに従うJSONオブジェクトが配置されていることが期待されています。

{

"title": "Asset Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Identifies the asset to which this NFT represents",
},
"description": {
"type": "string",
"description": "Describes the asset to which this NFT represents",
},
"image": {
"type": "string",
"description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive.",
}
}
}

今回は、メタデータを置くサーバーを用意したり、Swarmの処理を書いたりするのが面倒だったので、tokenURIとしてメタデータを配置しているURIではなく何らかのメッセージを書き込めるようにしてみました。実際に運用するときには、参照先のURIの内容のハッシュをトークンにもたせておくなどしてメタデータの正しさを担保する仕組みを用意する必要があるかもしれません。

Transferはトークンを他人に譲渡するボタンです。EtherscanはERC721には非対応ですが、ERC721のインターフェースは部分的にERC20と重なっているので、トランザクションの内容(誰から誰にトークンをどれだけ送ったか)はEtherscanでも確認できるようになっています。以下のURIは私がテスト用に作成したトランザクションです。

https://ropsten.etherscan.io/tx/0x5ebf764fa61c1dfa1953466bbed0a82c976c5cd74ba1213707fd56a131b90bbe


まとめ

OpenZeppelinを利用して簡単なERC721トークンを作成し、フロントエンドを実装して遊べるようにしてみました。

次は、ERC721で物品管理アプリを作った話を書きます。


参考記事