bitbank

Smart Contract Solidity ブロックチェーン

ERC721について非エンジニアでもわかるように概要から実装まで1から丁寧にわかりやすく解説

かるでね

こんにちは!実務でPython、Djangoを使って、機械学習やWebアプリケーション開発などをしているかるでねです!
最近はSolidityを使ってスマートコントラクト開発やバグを見つけたりしています。

今回は「ERC721」について1からわかりやすく解説していきます!

ER721」がわからなかったり、なんとなく理解している非エンジニアやこれから「ERC721」を理解しようとしているエンジニアの方向けの記事になります。

丁寧に解説している分だいぶ長くなってしまっています。1

時間があるときに読むか、辞書的に使っていただければ嬉しいです!

それでは早速中身を見ていきましょう!

ERC721」を理解する前に「ERC20」を理解しておくことをおすすめします。

以下の記事で「ERC20」についてまとめているので、是非読んでみてください!

ERC721とは

この章ではERC721の特徴について紹介していきます。

ERC721は「非代替性トークン」です。

非代替性トークン」について調べると以下のような定義がされていました。

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

https://www.coinbase.com/ja/learn/crypto-basics/what-are-nfts

1つ1つがユニークなトークンであるため、交換することができないということです。

NFT(Non Fungible Token)と呼ばれるものこそ、まさに「非代替性トークン」のことです。

逆に交換可能である「代替性トークン」というものは同じ価値であれば交換することができます。

ERC20と呼ばれるトークン規格がまさに「代替性トークン」です。

ERC20については以下の記事で詳しくまとめているのでぜひ読んでみてください。

ERC721規格の人気NFT

NFTですが、全てがERC721規格ではありません。

ERC721以外に有名な規格がERC1155です。

ERC1155の特徴を簡単に挙げると以下になります。

ERC1155の特徴

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

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

ではERC721規格のNFTコレクションを一覧で紹介していきます。

知っているNFTコレクションがあるのではないでしょうか?

どのコレクションも有名ですが、ここに載せていないだけで他の有名コレクションもERC721規格を採用しています。

以下のページからERC721規格のNFTを一覧で見ることができます。

EIP721

EIPとは

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

EIPとは 「Ethereum Improvement Proposals」の略で、「Ethereumの新しい機能やプロセスに関する提案を規定する標準規格」のことです。

EIPの細かい説明は以下に書かれています。

簡単に言うと以下になります。

EIPは、Ethereum Improvement Proposalsの略でイーサリアムをより良いものにするために議論される改善提案のこと。イーサリアムは、特定の誰かによって管理されるものではないため、世界中の誰もがEIPを提出することで、イーサリアムの発展に貢献することができる。

https://eips.ethereum.org/

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つずつ紹介していくのでついてきてください!

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
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

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

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

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

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

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

関数

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

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

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

_ownerに0アドレスが指定されたときは無効になる。

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

_tokenIdに指定されたNFTのIDの所有者のアドレスを返す関数。

0アドレスが所有者の場合は無効とみなされる。

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

_fromで指定されたアドレスから、_toに指定されたアドレスへ_tokenIdの番号を持つNFTを送る関数。

_toで指定されたアドレスがEOAアカウントかコントラクトアカウントかチェックし、コントラクトであるとき送り先のコントラクトでERC721がサポートされているかチェックする。

これがsafeと関数名についている理由です。

送り先のコントラクトがERC721をサポートしていない状態で送られてしまうと、そのNFTは永久に誰も触れなくなってしまいます。

その危険性を排除してくれているので安心して実行できます。

EOAアカウントとコントラクトアカウントについては以下を参考にしてください。

dataという引数には_toで指定されたアドレスの呼び出しで送信される追加データを渡す。

条件

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

先ほど説明した関数と同じ名前なので変に思うかもしれないですが、よく見ると引数のdataが消えているのがわかります。

Solidityでは、関数名が同じでも引数の数が異なれば別関数として扱われます。

そのためdataを引数に渡す必要がないときに、こちらのsafeTransferFromが使用されます。

機能自体は先ほど説明したsafeTransferFromと同じです。

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

_fromで指定されたアドレスから、_toに指定されたアドレスへ_tokenIdの番号を持つNFTを送る関数。

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

条件

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

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

条件

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

setApprovalForAll関数の実行アドレスが、_operatorで指定されたアドレスに全てのNFTの管理権限を与える、もしくは無効にする関数。

_approvedtrueの時は権限を与えて、falseの時は無効にする。

実行権限を与えられるNFTはsetApprovalForAll関数の実行アドレスが保有するNFTのみ。

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

_tokenIdで指定したIDのNFTを転送許可されたアドレスを返す関数。

もしアドレスがなければ0アドレスが返される。

条件

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

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

権限を与えていればtrueを返し、権限を与えていなければfalseを返す。

supportsInterface
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関数を実装する際に必要となるコントラクトです。

コードは以下になります。

