こんにちは!CryptoGamesというブロックチェーンゲーム企業でエンジニアをしているかるでねです!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
このブログ以外でも情報発信しているので、よければ他の記事も見ていってください。
https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58
今回は「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コレクションを一覧で紹介していきます。
ERC721規格のNFTコレクション
知っているNFTコレクションがあるのではないでしょうか?
どのコレクションも有名ですが、ここに載せていないだけで他の有名コレクションもERC721規格を採用しています。
以下のページからERC721規格のNFTを一覧で見ることができます。
https://etherscan.io/tokens-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について詳しくは以下に書かれています。
https://eips.ethereum.org/EIPS/eip-165
interface
interface
とは、コントラクトに似ていますが実行することはできません。
interface
には関数名やひきすうなどのみ定義されていて中身は一切定義されていません。
そのため、interface
を継承したコントラクト内で関数を再度定義して中身を記述する必要があります。
「interface
いらなくね?」
こう思う方もいると思います。
interface
を使用するメリットは、実装で必要な関数を確認できることにあります。
最初で述べたように、interface
には実装する上で必要な関数が定義されているので、開発者やコントラクトを実行するユーザーからどんな機能があるのかを簡単に確認できます。
有名なプロジェクトでもinterface
はよく使用されているので、この機会に理解しておくと後々役に立ちます。
公式ドキュメントは以下になります。
https://solidity-jp.readthedocs.io/ja/latest/contracts.html#interfaces
識別子
NFTには1つずつ固有の番号を持っています。
この番号は重複しなければどのような番号をそれぞれのNFTにつけても問題ありません。
正し一度決まった番号を変更することはできません。
NFTごとに単純に1ずつ増やす方法もありますが、IDは基本的にブラックボックスとして扱う必要があります
そのため、呼び出しがわはID番号に特定のパターンがあると仮定してはいけません。
ERC721
interface
について確認できたところで、EIP721に書かれているコードの詳細を見ていこうと思います!
以下を参考にしながら進めていきます。
https://eips.ethereum.org/EIPS/eip-721
以下がコードになります。
長くなってしまうので、コメントを取っ払っています。
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におけるイベントとは、簡単にいうと「スマートコントラクトでの動作結果をフロントに伝える」役割を持った機能です。
詳しくは以下の公式ドキュメントを参考にしてください。
https://solidity-jp.readthedocs.io/ja/latest/contracts.html#events
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の管理権限を与える、もしくは無効にする関数。
_approved
がtrue
の時は権限を与えて、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
を実装しているか確認する関数。
interface
IDに確認したいERC165で規定されているinterface
識別しを渡して確認する。
interface
IDが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
最後に拡張機能について紹介していきます。
拡張機能のため実装は任意です。
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
として実装されています。
コードは以下になります。
OpenZeppelin-IERC721Enumerable.sol
// 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を実装している部分を確認していきましょう!
コードは以下に書かれています。
長くなってしまうので、コードは記事内に記載しません。
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol
変数
まずは変数を紹介します。
// 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ではこのコントラクトを読み込んで継承しています。
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Context.sol
_approve
function _approve(address to, uint256 tokenId) internal virtual {
_tokenApprovals[tokenId] = to;
emit Approval(ERC721.ownerOf(tokenId), to, tokenId);
}
_tokenApprovals
配列にtokenId
とtransfer
許可を与えたアドレスを記録して、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の操作権限を与える、もしくは取り除く記録をしています。
approved
がtrue
であれば許可を与え、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アドレスとto
、tokenId
、1
という数値を渡して呼び出しています。
再度from
で指定したアドレスが、tokenId
で指定したIDのERC721トークンの所有者であるか確認しています。
ERC721トークンの所有者が変わるため、もし送ったERC721トークンの操作権限を他のアドレスに許可しているとよくないです。
そのため操作権限があるアドレスを記録する_tokenApprovals
配列から削除しています。
各アドレスがいくつのERC721トークンを所有しているか記録している_balances
配列を更新しています。
unchecked
をつけないと、オーバーフローとアンダーフローを自動で防止してくれます。
ただし、その分ガス代がかかるため、unchecked
をつけることで上記の制御を行わない代わりにガス代が安くなります。
オーバーフローとアンダーフローについては以下の記事を参考にしてください。
_owners
配列には、各ERC721トークンの所有者を記録しているため、データの更新しています。
Transfer
イベントを発行したのち、最後に_afterTokenTransfer
関数に0アドレスとto
、tokenId
、1
という数値を渡して呼び出しています。
_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アドレスとto
、tokenId
、1
という数値を渡して呼び出しています。
再度、tokenId
で指定したIDのERC721トークンがまだ存在しないか確認しています。
_balances
配列は各アドレスがいくつのERC721トークンを所有しているか記録してあるので、その値を更新しています。
_owners
配列に各ERC721トークンの所有者のアドレスを記録しています。
Transfer
イベントを発行したのち、最後に_afterTokenTransfer
関数に0アドレスとto
、tokenId
、1
という数値を渡して呼び出しています。
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というコントラクトを定義しています。
ERC721とIERC721Enumerableの2つを継承しています。
ERC721には前章で紹介した変数や関数が定義されています。
IERC721Enumerableには、ERC721Enumerableに実装に必要な変数や関数が定義されているinterface
です。
abstract
はinterface
と似ていますが、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トークンを記録している配列。
特定のアドレスがどのERC721トークンを所有しているかを取得することができます。
インデックス番号は、以下のように純粋に配列の何番目かが記録されています。
[<address1>: [0: <tokenID>, 1: <tokenID>, 2: <tokenID>], <address2>: [0: <tokenID>], <address1>: [0: <tokenID>, 1: <tokenID>]]
_ownedTokensIndex
「ERC721トークンID => インデックス番号」という配列で、各ERC721トークンが_ownedTokens
配列内の各アドレスが持つ配列の何番目にあるかを記録している配列。
各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);
}
引数で渡されたinterfaceId
がIERC721EnumerableのinterfaceId
と一致するか確認する関数。
別のコントラクトが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];
}
_allTokens
のindex
に渡されたインデックス番号に格納されている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では気になった記事などを共有しているので、ぜひフォローしてくれると嬉しいです!
参考
https://ethereum.org/ja/developers/docs/standards/tokens/erc-721/
https://eips.ethereum.org/EIPS/eip-721
https://ethereumnavi.com/2021/11/09/contract-study-2-solidity-erc721/#fn-safetransferfrom
https://note.com/standenglish/n/n09fd7dc58427
https://docs.openzeppelin.com/contracts/3.x/
https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC721