はじめに

初めまして。 『DApps開発入門』という本や色々記事を書いているかるでねです。

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!

今回は『ERC721』についてまとめていきます。

「ERC721」を理解する前に「ERC20」を理解しておくことをおすすめします。 以下の記事で「ERC20」についてまとめているので、是非読んでみてください!

ERC721とは

この章ではERC721の特徴について紹介していきます。 ERC721は「非代替性トークン」です。 「非代替性トークン」について調べると以下のような定義がされていました。

NFT(非代替性トークン)とは、ビットコインやドル紙幣のように全く同じ価値を持つ "代替可能 "な資産ではなく、それぞれが固有のものである特殊なトークンのことを指します。NFTは1つ1つが固有のものであるため、美術品や録音物、仮想現実の不動産やペットなどのデジタル資産の所有権を認証するために使用できます。

1つ1つがユニークなトークンであるため、交換することができないということです。 NFT(Non Fungible Token)と呼ばれるものこそ、まさに「非代替性トークン」のことです。 逆に交換可能である「代替性トークン」というものは同じ価値であれば交換することができます。 ERC20と呼ばれるトークン規格がまさに「代替性トークン」です。

ERC721規格の人気NFT

NFTですが、全てがERC721規格ではありません。 ERC721以外に有名な規格がERC1155です。 ERC1155の特徴を簡単に挙げると以下になります。

ERC1155の特徴

  • 1回の取引で複数のアイテムを送付できる
  • 1回の取引で複数の相手にNFTや通貨を送付できる
  • 上記2つの理由からガス代の節約につながる

有名なNFTコレクションの中でもERC721の規格を採用しているコレクションと、ERC1155の規格を採用しているコレクションがあります(他の規格を採用しているコレクションももちろんあります)。 ではERC721規格のNFTコレクションを一覧で紹介していきます。

ERC721規格のNFTコレクション

  • CryptoKitties
  • Sorare
  • ENS
  • Bored Ape Yacht Club
  • Art Blocks
  • CloneX

知っているNFTコレクションがあるのではないでしょうか? どのコレクションも有名ですが、ここに載せていないだけで他の有名コレクションもERC721規格を採用しています。 以下のページからERC721規格のNFTを一覧で見ることができます。

EIP721

EIPとは

前章までERC721といってきましたが、EIP721とは何でしょうか?

EIPとは「Ethereum Improvement Proposals」の略で、「Ethereumの新しい機能やプロセスに関する提案を規定する標準規格」のことです。 EIPの細かい説明は以下に書かれています。

簡単に言うと以下になります。 EIPは、Ethereum Improvement Proposalsの略でEthereumをより良いものにするために議論される改善提案のこと。 Ethereumは、特定の誰かによって管理されるものではないため、世界中の誰もがEIPを提出することで、Ethereumの発展に貢献することができる。 721という数字は721番目の提案ということです。

EIP721の仕様

ERC721に準拠したコントラクトを作成する際は、ERC721とERC165のinterfaceを実装する必要があります。 ERC165はinterfaceの実装機能と、コントラクトにinterfaceがあるかどうか検出する機能を持つ規格です。 ERC165について詳しくは以下に書かれています。

interface

interfaceとは、コントラクトに似ていますが実行することはできません。 interfaceには関数名や引数などのみ定義されていて中身は一切定義されていません。 そのため、interfaceを継承したコントラクト内で関数を再度定義して中身を記述する必要があります。 「interfaceいらなくね?」こう思う方もいると思います。 interfaceを使用するメリットは、実装で必要な関数を確認できることにあります。 最初で述べたように、interfaceには実装する上で必要な関数が定義されているので、開発者やコントラクトを実行するユーザーからどんな機能があるのかを簡単に確認できます。 有名なプロジェクトでもinterfaceはよく使用されているので、この機会に理解しておくと後々役に立ちます。 公式ドキュメントは以下になります。

識別子