/// @dev Note: the ERC-165 identifier for this interface is 0x150b7a02.
interface ERC721TokenReceiver {
    /// @notice Handle the receipt of an NFT
    /// @dev The ERC721 smart contract calls this function on the recipient
    ///  after a `transfer`. This function MAY throw to revert and reject the
    ///  transfer. Return of other than the magic value MUST result in the
    ///  transaction being reverted.
    ///  Note: the contract address is always the message sender.
    /// @param _operator The address which called `safeTransferFrom` function
    /// @param _from The address which previously owned the token
    /// @param _tokenId The NFT identifier which is being transferred
    /// @param _data Additional data with no specified format
    /// @return `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
    ///  unless throwing
    function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);
}

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

onERC721Received

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

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

ERC721Metadata

次にERC721のメタデータについて説明していきます。

このERC721Metadataはオプションの拡張機能です。

メタデータの定義は以下になります。

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

https://e-words.jp/w/メタデータ.html

ERC721でいえば、NFTというデータを表す情報を記述するものです。

コードは以下になります。

/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension
/// @dev See https://eips.ethereum.org/EIPS/eip-721
///  Note: the ERC-165 identifier for this interface is 0x5b5e139f.
interface ERC721Metadata /* is ERC721 */ {
    /// @notice A descriptive name for a collection of NFTs in this contract
    function name() external view returns (string _name);

    /// @notice An abbreviated name for NFTs in this contract
    function symbol() external view returns (string _symbol);

    /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
    /// @dev Throws if `_tokenId` is not a valid NFT. URIs are defined in RFC
    ///  3986. The URI may point to a JSON file that conforms to the "ERC721
    ///  Metadata JSON Schema".
    function tokenURI(uint256 _tokenId) external view returns (string);
}
name
function name() external view returns (string _name);

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

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

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

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

_tokenIdで指定されたIDのNFTのURI(Uniform Resource Identifier)を返す関数。

URIとは、NFTを識別するためのデータ書式を定義したものです。

以下のように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

最後に拡張機能について紹介していきます。

コードは以下になります。

/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
/// @dev See https://eips.ethereum.org/EIPS/eip-721
///  Note: the ERC-165 identifier for this interface is 0x780e9d63.
interface ERC721Enumerable /* is ERC721 */ {
    /// @notice Count NFTs tracked by this contract
    /// @return A count of valid NFTs tracked by this contract, where each one of
    ///  them has an assigned and queryable owner not equal to the zero address
    function totalSupply() external view returns (uint256);

    /// @notice Enumerate valid NFTs
    /// @dev Throws if `_index` >= `totalSupply()`.
    /// @param _index A counter less than `totalSupply()`
    /// @return The token identifier for the `_index`th NFT,
    ///  (sort order not specified)
    function tokenByIndex(uint256 _index) external view returns (uint256);

    /// @notice Enumerate NFTs assigned to an owner
    /// @dev Throws if `_index` >= `balanceOf(_owner)` or if
    ///  `_owner` is the zero address, representing invalid NFTs.
    /// @param _owner An address where we are interested in NFTs owned by them
    /// @param _index A counter less than `balanceOf(_owner)`
    /// @return The token identifier for the `_index`th NFT assigned to `_owner`,
    ///   (sort order not specified)
    function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
}

totalSupply

function totalSupply() external view returns (uint256);

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

tokenByIndex

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

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

条件

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

tokenOfOwnerByIndex

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で定義されている関数やイベントは必須の機能なので他の規格を見るときに覚えておくと良いです。

コードは以下になります。

長くなってしまうので、コメントを取っ払っています。

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/IERC721.sol)

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と同じですね。

コードは以下になります。

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC721/extensions/IERC721Metadata.sol)

pragma solidity ^0.8.0;

import "../IERC721.sol";

/**
 * @title ERC-721 Non-Fungible Token Standard, optional metadata extension
 * @dev See https://eips.ethereum.org/EIPS/eip-721
 */
interface IERC721Metadata is IERC721 {
    /**
     * @dev Returns the token collection name.
     */
    function name() external view returns (string memory);

    /**
     * @dev Returns the token collection symbol.
     */
    function symbol() external view returns (string memory);

    /**
     * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token.
     */
    function tokenURI(uint256 tokenId) external view returns (string memory);
}

IERC721Enumerable

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

拡張機能というように、オプションの機能なため実装するかどうかは任意です。

IERC721Enumerable

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

こちらもEIP721で定義されていたERC721Enumerableと同じですね。

ここまででわかるように、EIP721で定義されていたものは全てERC721ではinterfaceとして実装されています。

コードは以下になります。

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC721/extensions/IERC721Enumerable.sol)

pragma solidity ^0.8.0;

import "../IERC721.sol";

/**
 * @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
 * @dev See https://eips.ethereum.org/EIPS/eip-721
 */
