openzeppelin-solidityでERC721トークンを発行しよう

Takuya Fujita
GBEC Tech Blog
Published in
17 min readDec 15, 2018

--

はじめに

ブロックチェーンの仕組みを学び、Ethereumを学び、さらにsolidityの書き方まで勉強しここに辿り着いたあなたは、次にERC20トークンかERC721トークンを発行したくてうずうずしているはずです。CryptoPuppies(暗号犬)でも作って自分が作ったトークンに何千万円の価値がついたらいいななんて考えますよね。
この記事では夢みるそんなあなたのために、openzeppelin-solidityというNode.jsのモジュールを使ってどうERC721トークンを発行するのか、一緒にコントラクトの中をのぞいていきます。

注意

2018年9月にopenzeppelin-slidity内のERC721のコントラクトの仕様が大きく変わってしまい、今まであった日本語での解説記事が過去のものとなってしまっていたため、改めて新しい解説を行うことにしました。Node.jsでのモジュールのインストールの仕方や、開発環境の整え方等は他を参照してください。この記事は2018年12月14日現在のものです。

GitHubをみてみよう

では早速、モジュールの中をのぞいていきます。

いろんなsolidityのコントラクトがあってどれを使えばいいのか迷いますが、ERC721Fullを継承しておけば間違いないです。継承するコントラクトの例は以下のようになります。

pragma solidity ^0.4.23;import “openzeppelin-solidity/contracts/token/ERC721/ERC721Full.sol”;contract CryptoPuppies is ERC721Full {  constructor() public ERC721Full(“CryptoPuppies”, “CP”){}  uint256 internal nextTokenId = 0;  function mint() external {
uint256 tokenId = nextTokenId;
nextTokenId = nextTokenId.add(1);
super._mint(msg.sender, tokenId);
}

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

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

CryptoPuppiesというコントラクトがERC721Fullを継承しています。まず三つの関数 mint(), setTokenURI(), burn()について簡単に説明します。

mint()はERC721トークンを発行して、関数実行者に対してトークンを付与する関数。tokenIdは0から発行順に割り当てられているのがわかると思います。setTokenURI()はmint()で発行したトークンに対してMetadataへのURIを埋め込む関数。burn()は発行したERC721トークンをIdを指定し、消すための関数です。

superとは

継承先の関数を呼び出す際、関数につけているsuperは、コントラクトの継承において、菱形継承問題というようなクラスの分岐が起きてしまった際に、分岐先のどちらの関数も正しく参照できるようにするためのものです。ここのCryptoPuppiesコントラクトではERC721Fullの一つしか継承していませんが、ERC721Fullをみてみると複数継承しており、分岐していることがわかります。

Metadataについて

Metadataというのは、CryptoKittiesでいう猫の毛の色、模様、目の色などのそれぞれのトークンを特徴づけるための情報のことです。

ERC721トークンにおけるこのMetadata自体はEthereumのチェーン上に記録するわけではなく、どこか自分で用意したサーバ、DBに保存し、トークンを扱う際に都度URIを確認し、そのサーバ、DBから情報を取得してくるという仕組みになっています。。トークンの中にはURIとMetadataをハッシュ化したデータを入れておくことで、トークン側からMetadataを探すことも、Metadata側からトークンを見つけ出すこともできるようになります。

コントラクトの継承をたどる

上の三つの関数の中で使っている_mint(),_setTokenURI(),_burn()はERC721Fullから継承してきたものです。どういう風に継承しているのか、また他にどういう関数が使えるのか見ていきましょう。

まずはERC721Full.sol

pragma solidity ^0.4.24;import “./ERC721.sol”;
import “./ERC721Enumerable.sol”;
import “./ERC721Metadata.sol”;
contract ERC721Full is ERC721, ERC721Enumerable, ERC721Metadata {
constructor(string name, string symbol) ERC721Metadata(name, symbol)
public
{
}
}

ERC721, ERC721Enumerable, ERCMetadata と複数のコントラクトを継承していることがわかります。ここにあるconstructorは、トークンの名前とシンボルを決めるためのものです。先ほど出した三つの関数はERC721Fullから継承としてきたと言いましたが、それらはここに記述されているのではなく、もとあったコントラクトからさらに継承されているということです。

次の継承先のERC721をみていきます。長いのでコメントはある程度省いています。

pragma solidity ^0.4.24;import “./IERC721.sol”;
import “./IERC721Receiver.sol”;
import “../../math/SafeMath.sol”;
import “../../utils/Address.sol”;
import “../../introspection/ERC165.sol”;
contract ERC721 is ERC165, IERC721 {using SafeMath for uint256;
using Address for address;
bytes4 private constant _ERC721_RECEIVED = 0x150b7a02;// Mapping from token ID to owner
mapping (uint256 => address) private _tokenOwner;
// Mapping from token ID to approved address
mapping (uint256 => address) private _tokenApprovals;
// Mapping from owner to number of owned token
mapping (address => uint256) private _ownedTokensCount;
// Mapping from owner to operator approvals
mapping (address => mapping (address => bool)) private _operatorApprovals;
bytes4 private constant _InterfaceId_ERC721 = 0x80ac58cd;constructor()
public
{
// register the supported interfaces to conform to ERC721 via ERC165
_registerInterface(_InterfaceId_ERC721);
}
function balanceOf(address owner) public view returns (uint256) {
require(owner != address(0));
return _ownedTokensCount[owner];
}
function ownerOf(uint256 tokenId) public view returns (address) {
address owner = _tokenOwner[tokenId];
require(owner != address(0));
return owner;
}
function approve(address to, uint256 tokenId) public {
address owner = ownerOf(tokenId);
require(to != owner);
require(msg.sender == owner || isApprovedForAll(owner, msg.sender));
_tokenApprovals[tokenId] = to;
emit Approval(owner, to, tokenId);
}
function getApproved(uint256 tokenId) public view returns (address) {
require(_exists(tokenId));
return _tokenApprovals[tokenId];
}
function setApprovalForAll(address to, bool approved) public {
require(to != msg.sender);
_operatorApprovals[msg.sender][to] = approved;
emit ApprovalForAll(msg.sender, to, approved);
}
function isApprovedForAll(
address owner,
address operator
)
public
view
returns (bool)
{
return _operatorApprovals[owner][operator];
}
function transferFrom(
address from,
address to,
uint256 tokenId
)
public
{
require(_isApprovedOrOwner(msg.sender, tokenId));
require(to != address(0));
_clearApproval(from, tokenId);
_removeTokenFrom(from, tokenId);
_addTokenTo(to, tokenId);
emit Transfer(from, to, tokenId);
}
function safeTransferFrom(
address from,
address to,
uint256 tokenId
)
public
{
// solium-disable-next-line arg-overflow
safeTransferFrom(from, to, tokenId, “”);
}
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes _data
)
public
{
transferFrom(from, to, tokenId);
// solium-disable-next-line arg-overflow
require(_checkOnERC721Received(from, to, tokenId, _data));
}
function _exists(uint256 tokenId) internal view returns (bool) {
address owner = _tokenOwner[tokenId];
return owner != address(0);
}
function _isApprovedOrOwner(
address spender,
uint256 tokenId
)
internal
view
returns (bool)
{
address owner = ownerOf(tokenId);

return (
spender == owner ||
getApproved(tokenId) == spender ||
isApprovedForAll(owner, spender)
);
}
function _mint(address to, uint256 tokenId) internal {
require(to != address(0));
_addTokenTo(to, tokenId);
emit Transfer(address(0), to, tokenId);
}
function _burn(address owner, uint256 tokenId) internal {
_clearApproval(owner, tokenId);
_removeTokenFrom(owner, tokenId);
emit Transfer(owner, address(0), tokenId);
}
function _addTokenTo(address to, uint256 tokenId) internal {
require(_tokenOwner[tokenId] == address(0));
_tokenOwner[tokenId] = to;
_ownedTokensCount[to] = _ownedTokensCount[to].add(1);
}
function _removeTokenFrom(address from, uint256 tokenId) internal {
require(ownerOf(tokenId) == from);
_ownedTokensCount[from] = _ownedTokensCount[from].sub(1);
_tokenOwner[tokenId] = address(0);
}
function _checkOnERC721Received(
address from,
address to,
uint256 tokenId,
bytes _data
)
internal
returns (bool)
{
if (!to.isContract()) {
return true;
}
bytes4 retval = IERC721Receiver(to).onERC721Received(
msg.sender, from, tokenId, _data);
return (retval == _ERC721_RECEIVED);
}
function _clearApproval(address owner, uint256 tokenId) private {
require(ownerOf(tokenId) == owner);
if (_tokenApprovals[tokenId] != address(0)) {
_tokenApprovals[tokenId] = address(0);
}
}
}