NFTには1つずつ固有の番号を持っています。 この番号は重複しなければどのような番号をそれぞれのNFTにつけても問題ありません。 ただし一度決まった番号を変更することはできません。 NFTごとに単純に1ずつ増やす方法もありますが、IDは基本的にブラックボックスとして扱う必要があります。 そのため、呼び出し側はID番号に特定のパターンがあると仮定してはいけません。

ERC721

interfaceについて確認できたところで、EIP721に書かれているコードの詳細を見ていこうと思います! 以下を参考にしながら進めていきます。

以下がコードになります。 長くなってしまうので、コメントを取っ払っています。 1つずつ紹介していくのでついてきてください!

js
pragma solidity ^0.4.20;
interface ERC721 /* is ERC165 */ {
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
    event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
    function balanceOf(address _owner) external view returns (uint256);
    function ownerOf(uint256 _tokenId) external view returns (address);
    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
    function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
    function approve(address _approved, uint256 _tokenId) external payable;
    function setApprovalForAll(address _operator, bool _approved) external;
    function getApproved(uint256 _tokenId) external view returns (address);
    function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}
interface ERC165 {
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

Event

まずはEvent系を一気に紹介します。 Solidityにおけるイベントとは、簡単にいうと「スマートコントラクトでの動作結果をフロントに伝える」役割を持った機能です。 詳しくは以下の公式ドキュメントを参考にしてください。

Transfer
js
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

NFTの所有権が変更されたとき(生成や破棄)に発行されます。

Approval
js
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

_ownerで指定されたアドレスが保有するNFTの送付許可されたアドレス(_approved)が変更、または再確認されたときに発行される。

ApprovalForAll
js
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

_operatorで指定されたアドレスが、_ownerで指定されたアドレスが所有しているNFTの操作権限を与えられたときと無効にされたときに発行される。

関数

では次に関数を1つずつ紹介していきます。

balanceOf
js
function balanceOf(address _owner) external view returns (uint256);

_ownerに指定されたアドレスが所有する全てのNFTの数をカウントして返す関数。 _ownerに0アドレスが指定されたときは無効になる。

ownerOf
js
function ownerOf(uint256 _tokenId) external view returns (address);

_tokenIdに指定されたNFTのIDの所有者のアドレスを返す関数。 0アドレスが所有者の場合は無効とみなされる。

safeTransferFrom
js
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;

_fromで指定されたアドレスから、_toに指定されたアドレスへ_tokenIdの番号を持つNFTを送る関数。 _toで指定されたアドレスがEOAアカウントかコントラクトアカウントかチェックし、コントラクトであるとき送り先のコントラクトでERC721がサポートされているかチェックする。 これがsafeと関数名についている理由です。 送り先のコントラクトがERC721をサポートしていない状態で送られてしまうと、そのNFTは永久に誰も触れなくなってしまいます。 その危険性を排除してくれているので安心して実行できます。 dataという引数には_toで指定されたアドレスの呼び出しで送信される追加データを渡す。

条件

  • _fromにはNFTの所有者か、NFTの所有者から転送許可されたアドレスでないといけない
  • _tokenIdは存在するIDでないといけない
  • _toがコントラクトであるとき、ERC721をサポートしていないといけない
safeTransferFrom
js
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

先ほど説明した関数と同じ名前なので変に思うかもしれないですが、よく見ると引数のdataが消えているのがわかります。 Solidityでは、関数名が同じでも引数の数が異なれば別関数として扱われます。 そのためdataを引数に渡す必要がないときに、こちらのsafeTransferFromが使用されます。 機能自体は先ほど説明したsafeTransferFromと同じです。

transferFrom
js
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

_fromで指定されたアドレスから、_toに指定されたアドレスへ_tokenIdの番号を持つNFTを送る関数。 _toで指定されたアドレスがEOAアカウントかコントラクトアカウントかチェックしないため、送り先のコントラクトがERC721をサポートしていない状態で送られてしまうと、そのNFTは永久に誰も触れなくなってしまいます。

条件

  • _fromにはNFTの所有者か、NFTの所有者から転送許可されたアドレスでないといけない
  • _tokenIdは存在するIDでないといけない
approve
js
function approve(address _approved, uint256 _tokenId) external payable;

approve()関数の実行アドレスが_approvedで指定されたアドレスに、_tokenIdで指定したIDのNFTの送付許可を与える関数。

条件