interface IERC721Enumerable is IERC721 {
    /**
     * @dev Returns the total amount of tokens stored by the contract.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns a token ID owned by `owner` at a given `index` of its token list.
     * Use along with {balanceOf} to enumerate all of ``owner``'s tokens.
     */
    function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256);

    /**
     * @dev Returns a token ID at a given `index` of all the tokens stored by the contract.
     * Use along with {totalSupply} to enumerate all tokens.
     */
    function tokenByIndex(uint256 index) external view returns (uint256);
}

ERC721

では、ERC721を実装している部分を確認していきましょう!

コードは以下に書かれています。

長くなってしまうので、コードは記事内に記載しません。

変数

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

// 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で紹介している関数と説明が被ってしまうので、すでに前章で解説した関数については説明を簡略させていただきます。

関数

  • constructor(name_, symbol_)
  • balanceOf(owner)
  • ownerOf(tokenId)
  • name()
  • symbol()
  • tokenURI(tokenId)
  • baseURI()
  • tokenOfOwnerByIndex(owner, index)
  • totalSupply()
  • tokenByIndex(index)
  • approve(to, tokenId)
  • getApproved(tokenId)
  • setApprovalForAll(operator, approved)
  • isApprovedForAll(owner, operator)
  • transferFrom(from, to, tokenId)
  • safeTransferFrom(from, to, tokenId)
  • safeTransferFrom(from, to, tokenId, _data)
  • _safeTransfer(from, to, tokenId, _data)
  • _exists(tokenId)
  • _isApprovedOrOwner(spender, tokenId)
  • _safeMint(to, tokenId)
  • _safeMint(to, tokenId, _data)
  • _mint(to, tokenId)
  • _burn(tokenId)
  • _transfer(from, to, tokenId)
  • _setTokenURI(tokenId, _tokenURI)
  • _setBaseURI(baseURI_)
  • _approve(to, tokenId)
  • _beforeTokenTransfer(from, to, tokenId)
  • supportsInterface(interfaceId)
  • _registerInterface(interfaceId)

constructor

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

ERC721トークンの名前とシンボルを設定しています。

constructorは、コントラクトがデプロイされた際に一度だけ実行されます。

balanceOf

function balanceOf(address owner) public view virtual override returns (uint256) {
    require(owner != address(0), "ERC721: address zero is not a valid owner");
    return _balances[owner];
}

ownerを引数にとり、ownerが所有しているERC721トークンの量を返す関数です。

ownerOf

function ownerOf(uint256 tokenId) public view virtual override returns (address) {
    address owner = _ownerOf(tokenId);
    require(owner != address(0), "ERC721: invalid token ID");
    return owner;
}

tokenIdというERC721のトークンIDを引数に取り、そのトークンIDを所有しているアドレスを返す関数です。

_ownerOf
function _ownerOf(uint256 tokenId) internal view virtual returns (address) {
    return _owners[tokenId];
}

ここで実際に_owners配列を参照して、tokenIdで指定されたERC721トークンの所有者アドレスを返す関数です。

name

function name() public view virtual override returns (string memory) {
    return _name;
}

ERC721トークンの名前を返す関数です。

symbol

function symbol() public view virtual override returns (string memory) {
    return _symbol;
}

ERC721トークンのシンボルを返す関数です。

_baseURI

function _baseURI() internal view virtual returns (string memory) {
    return "";
}

デフォルトでは””を返す関数です。

virtualとついているので好きな文字列を返すように変更可能です。

tokenURI

function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
    _requireMinted(tokenId);

    string memory baseURI = _baseURI();
    return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
}

tokenIdというERC721のトークンIDを引数に取り、トークンIDを持つERC721のURI(Uniform Resource Identifier)を返す関数です。

string(abi.encodePacked(baseURI, tokenId.toString()))の部分では文字列の連結を行なっています。

baseURI関数はデフォルトでは””が返ってくるので、baseURI関数を上書きして別の値が返ってくるようにすることもできます。

tokenURI関数で使用されている関数を一気に紹介します。

_exists
function _exists(uint256 tokenId) internal view virtual returns (bool) {
    return _ownerOf(tokenId) != address(0);
}

tokenIdで指定されたERC721トークンの所有者が0アドレスでないかを確認する関数です。

_requireMinted

function _requireMinted(uint256 tokenId) internal view virtual {
    require(_exists(tokenId), "ERC721: invalid token ID");
}

tokenIdで指定されたERC721トークンがすでにミントされているか確認する関数です。

approve

function approve(address to, uint256 tokenId) public virtual override {
    address owner = ERC721.ownerOf(tokenId);
    require(to != owner, "ERC721: approval to current owner");

    require(
        _msgSender() == owner || isApprovedForAll(owner, _msgSender()),
        "ERC721: approve caller is not token owner or approved for all"
    );

    _approve(to, tokenId);
}

approve関数の実行ユーザーが所有しているtokenIdのIDを持つERC721トークンをtransferする許可を与える関数です。

5~8行目では、approve関数の実行ユーザーがtokenIdのIDを持つERC721トークンの所有者、もしくはtokenIdのIDを持つERC721トークンの所有者から、所有しているすべてのERC721トークンの操作を許可されているアドレスかを確認しています。

_msgSender
function _msgSender() internal view virtual returns (address) {
    return msg.sender;
}

msg.senderを返す関数です。

以下のContext.solに書かれていて、ERC721.solではこのコントラクトを読み込んで継承しています。

_approve
function _approve(address to, uint256 tokenId) internal virtual {
    _tokenApprovals[tokenId] = to;
    emit Approval(ERC721.ownerOf(tokenId), to, tokenId);
}

_tokenApprovals配列にtokenIdtransfer許可を与えたアドレスを記録して、Approvalイベントを発行している関数です。

getApproved

function getApproved(uint256 tokenId) public view virtual override returns (address) {
    _requireMinted(tokenId);

    return _tokenApprovals[tokenId];
}

tokenIdのIDを持つERC721トークンの所有者が、transfer許可を与えたアドレスを確認する関数です。

setApprovalForAll

function setApprovalForAll(address operator, bool approved) public virtual override {
    _setApprovalForAll(_msgSender(), operator, approved);
}

setApprovalForAll関数の実行ユーザーが所有するERC721トークン全ての操作権限を、operatorに指定したアドレスに与える、もしくは取り除く関数です。

_setApprovalForAll
function _setApprovalForAll(address owner, address operator, bool approved) internal virtual {
    require(owner != operator, "ERC721: approve to caller");
    _operatorApprovals[owner][operator] = approved;
    emit ApprovalForAll(owner, operator, approved);
}

_operatorApprovals配列にoperatorで指定したアドレスが、ownerの所有するERC721の操作権限を与える、もしくは取り除く記録をしています。

approvedtrueであれば許可を与え、falseであれば許可を取り除いています。

最後にApprovalForAllイベントを呼び出しています。

isApprovedForAll

function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
    return _operatorApprovals[owner][operator];
}

operatorで指定したアドレスが、ownerで指定したアドレスが所有しているERC721トークンの操作権限があるかどうかを返す関数です。

transferFrom

function transferFrom(address from, address to, uint256 tokenId) public virtual override {
    //solhint-disable-next-line max-line-length
    require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");

    _transfer(from, to, tokenId);
}

fromで指定したアドレスが所有する、tokenIdで指定されたIDのERC721トークンをtoで指定したアドレスへ送る関数です。

_transfer
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);

    // Check that tokenId was not transferred by `_beforeTokenTransfer` hook
    require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");

    // Clear approvals from the previous owner
    delete _tokenApprovals[tokenId];

    unchecked {
        // `_balances[from]` cannot overflow for the same reason as described in `_burn`:
        // `from`'s balance is the number of token held, which is at least one before the current
        // transfer.
        // `_balances[to]` could overflow in the conditions described in `_mint`. That would require
        // all 2**256 token ids to be minted, which in practice is impossible.
        _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関数に0アドレスとtotokenId1という数値を渡して呼び出しています。

再度fromで指定したアドレスが、tokenIdで指定したIDのERC721トークンの所有者であるか確認しています。

ERC721トークンの所有者が変わるため、もし送ったERC721トークンの操作権限を他のアドレスに許可しているとよくないです。

そのため操作権限があるアドレスを記録する_tokenApprovals配列から削除しています。

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

uncheckedをつけないと、オーバーフローとアンダーフローを自動で防止してくれます。

ただし、その分ガス代がかかるため、uncheckedをつけることで上記の制御を行わない代わりにガス代が安くなります。

オーバーフローとアンダーフローについては以下の記事を参考にしてください。

_owners配列には、各ERC721トークンの所有者を記録しているため、データの更新しています。

Transferイベントを発行したのち、最後に_afterTokenTransfer関数に0アドレスとtotokenId1という数値を渡して呼び出しています。

_beforeTokenTransfer
function _beforeTokenTransfer(
    address from,
    address to,
    uint256 /* firstTokenId */,
    uint256 batchSize
) internal virtual {}

ERC721トークンを送る前に呼び出される関数です。

ERC721トークンを送る前に何か実行させたければここに記述します。

_afterTokenTransfer
function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal virtual {}

ERC721トークンを送った後に呼び出される関数です。

ERC721トークンを送った後に何か実行させたければここに記述します。

_isApprovedOrOwner
function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
    address owner = ERC721.ownerOf(tokenId);
    return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender);
}

spenderで指定されたアドレスが、tokenIdで指定したIDのERC721トークンの所有者か、操作権限を与えられたアドレスか確認している関数です。

safeTransferFrom

function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override {
    safeTransferFrom(from, to, tokenId, "");
}

fromで指定したアドレスが所有する、tokenIdで指定されたIDのERC721トークンをtoで指定したアドレスへ送る関数です。

ただここで1つ気になるのが、safeTransferFrom関数の中でsafeTransferFrom関数を呼び出していることです。