長いですね笑

balanceOf(), ownerOf(), approve()などの基本的な機能の関数が入っているのがわかります。終わりの方に、先ほど参照していた、_mint()と_burn()も確認できます。

自分で作ったコントラクトで必要な関数を作成したい場合は、これらの関数を参考にすると良いです。実際のところ、欲しいと思った関数はだいたい用意されています。

個人的には、privateがついた関数やmappingは継承先のコントラクトでは参照できないということを忘れてつまづいたりしていたので注意です。それらの情報を扱うための関数も他で用意されているはずなのでよく読んでみましょう。

ERC721Enumerableはそれぞれのアドレスがいくつ、どのトークンを持っているのかを確認するためのもので、ERCMetadataのコントラクトはMetadataの参照と、変更を行うためのものです。今回は長くなるので割愛しますが、上のようにどういう関数があるか確認してみると良いでしょう。

まとめ

他の言語においては扱うモジュールの中の挙動まで確認する機会は少ないと思いますが、solidity、スマートコントラクトに関しては堅牢性を保つためにも、継承先のコントラクトがどう動いているかを確認しておいた方がいいかもしれません。

常に更新されて保証されたsolidityのコードに触れる機会もなかなかありませんし、そこに触れられるだけでも読む価値があるものだと思います。

openzeppelin-solidityにおいては他にも色々なコントラクトがあり、今回扱っていないコントラクトや関数を学ぶだけでも表現の幅が広がると思うのでぜひ触ってみてください!

執筆者:藤田 拓也(HashHubインターン:@peaceandwhisky

参考文献

お知らせ

■HashHubでは入居者募集中です!
HashHubは、ブロックチェーン業界で働いている人のためのコワーキングスペースを運営しています。ご利用をご検討の方は、下記のWEBサイトからお問い合わせください。また、最新情報はTwitterで発信中です。

HashHub:https://hashhub.tokyo/
Twitter:https://twitter.com/HashHub_Tokyo

■ブロックチェーンエンジニア集中講座開講中!
HashHubではブロックチェーンエンジニアを育成するための短期集中講座を開講しています。お申込み、詳細は下記のページをご覧ください。

ブロックチェーンエンジニア集中講座:https://www.blockchain-edu.jp/

■HashHubでは下記のポジションを積極採用中です!
・コミュニティマネージャー
・ブロックチェーン技術者・開発者
・ビジネスディベロップメント

詳細は下記Wantedlyのページをご覧ください。

Wantedly:https://www.wantedly.com/companies/hashhub/projects

--

--