  • _approvedに0アドレスを指定できない
  • approve()関数実行アドレスが_tokenIdの所有者、または_tokenIdの所有者から送付許可されたアドレスでないといけない
setApprovalForAll
js
function setApprovalForAll(address _operator, bool _approved) external;

setApprovalForAll()関数の実行アドレスが、_operatorで指定されたアドレスに全てのNFTの管理権限を与える、もしくは無効にする関数。 _approvedがtrueの時は権限を与えて、falseの時は無効にする。 実行権限を与えられるNFTはsetApprovalForAll()関数の実行アドレスが保有するNFTのみ。

getApproved
js
function getApproved(uint256 _tokenId) external view returns (address);

_tokenIdで指定したIDのNFTを送付許可されたアドレスを返す関数。 もしアドレスがなければ0アドレスが返される。

条件

  • _tokenIdで指定したIDのNFTが存在している必要がある
isApprovedForAll
js
function isApprovedForAll(address _owner, address _operator) external view returns (bool);

_ownerで指定されたアドレスが、_operatorに指定されたアドレスに対して、所有しているNFTの全ての管理権限を与えているか返す関数。 権限を与えていればtrueを返し、権限を与えていなければfalseを返す。

supportsInterface
js
interface ERC165 {
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

ERC165の実装部分です。 コントラクトがinterfaceを実装しているか確認する関数。 interfaceIDに確認したいERC165で規定されているinterface識別子を渡して確認する。 interfaceIDが0xffffff以外であればtrueを返し、0xffffffであればfalseを返す。

ERC721TokenReceiver

ERC721の関数を確認できたところで、次にERC721TokenReceiverについて確認していきます。 ERC721TokenReceiverは、safeTransferFrom関数を実装する時に必要となるコントラクトです。 コードは以下になります。

js
interface ERC721TokenReceiver {
    function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);
}

safeTransferFrom関数の部分でも書いているように、_toで指定されたアドレスがコントラクトであるとき、送り先のコントラクトでERC721がサポートされているかチェックして、トークンがコントラクト内で永久にロックされるのを防いでくれます。

onERC721Received

_operatorで指定したアドレスがERC721をサポートしているかチェックします。

ERC721Metadata

次にERC721のメタデータについて説明していきます。 このERC721Metadataはオプションの拡張機能です。 メタデータの定義は以下になります。

メタデータとは、データについてのデータ。あるデータそのものではなく、そのデータを表す属性や関連する情報を記述したデータのこと。データを効率的に管理したり検索したりするためには、メタデータの適切な付与と維持が重要となる。

ERC721でいえば、NFTというデータを表す情報を記述するものです。 コードは以下になります。

js
interface ERC721Metadata /* is ERC721 */ {
    function name() external view returns (string _name);
    function symbol() external view returns (string _symbol);
    function tokenURI(uint256 _tokenId) external view returns (string);
}
name
js
function name() external view returns (string _name);

NFTの名前を設定する関数。

symbol
js
function symbol() external view returns (string _symbol);

NFTのシンボルを設定する関数。

tokenURI
js
function tokenURI(uint256 _tokenId) external view returns (string);

_tokenIdで指定されたIDのNFTの URI(Uniform Resource Identifier) を返す関数。 URIとは、NFTを識別するためのデータ書式を定義したものです。 以下のようにJSON形式で記述します。

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."
        }
    }
}

nameにNFTの名前、descriptionにそのNFTの説明、imageに画像データや画像データが格納されたURLを格納します。

ERC721Enumerable

最後に拡張機能について紹介していきます。 拡張機能のため実装は任意です。 ERC721Enumerableを実装すると、特定のアドレスが所有しているトークンの一覧を取得することができます。 ただ、「どのアドレスがどのトークンを所有しているか」というデータをストレージに保存しなければいけないため、その分ガス代が高くなります。 コードは以下になります。