ん?無限ループしちゃうんじゃ…」と思う方は惜しいです!

よく見るとわかりますが、引数の数が違うことに気づくと思います。

solidityでは、引数の数が異なると同じ関数名でも別の関数として扱います。

そのため、safeTransferFrom関数と関数内で読んでいるsafeTransferFrom関数は別の関数になります。

関数内で呼んでいるsafeTransferFrom関数は以下になります。

safeTransferFrom

function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual override {
    require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
    _safeTransfer(from, to, tokenId, data);
}

fromで指定したアドレスが所有する、tokenIdで指定されたIDのERC721トークンをtoで指定したアドレスへ送る関数です。

dataという引数を4つ目にとっています。

_safeTransfer
function _safeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal virtual {
    _transfer(from, to, tokenId);
    require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer");
}

fromで指定したアドレスが所有する、tokenIdで指定されたIDのERC721トークンをtoで指定したアドレスへ安全に送る関数です。

安全にとはどういうことでしょうか?

実際に_checkOnERC721Received関数を見ながら解きほぐしていきましょう!

_checkOnERC721Received
function _checkOnERC721Received(
    address from,
    address to,
    uint256 tokenId,
    bytes memory data
) private returns (bool) {
    if (to.isContract()) {
        try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval) {
            return retval == IERC721Receiver.onERC721Received.selector;
        } catch (bytes memory reason) {
            if (reason.length == 0) {
                revert("ERC721: transfer to non ERC721Receiver implementer");
            } else {
                /// @solidity memory-safe-assembly
                assembly {
                    revert(add(32, reason), mload(reason))
                }
            }
        }
    } else {
        return true;
    }
}

細かい処理は置いておいて、この関数で行なっていることは「送り先がコントラクトの時、ERC721とサポートしているか確認し、もしサポートしていなかったら送らない」ということです。

さらに詳細の説明は前章で説明しています。

_safeMint

function _safeMint(address to, uint256 tokenId) internal virtual {
    _safeMint(to, tokenId, "");
}

tokenIdで指定したIDのERC721トークンを発行する関数です。

こちらもsafeTransferFrom関数同様、引数の数が異なる別の_safeMint関数を呼び出しています。

_safeMint
function _safeMint(address to, uint256 tokenId, bytes memory data) internal virtual {
    _mint(to, tokenId);
    require(
        _checkOnERC721Received(address(0), to, tokenId, data),
        "ERC721: transfer to non ERC721Receiver implementer"
    );
}

tokenIdで指定したIDのERC721トークンを発行して、toで指定したアドレスに送っています。

その後_checkOnERC721Received関数で「送り先がコントラクトの時、ERC721とサポートしているか確認し、もしサポートしていなかったら送らない」という確認をしています。

_mint
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);

    // Check that tokenId was not minted by `_beforeTokenTransfer` hook
    require(!_exists(tokenId), "ERC721: token already minted");

    unchecked {
        // Will not overflow unless all 2**256 token ids are minted to the same owner.
        // Given that tokens are minted one by one, it is impossible in practice that
        // this ever happens. Might change if we allow batch minting.
        // The ERC fails to describe this case.
        _balances[to] += 1;
    }

    _owners[tokenId] = to;

    emit Transfer(address(0), to, tokenId);

    _afterTokenTransfer(address(0), to, tokenId, 1);
}

ERC721トークンを発行する前にいくつかチェックや更新を行っています。

まず、送り先のアドレスが0アドレスではないかを確認し、tokenIdで指定したIDのERC721トークンがまだ存在しないか確認しています。

_beforeTokenTransfer関数に0アドレスとtotokenId1という数値を渡して呼び出しています。

再度、tokenIdで指定したIDのERC721トークンがまだ存在しないか確認しています。

_balances配列は各アドレスがいくつのERC721トークンを所有しているか記録してあるので、その値を更新しています。

_owners配列に各ERC721トークンの所有者のアドレスを記録しています。

Transferイベントを発行したのち、最後に_afterTokenTransfer関数に0アドレスとtotokenId1という数値を渡して呼び出しています。

ERC721Enumerable

では、次にERC721を実装している部分を確認していきましょう!

コードは以下になります。

長くなってしまうので、コメントは取っ払っています。

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/extensions/ERC721Enumerable.sol)

pragma solidity ^0.8.0;

import "../ERC721.sol";
import "./IERC721Enumerable.sol";

abstract contract ERC721Enumerable is ERC721, IERC721Enumerable {
    mapping(address => mapping(uint256 => uint256)) private _ownedTokens;

    mapping(uint256 => uint256) private _ownedTokensIndex;

    uint256[] private _allTokens;

    mapping(uint256 => uint256) private _allTokensIndex;

    function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721) returns (bool) {
        return interfaceId == type(IERC721Enumerable).interfaceId || super.supportsInterface(interfaceId);
    }

    function tokenOfOwnerByIndex(address owner, uint256 index) public view virtual override returns (uint256) {
        require(index < ERC721.balanceOf(owner), "ERC721Enumerable: owner index out of bounds");
        return _ownedTokens[owner][index];
    }

    function totalSupply() public view virtual override returns (uint256) {
        return _allTokens.length;
    }

    function tokenByIndex(uint256 index) public view virtual override returns (uint256) {
        require(index < ERC721Enumerable.totalSupply(), "ERC721Enumerable: global index out of bounds");
        return _allTokens[index];
    }

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 firstTokenId,
        uint256 batchSize
    ) internal virtual override {
        super._beforeTokenTransfer(from, to, firstTokenId, batchSize);

        if (batchSize > 1) {
            revert("ERC721Enumerable: consecutive transfers not supported");
        }

        uint256 tokenId = firstTokenId;

        if (from == address(0)) {
            _addTokenToAllTokensEnumeration(tokenId);
        } else if (from != to) {
            _removeTokenFromOwnerEnumeration(from, tokenId);
        }
        if (to == address(0)) {
            _removeTokenFromAllTokensEnumeration(tokenId);
        } else if (to != from) {
            _addTokenToOwnerEnumeration(to, tokenId);
        }
    }

    function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private {
        uint256 length = ERC721.balanceOf(to);
        _ownedTokens[to][length] = tokenId;
        _ownedTokensIndex[tokenId] = length;
    }

    function _addTokenToAllTokensEnumeration(uint256 tokenId) private {
        _allTokensIndex[tokenId] = _allTokens.length;
        _allTokens.push(tokenId);
    }

    function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private {

        uint256 lastTokenIndex = ERC721.balanceOf(from) - 1;
        uint256 tokenIndex = _ownedTokensIndex[tokenId];

        if (tokenIndex != lastTokenIndex) {
            uint256 lastTokenId = _ownedTokens[from][lastTokenIndex];

            _ownedTokens[from][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
            _ownedTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index
        }

        delete _ownedTokensIndex[tokenId];
        delete _ownedTokens[from][lastTokenIndex];
    }

    function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private {
        uint256 lastTokenIndex = _allTokens.length - 1;
        uint256 tokenIndex = _allTokensIndex[tokenId];

        uint256 lastTokenId = _allTokens[lastTokenIndex];

        _allTokens[tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
        _allTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index

        delete _allTokensIndex[tokenId];
        _allTokens.pop();
    }
}

では1つずつ確認していきましょう。

コントラクト定義

abstract contract ERC721Enumerable is ERC721, IERC721Enumerable {

ERC721Enumerableというコントラクトを定義しています。

ERC721IERC721Enumerableの2つを継承しています。

ERC721には前章で紹介した変数や関数が定義されています。

IERC721Enumerableには、ERC721Enumerableに実装に必要な変数や関数が定義されているinterfaceです。

abstractinterfaceと似ていますが、interfaceと違い関数の中身を記述することができます。

変数

// Mapping from owner to list of owned token IDs
mapping(address => mapping(uint256 => uint256)) private _ownedTokens;

// Mapping from token ID to index of the owner tokens list
mapping(uint256 => uint256) private _ownedTokensIndex;

// Array with all token ids, used for enumeration
uint256[] private _allTokens;

// Mapping from token id to position in the allTokens array
mapping(uint256 => uint256) private _allTokensIndex;

_ownedTokens

アドレス => インデックス番号 => ERC721トークンID」という配列で、各アドレスが保有しているERC721トークンを記録している配列。

インデックス番号は、以下のように純粋に配列の何番目かが記録されています。

[<address1>: [1: <tokenID>, 2: <tokenID>, 3: <tokenID>], <address2>: [1: <tokenID>], <address1>: [1: <tokenID>, 2: <tokenID>]]

_ownedTokensIndex

ERC721トークンID => インデックス番号」という配列で、各ERC721トークンが_ownedTokens配列内の各アドレスが持つ配列の何番目にあるかを記録している配列。

_allTokens

_ownedTokensIndex配列に記録されたERC721トークンのIDを記録する配列。

_allTokensIndex

各ERC721トークンが_allTokens配列の何番目に格納されているかを記録する配列。

supportsInterface

function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721) returns (bool) {
    return interfaceId == type(IERC721Enumerable).interfaceId || super.supportsInterface(interfaceId);
}

引数で渡されたinterfaceIdIERC721EnumerableinterfaceIdと一致するか確認する関数。

別のコントラクトがIERC721Enumerableをサポートしているか確認するために使用される。

tokenOfOwnerByIndex

function tokenOfOwnerByIndex(address owner, uint256 index) public view virtual override returns (uint256) {
    require(index < ERC721.balanceOf(owner), "ERC721Enumerable: owner index out of bounds");
    return _ownedTokens[owner][index];
}

ownerに渡されたアドレスが所有するERC721トークン配列の、indexに渡された数値の位置に格納されたERC721トークンIDを返す関数。

ownerが保有するERC721トークンよりも大きい数値をindexに指定するとエラーが返されます。

totalSupply

function totalSupply() public view virtual override returns (uint256) {
    return _allTokens.length;
}

_allTokensの配列の長さを返す関数。

コントラクト内の_allTokensに記録されているERC721トークンの量を返すことができます。

tokenByIndex

function tokenByIndex(uint256 index) public view virtual override returns (uint256) {
    require(index < ERC721Enumerable.totalSupply(), "ERC721Enumerable: global index out of bounds");
    return _allTokens[index];
}

_allTokensindexに渡されたインデックス番号に格納されているERC721トークンのIDを返す関数。

_allTokens配列の長さよりも大きい数値をindexに渡すとエラーを返します。

_addTokenToOwnerEnumeration

function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private {
    uint256 length = ERC721.balanceOf(to);
    _ownedTokens[to][length] = tokenId;
    _ownedTokensIndex[tokenId] = length;
}

ERC721トークンが発行された際に実行され、配列に記録する関数。

_ownedTokens配列のtoで指定されたアドレスの一番後ろにtokenIdに渡されてERC721トークンのIDを格納しています。

その後、_ownedTokensIndex配列にtokenIdをキーにして、_ownedTokensに格納されたERC721トークンのIDのインデックス番号を格納しています。

_addTokenToAllTokensEnumeration

function _addTokenToAllTokensEnumeration(uint256 tokenId) private {
    _allTokensIndex[tokenId] = _allTokens.length;
    _allTokens.push(tokenId);
}

ERC721トークンが発行された際に実行され、配列に記録する関数。

_allTokensIndex配列に、tokenIdに渡されたERC721トークンのIDをキーに_allTokensの配列に長さを格納します。

その後、_allTokens配列にERC721トークンのIDを末尾に追加します。

_removeTokenFromOwnerEnumeration

function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private {
    // To prevent a gap in from's tokens array, we store the last token in the index of the token to delete, and
    // then delete the last slot (swap and pop).

    uint256 lastTokenIndex = ERC721.balanceOf(from) - 1;
    uint256 tokenIndex = _ownedTokensIndex[tokenId];

    // When the token to delete is the last token, the swap operation is unnecessary
    if (tokenIndex != lastTokenIndex) {
        uint256 lastTokenId = _ownedTokens[from][lastTokenIndex];

        _ownedTokens[from][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
        _ownedTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index
    }

    // This also deletes the contents at the last position of the array
    delete _ownedTokensIndex[tokenId];
    delete _ownedTokens[from][lastTokenIndex];
}

ERC721トークンの保有者が変更された時、_ownedTokensIndex配列と_ownedTokens配列の記録を更新する関数。

uint256 lastTokenIndex = ERC721.balanceOf(from) - 1;
uint256 tokenIndex = _ownedTokensIndex[tokenId];

fromに渡されたアドレスが保有しているERC721トークンの量を取得し、マイナス1してlastTokenIndex変数に格納します。

その後、tokenIdに渡されたERC721トークンのIDのインデックス番号を取得してtokenIndex変数に格納しています。

// When the token to delete is the last token, the swap operation is unnecessary
if (tokenIndex != lastTokenIndex) {
    uint256 lastTokenId = _ownedTokens[from][lastTokenIndex];

    _ownedTokens[from][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
    _ownedTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index
}

tokenIndex変数とlastTokenId変数の値が異なるとき、各配列を更新しています。

fromに渡されたアドレスが保有するERC721トークンの一番末尾のインデックス番号を取得して、lastTokenId変数に格納しています。

その後、_ownedTokens配列の末尾のERC721のトークンIDを、fromが他のアドレスに送ったERC721トークンのインデックス番号の位置に格納し、_ownedTokensIndex配列のlastTokenIdのインデックス番号も変更しています。

この処理を行うことで、ERC721トークンの持ち主が変わっても、配列内の過去の情報を消すことができます。

// This also deletes the contents at the last position of the array
delete _ownedTokensIndex[tokenId];
delete _ownedTokens[from][lastTokenIndex];

最後に_ownedTokensIndex配列と_ownedTokens配列の重複しているデータと不要なデータを削除しています。

_removeTokenFromAllTokensEnumeration

function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private {
    // To prevent a gap in the tokens array, we store the last token in the index of the token to delete, and
    // then delete the last slot (swap and pop).

    uint256 lastTokenIndex = _allTokens.length - 1;
    uint256 tokenIndex = _allTokensIndex[tokenId];

    // When the token to delete is the last token, the swap operation is unnecessary. However, since this occurs so
    // rarely (when the last minted token is burnt) that we still do the swap here to avoid the gas cost of adding
    // an 'if' statement (like in _removeTokenFromOwnerEnumeration)
    uint256 lastTokenId = _allTokens[lastTokenIndex];

    _allTokens[tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
    _allTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index

    // This also deletes the contents at the last position of the array
    delete _allTokensIndex[tokenId];
    _allTokens.pop();
}

ERC721トークンが燃やされた時、_allTokens配列と_allTokensIndex配列の記録を更新する関数。

uint256 lastTokenIndex = _allTokens.length - 1;
uint256 tokenIndex = _allTokensIndex[tokenId];

_allTokens配列の最後尾のインデックス番号をlastTokenIndex変数に格納しています。

その後、_allTokensIndex配列のtokenIdに渡された、ERC721トークンのIDのインデックス番号をtokenIndex変数に格納しています。

uint256 lastTokenId = _allTokens[lastTokenIndex];

_allTokens配列の末尾のERC721トークンのIDをlastTokenId変数に格納しています。

_allTokens[tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
_allTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index

_allTokens配列のtokenIndex_allTokens配列の末尾のERC721トークンのIDを格納しています。

その後、_allTokensIndex配列のERC721トークンのインデックス番号を更新しています。

delete _allTokensIndex[tokenId];
_allTokens.pop();

_allTokensIndex配列と_allTokens配列の重複データを削除しています。

_beforeTokenTransfer

function _beforeTokenTransfer(
    address from,
    address to,
    uint256 firstTokenId,
    uint256 batchSize
) internal virtual override {
    super._beforeTokenTransfer(from, to, firstTokenId, batchSize);

    if (batchSize > 1) {
        // Will only trigger during construction. Batch transferring (minting) is not available afterwards.
        revert("ERC721Enumerable: consecutive transfers not supported");
    }

    uint256 tokenId = firstTokenId;

    if (from == address(0)) {
        _addTokenToAllTokensEnumeration(tokenId);
    } else if (from != to) {
        _removeTokenFromOwnerEnumeration(from, tokenId);
    }
    if (to == address(0)) {
        _removeTokenFromAllTokensEnumeration(tokenId);
    } else if (to != from) {
        _addTokenToOwnerEnumeration(to, tokenId);
    }
}

ERC721に実装されている_beforeTokenTransfer関数を上書きして、各配列の記録を変更する関数。

_beforeTokenTransfer関数は、ERC721トークンを送る前に呼び出される関数です。

super._beforeTokenTransfer(from, to, firstTokenId, batchSize);

この部分で継承元であるERC721コントラクト内に実装されている_beforeTokenTransfer関数を実行しています。

superは継承元のコントラクトを呼び出すものです。

if (batchSize > 1) {
    // Will only trigger during construction. Batch transferring (minting) is not available afterwards.
    revert("ERC721Enumerable: consecutive transfers not supported");
}

一度のトランザクションで複数のERC721トークンを送りたい場合に、送りたいERC721トークンの数をbatchSizeに指定します。

これにより複数トランザクションが走らなくなり、ガス代を節約できます。

しかし、ERC721Enumerableではこの機能はサポートされていません。

そのため、batchSizeに指定された値が1よりも大きいとエラーを返します。

uint256 tokenId = firstTokenId;

if (from == address(0)) {
    _addTokenToAllTokensEnumeration(tokenId);
} else if (from != to) {
    _removeTokenFromOwnerEnumeration(from, tokenId);
}
if (to == address(0)) {
    _removeTokenFromAllTokensEnumeration(tokenId);
} else if (to != from) {
    _addTokenToOwnerEnumeration(to, tokenId);
}

fromに渡されたアドレスが0アドレス(ミント)の時、_addTokenToAllTokensEnumeration関数を実行します。

fromに渡されたアドレスとtoに渡されたアドレスが異なれば、_removeTokenFromOwnerEnumeration関数と_addTokenToOwnerEnumeration関数を実行します。

toに渡されたアドレスが0アドレス(バーン)の時、_removeTokenFromAllTokensEnumeration関数を実行します。

ERC721の実装

前章まででERC721のコードを確認してきました。

この章から実際にERC721を実装していきましょう!

// 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エディタがブラウザ上で開きます。

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

いかがだったでしょうか?

ちゃんとERC721内の関数が使えていましたね!

全ての関数を実行したかったのですが、そうするとこの記事が異常なほど長くなってしまうので今回は控えておきます。

是非みなさんのお手元で実行してみてください!

最後に

今回は「ERC721」について解説していきました!

だいぶ長かったので、最後まで読んでいただきありがとうございます。

そしておつかれさまです!

ポイント

  • ERC721が何か理解できた
  • ERC721のコードを理解できた
  • ERC721の実装方法が理解できた

上記が当てはまっていれば嬉しいです!

もし何か質問などがあれば以下のTwitterなどからDMしてください!

もし何か質問などがあれば以下のTwitterなどから連絡ください!

普段はSolidityやブロックチェーン、Web3についての情報発信をしています。

Twiiterでは気になった記事などを共有しているので、ぜひフォローしてくれると嬉しいです!

参考

-Smart Contract, Solidity, ブロックチェーン
-,