js
interface ERC721Enumerable /* is ERC721 */ {
    function totalSupply() external view returns (uint256);
    function tokenByIndex(uint256 _index) external view returns (uint256);
    function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
}
totalSupply
js
function totalSupply() external view returns (uint256);

NFTの総供給量を返す関数。

tokenByIndex
js
function tokenByIndex(uint256 _index) external view returns (uint256);

全てのトークンの中で _index で指定されたインデックス番号を持つNFTのIDを返す関数。

条件

  • totalSupply より大きな値を_indexに指定できない
tokenOfOwnerByIndex
js
function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);

_owner に指定されたアドレスが所有するNFTの中で、_index で指定されたインデックス番号を持つNFTのIDを返す関数。

条件

  • totalSupply より大きな値を_indexに指定できない

ERC721のコード

ここまででERC721の概要とEIP721について確認していきました。 この章ではERC721のコードを実際に見ていきたいと思います! 「え?さっき確認したんじゃないの?」という疑問が浮かぶと思いますが、前章ではあくまでEIP721について紹介しただけなので、ERC721のコードの説明自体はまだしていないです。 前章よりも長くなると思うので覚悟してついてきてください!

IERC721

ERC721を実装する上で必須の関数やイベントが定義されています。

関数とイベント

  • balanceOf(owner)
  • ownerOf(tokenId)
  • safeTransferFrom(from, to, tokenId)
  • transferFrom(from, to, tokenId)
  • approve(to, tokenId)
  • getApproved(tokenId)
  • setApprovalForAll(operator, _approved)
  • isApprovedForAll(owner, operator)
  • safeTransferFrom(from, to, tokenId, data)
  • Transfer(from, to, tokenId)
  • Approval(owner, approved, tokenId)
  • ApprovalForAll(owner, operator, approved)

EIP721に書かれていた関数やイベントと同じですね! EIPで定義されている関数やイベントは必須の機能なので他の規格を見るときに覚えておくと良いです。 コードは以下になります。

js
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../../utils/introspection/IERC165.sol";
interface IERC721 is IERC165 {
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    function balanceOf(address owner) external view returns (uint256 balance);
    function ownerOf(uint256 tokenId) external view returns (address owner);
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
    function safeTransferFrom(address from, address to, uint256 tokenId) external;
    function transferFrom(address from, address to, uint256 tokenId) external;
    function approve(address to, uint256 tokenId) external;
    function setApprovalForAll(address operator, bool approved) external;
    function getApproved(uint256 tokenId) external view returns (address operator);
    function isApprovedForAll(address owner, address operator) external view returns (bool);
}

IERC721Metadata

ERC721を実装する上で拡張機能が定義されています。

IERC721Metadata

  • name()
  • symbol()
  • tokenURI(tokenId)

こちらもEIP721で定義されていたERC721Metadataと同じですね。 コードは以下になります。

IERC721Enumerable

ERC721を実装する上で拡張機能が定義されています。 拡張機能というように、オプションの機能なため実装するかどうかは任意です。

IERC721Enumerable

  • totalSupply()
  • tokenOfOwnerByIndex(owner, index)
  • tokenByIndex(index)

こちらもEIP721で定義されていたERC721Enumerableと同じですね。 ここまででわかるように、EIP721で定義されていたものは全てERC721ではinterfaceとして実装されています。 コードは以下になります。

ERC721

では、ERC721を実装している部分を確認していきましょう! コードは以下に書かれています。 長くなってしまうので、コードは記事内に記載しません。

変数

まずは変数を紹介します。

js
// Token name
string private _name;
// Token symbol
string private _symbol;
// Mapping from token ID to owner address
mapping(uint256 => address) private _owners;
// Mapping owner address to token count
mapping(address => uint256) private _balances;
// Mapping from token ID to approved address
mapping(uint256 => address) private _tokenApprovals;
// Mapping from owner to operator approvals
mapping(address => mapping(address => bool)) private _operatorApprovals;
_name

ERC721トークンの名前が格納されます。

_symbol

ERC721トークンのシンボルが格納されます。

_owners

各ERC721トークンのIDとアドレスを紐づける配列。

_balances

各アドレスがいくつのERC721トークンを所有しているか記録する配列。

_tokenApprovals

各ERC721トークンのIDのtransfer権限のあるアドレスを紐づける配列。

_operatorApprovals

ERC721トークンの所有者がアドレスに対して transfer 権限を与えているかの配列。

関数

次に関数を確認していきます。 前章のinterfaceで紹介している関数と説明が被ってしまうので、すでに前章で解説した関数については説明を簡略させていただきます。 主要な関数のみ紹介します。詳細はOpenZeppelinのコードを参照してください。

constructor
js
constructor(string memory name_, string memory symbol_) {
    _name = name_;
    _symbol = symbol_;
}

ERC721トークンの名前とシンボルを設定しています。 constructor は、コントラクトがデプロイされた際に一度だけ実行されます。

_transfer
js
function _transfer(address from, address to, uint256 tokenId) internal virtual {
    require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
    require(to != address(0), "ERC721: transfer to the zero address");
    _beforeTokenTransfer(from, to, tokenId, 1);
    require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
    delete _tokenApprovals[tokenId];
    unchecked {
        _balances[from] -= 1;
        _balances[to] += 1;
    }
    _owners[tokenId] = to;
    emit Transfer(from, to, tokenId);
    _afterTokenTransfer(from, to, tokenId, 1);
}

fromで指定したアドレスが、tokenIdで指定したIDのERC721トークンの所有者であるか確認しています。 その後送り先のアドレスが0アドレスでないか確認しています。 _beforeTokenTransfer関数を呼び出した後、各配列を更新し、最後に_afterTokenTransfer関数を呼び出しています。

_mint
js
function _mint(address to, uint256 tokenId) internal virtual {
    require(to != address(0), "ERC721: mint to the zero address");
    require(!_exists(tokenId), "ERC721: token already minted");
    _beforeTokenTransfer(address(0), to, tokenId, 1);
    require(!_exists(tokenId), "ERC721: token already minted");
    unchecked {
        _balances[to] += 1;
    }
    _owners[tokenId] = to;
    emit Transfer(address(0), to, tokenId);
    _afterTokenTransfer(address(0), to, tokenId, 1);
}

ERC721トークンを発行する前にいくつかチェックや更新を行っています。 送り先のアドレスが0アドレスではないかを確認し、tokenIdで指定したIDのERC721トークンがまだ存在しないか確認しています。

ERC721Enumerable

では、次にERC721Enumerableを実装している部分を確認していきましょう! コードは以下になります。

詳細な説明は長くなるため省略しますが、主要な配列と関数のみ紹介します。

変数

js
mapping(address => mapping(uint256 => uint256)) private _ownedTokens;
mapping(uint256 => uint256) private _ownedTokensIndex;
uint256[] private _allTokens;
mapping(uint256 => uint256) private _allTokensIndex;

これらの配列により、特定のアドレスが所有しているトークンの一覧を効率的に取得できます。

ERC721の実装

前章まででERC721のコードを確認してきました。 この章から実際にERC721を実装していきましょう!

js
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract CardeneToken is ERC721 {
    constructor() ERC721("CardeneToken", "CARD") {}
}

実は最低限の実装はこれだけで十分です。 なんか拍子抜けですよね。 さっきまでの長いコードはどこに行ったかと不思議に思う方もいると思いますが、ERC721コントラクトを継承しているため、ERC721コントラクト内の関数を全て使用することができます。

ERC721の実行

では最後にERC721を実行していきましょう! 以下のURLを開くとコードが記述された状態で、Remixエディタがブラウザ上で開きます。

ではコンパイルとデプロイをしていきましょう!

名称未設定のデザイン (1).gif

いかがだったでしょうか? ちゃんとERC721内の関数が使えていましたね! 全ての関数を実行したかったのですが、そうするとこの記事が異常なほど長くなってしまうので今回は控えておきます。 是非みなさんのお手元で実行してみてください!

最後に

今回は『ERC721』についてまとめてきました。 他でも色々記事を書いているのでぜひよろしければ読んでいってください!

参考