こんにちは!CryptoGamesというブロックチェーンゲーム企業でエンジニアをしているかるでねです!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
このブログ以外でも情報発信しているので、よければ他の記事も見ていってください。
https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58
今回は「ERC721A」について1からわかりやすく解説していきます!
「ERC721A」は名前の通り「ER721」に関連するものです。
有名NFTプロジェクトの「Azuki」が提案・実装したもので、国内外問わず他の有名NFTプロジェクトでも実装されています。
「ER721」にAがついただけですが、中身を理解するのに意外と時間がかかります。
この記事ではわかりやすく丁寧に解説していくので、最後までついてきてください!
2023年2月17日に開催した以下の勉強会で使用したスライドをこちらにも載せておきます。
https://cryptogames.connpass.com/event/273086/
ERC721とは
ERC721Aについて理解する前にERC721を理解する必要があります。
ERC721についてあまり理解できていない人は、以下の記事でわかりやすくまとめているので読んでみてください!
ERC721Enumerableとは
ERC721Enumerableを知っていますでしょうか?
ERC721Enumerableは簡単にいうと、「特定のアドレスが所有しているERC721トークンの一覧を取得できる」機能です。
なんとなく理解している方は以下の記事を参考にしていただけると嬉しいです!
ERC721Aとは
概要
ERC721は聞いたことがあっても、ERC721Aについて聞いたことがある人は少ないと思います。
ERC721Aは人気NFTプロジェクトである「Azuki」が提案した独自の規格です。
国内外の有名NFTプロジェクトでも採用されている規格です。
NFTプロジェクト
- NEO TOKYO PUNKS
- NEO Samurai Mokeys
- Moonbirds
- Dooplicator
- RTFKT
- goblintown
- adidas IMPOSSIBLE BOX
ERC721Aを一言でまとめると、「ERC721Enumerableのインターフェースを保持しつつ、複数のNFTを発行する際のガス代を削減できる」規格です。
ではまずERC721Aの特徴から確認していきましょう。
特徴
ERC721Aの特徴は以下の3つになります。
ERC721Aの特徴
- ERC721Enumerableのインターフェースを保持。
- ERC721Enumerableから重複するストレージの削除。
- ERC721トークン所有者の残高や所有者データ更新をまとめて行う。
では1つずつ確認していきましょう。
*OpneZeepelinでのERC721 / ERC721Enumerableの実装を前提に話を進めていきます。
ERC721Enumerableのインターフェースを保持
前章でERC721Enumerableについて以下のように簡単に説明しました。
「特定のアドレスが所有しているERC721トークンの一覧を取得できる」
ERC721AはERC721Enumerableを外すという選択をせず、「全てのトークンの中から特定のアドレスが所有しているトークンを探し出す」という形でERC721Enumerableと同じことを実現しています。
ERC721Enumerableから重複するストレージの削除
スマートコントラクトでは、ストレージに情報を追加したり更新する際にガス代がかかります。
ERC721Enumerableでは、ERC721トークン1つにつきトークンの発行や送る際、複数のデータを更新します。
更新するデータについては、以下を参考にしてください。
ERC721Enumerableでは、データの更新に大きなコストをかけることで、データの読み込みを最適化する方法をとっています。
ただ、データの読み込みにはガス代がかからない、もしくはガス代がほとんどかからないため、理想的な実装方法とは言えません。
また、ERC721トークンにはトークンIDという一意の番号がついているため、より良い実装方法が考えられます(詳しくは次の項目で解説します)。
ERC721トークン所有者の残高や所有者データ更新をまとめて行う
例えばあなたが2つのトークンを所有していて、追加で3つのトークンを購入したいとします。
ERC721Enumerableでは以下のように、所有トークンの数などのデータを1つずつ値を更新しなければいけないためガス代がかかります。
2→3→4→5
しかし、実際以下のように何回も更新せずに一気にデータを更新できた方がガス代が節約できます。
2→5
比較的単純なコンセプトですが、OpenZeppelinでの実装では、バッチミント(1つのトランザクションで複数のNFTをミントできる)が実装されていません。
また、トークンの所有者データも一度に更新できた方がガス代が節約できます。
先ほどと同じようにあなたが2つのトークンを所有していて、追加で3つのトークンを購入したいとします。
ERC721であれば、各トークンの保有者データを以下のように1つずつ更新する必要があります。
この部分も「トークンIDが50~55の所有者は0xabc123…というアドレス」というように保存できればガス代を節約できます。
ただし、この実装は複数のトークンを同時にMintする際にのみ効力を発揮するため、1つずつMintする際はERC721とかわりません。
ERC721Aのガス代
ではこの章の最後にERC721Aを使用したときと、ERC721Enumerableを使用したときのガス代を比較してみましょう。
以下の公式のページにて掲載されている画像を使用して解説していきます。
ERC721Enumerableを使用した場合は、トークンを発行するごとにガス代が大きくかかっています。
一方、ERC721Aの場合は半分以下のガス代で済んでいます。
ドルに換算したものも合わせて掲載されているので、そちらも確認しましょう。
記事執筆時点での価格が記載されていますが、ガス代をだいぶ節約できているのが確認できます。
ERC721Aの仕組み
前章ではERC721Aの概要と特徴を確認しました。
この章ではERC721Aの仕組みをもう少し詳しく見ていきます。
この章で使用している画像は以下の記事をめちゃくちゃ参考にして作らせていただきました。
Azukiが開発したNFT規格ERC712Aは何を行なっているのか?
トークン発行
ERC721
前章で「ERC721Aでは、トークン発行時に所有者データを一括更新できる」ということを書きました。
ERC721では以下のように、トークンごとに所有者が紐付けられています。
このように連続した複数のトークンを発行するとき、トークンごとに所有者情報を記録しているとガス代がトークン分発生します。
「連続した複数のトークン」というのは、「1, 2, 3のように連続したトークンIDの複数のトークン」ということです。
ERC721Aではこの部分を改善する方法を提案しています。
ERC721A
ERC721Aでは、以下のように連続したトークンを発行するとき、連続したトークンの中の最初のトークンIDのみ記録する方法をとっています。
このようにすることで、ERC721のときトークンごとにデータの更新が発生した箇所を、実質1回の実行にまとめることができています。
また、アドレスがいくつのトークンを所有しているかのデータも、「2→5」のようにまとめて更新できます。
ちなみに「1, 2, 5, 6」というトークンIDのトークンを発行したときは、以下のように記録します。
ではこのデータをどのように取得するのでしょうか?
記録されているトークンの所有者データは取得できても、連続している記録されていないトークンの所有者データはどのように取得するのでしょうか?
トークン取得
ではここからあるアドレスが所有しているトークンの取得方法を紹介していきます。
この部分は結構単純で、全てのトークンをチェックしています。
以下の図のように、発行済みのトークン1つずつを確認して該当のアドレスが存在した部分を起点とし、別のアドレスが現れるまでのトークンIDを取得するということをトークンの数だけ繰り返します。
上記の実装はストレージデータの更新は行われないため、そこまで大きなガス代は発生しませんが、膨大な量のトークンが発行されているときは注意が必要になります。
トークン更新
では最後にトークンの所有者が変わったとき、どのような処理がされるのかをみていきましょう。
以下の図の時にトークンIDが3
のトークンを別のアドレスに送ったとします。
所有者のアドレスを記録するとともに、トークンIDが4
のトークンの所有者も更新する必要があります。
その理由としては、トークンIDが3
のトークンの所有者データのみ更新されてしまうと、以下の図のトークンIDが4
と5
のトークンの所有者が「0xC…」になってしまうからです。
ではトークンIDが3
のトークンと4
のトークンの所有者を更新してみましょう。
最終的には以下の図のようになります。
ERC721Aのコード確認
ではこの章から実際にERC721Aのコードを確認していきましょう!
コードは以下になります。
ERC721Aでは、ERC721でも使用されているコードも存在するため、その部分の解説は簡単にさせていただきます。
詳しく確認したい方は以下の記事を参考にしてください。
Command + F
で確認したい関数名を入れていただくことで簡単にページ内検索ができます。
Struct
struct TokenApprovalRef {
address value;
}
トークンを送る許可を与えたアドレスが格納されます。
カスタマイズして、アドレス以外の情報を格納することもできます。
STORAGE
_currentIndex
uint256 private _currentIndex;
次に発行されるトークンのトークンIDが格納されています。
トークンが発行されるたびにインクリメントされます。
_burnCounter
uint256 private _burnCounter;
燃やされたトークンの数が格納されています。
トークンが燃やされるたびにインクリメントされます。
_name, _symbol
string private _name;
string private _symbol;
ERC721トークンの名前とシンボルが格納されます。
_packedOwnerships
mapping(uint256 => uint256) private _packedOwnerships;
トークンIDをトークン所有者データを紐付ける構造体です。
トークンIDに紐づく所有者データが空の場合、前章までで解説してきたように所有者がいないとかは限りません。
連続したトークンIDを所有している場合、一番トークンIDの値が小さい部分にのみ所有者データが格納されるためです。
値として以下のような値が格納されます。
// - [0..159] `addr`
// - [160..223] `startTimestamp`
// - [224] `burned`
// - [225] `nextInitialized`
// - [232..255] `extraData`
上記のような構成されたデータを256ビットに変換して格納しています。
このデータを取り出すときは、シフト演算とビット演算を行い、「所有者アドレス」や「燃やされているのか」などのデータを取得することができます。
_packedAddressData
mapping(address => uint256) private _packedAddressData;
トークンの所有者のアドレスをキーにして、所有しているトークンの量やいくつのトークンを発行したかなどの情報で構成されたデータを256ビットに変換した値が格納されています。
構成は以下のようになっています。
// - [0..63] `balance`
// - [64..127] `numberMinted`
// - [128..191] `numberBurned`
// - [192..255] `aux`
_tokenApprovals
mapping(uint256 => TokenApprovalRef) private _tokenApprovals;
トークンIDをキーにして、トークンを送る許可されたアドレスが格納されているTokenApprovalRef
構造体を値に格納しています。
_operatorApprovals
mapping(address => mapping(address => bool)) private _operatorApprovals;
トークン所有者のアドレスをキーにして、あるアドレスが自身では所有していないトークンを送る許可が与えられているかという配列を値に格納しています。
ERC721にも同じような配列が定義されています。
CONSTRUCTOR
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
_currentIndex = _startTokenId();
}
コントラクトのデプロイ時に一度だけ実行される関数です。
ERC721トークンの名前とシンボルを引数にとり、_startTokenId
関数を実行して_currentIndex
にトークンIDの初期値を入れています。
トークンIDの初期値はコントラクト作成者が自由に決めることができます。
基本的に1からカウントしていくことが多いですが、1000からカウントすることも可能です。
TOKEN COUNTING OPERATIONS
_startTokenId
function _startTokenId() internal view virtual returns (uint256) {
return 0;
}
トークンIDの初期値を返す関数。
_nextTokenId
function _nextTokenId() internal view virtual returns (uint256) {
return _currentIndex;
}
次に発行されるトークンのIDが格納された_currentIndex
を返す関数。
totalSupply
function totalSupply() public view virtual override returns (uint256) {
unchecked {
return _currentIndex - _burnCounter - _startTokenId();
}
}
現在発行されていて燃やされていないトークンの量を返す関数。
_currentIndex
で次に発行されるトークンのIDを取得し、燃やされたトークンの数が格納されている_burnCounter
と_startTokenId
関数から得られるトークンIDの初期値を引くことで、現在発行されていて燃やされていないトークンの量を計算することができます。
_totalMinted
function _totalMinted() internal view virtual returns (uint256) {
unchecked {
return _currentIndex - _startTokenId();
}
}
今まで発行されてきたトークンの量を返す関数。
_currentIndex
で次に発行されるトークンのIDを取得し、_startTokenId
関数から得られるトークンIDの初期値を引くことで、現在までで発行されてきたトークンの量を計算することができます。
_totalBurned
function _totalBurned() internal view virtual returns (uint256) {
return _burnCounter;
}
燃やされたトークンの量を返す関数。
ADDRESS DATA OPERATIONS
balanceOf
function balanceOf(address owner) public view virtual override returns (uint256) {
if (owner == address(0)) _revert(BalanceQueryForZeroAddress.selector);
return _packedAddressData[owner] & _BITMASK_ADDRESS_DATA_ENTRY;
}
owner
に渡されたアドレスが所有するトークンの量を返す関数。
条件
owner
には0アドレスを指定できない。
_packedAddressData
配列から取得した値に対して、ビット演算を行なって値を計算しています。
####_numberMinted
function _numberMinted(address owner) internal view returns (uint256) {
return (_packedAddressData[owner] >> _BITPOS_NUMBER_MINTED) & _BITMASK_ADDRESS_DATA_ENTRY;
}
_numberBurned
function _numberBurned(address owner) internal view returns (uint256) {
return (_packedAddressData[owner] >> _BITPOS_NUMBER_BURNED) & _BITMASK_ADDRESS_DATA_ENTRY;
}
owner
に渡されたアドレスが燃やしたトークンの量を返す関数。
_packedAddressData
配列から取得した値に対して、シフト演算とビット演算を行なって値を計算しています。
_getAux
function _getAux(address owner) internal view returns (uint64) {
return uint64(_packedAddressData[owner] >> _BITPOS_AUX);
}
トークン所有者の補助データを返す関数。
ホワイトリストに登録されているアドレスが発行できるトークン量などの値を返します。
_setAux
unction _setAux(address owner, uint64 aux) internal virtual {
uint256 packed = _packedAddressData[owner];
uint256 auxCasted;
// Cast `aux` with assembly to avoid redundant masking.
assembly {
auxCasted := aux
}
packed = (packed & _BITMASK_AUX_COMPLEMENT) | (auxCasted << _BITPOS_AUX);
_packedAddressData[owner] = packed;
}
トークン所有者の補助データにデータを追加する関数。
ホワイトリストに登録されているアドレスが発行できるトークン量などの値を格納することができます。
格納する値はシフト演算・ビット演算を行いunit256
に変換しています。
IERC165
supportsInterface
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return
interfaceId == 0x01ffc9a7 || // ERC165 interface ID for ERC165.
interfaceId == 0x80ac58cd || // ERC165 interface ID for ERC721.
interfaceId == 0x5b5e139f; // ERC165 interface ID for ERC721Metadata.
}
この関数を呼び出したコントラクトがERC165かERC721、ERC721Metadataをサポートしているか確認する関数。
interfaceId
に渡されたコントラクトアドレスの先頭4ビットがERC165かERC721、ERC721Metadataに一致しているか確認しています。
IERC721Metadata
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トークンのシンボルを返す関数。
tokenURI
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
if (!_exists(tokenId)) _revert(URIQueryForNonexistentToken.selector);
string memory baseURI = _baseURI();
return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId))) : '';
}
ERC721トークンのtokenURI
を返す関数。
条件
- 存在しない
tokenId
を渡すことができない。
tokenURI
については以下の記事で詳しく解説しています。
_baseURI
function _baseURI() internal view virtual returns (string memory) {
return '';
}
ERC721トークンのbaseURI
を返す関数。
デフォルトは空なので、設定したければ追記して使用します。
OWNERSHIPS OPERATIONS
ownerOf
function ownerOf(uint256 tokenId) public view virtual override returns (address) {
return address(uint160(_packedOwnershipOf(tokenId)));
}
tokenId
に渡されたトークン所有者のアドレスを返す関数。
_ownershipOf
function _ownershipOf(uint256 tokenId) internal view virtual returns (TokenOwnership memory) {
return _unpackedOwnership(_packedOwnershipOf(tokenId));
}
tokenId
に渡されたトークンに紐づいたデータを構造体にして返す関数。
packedOwnerships構造体
- 所有者アドレス
- トークンが発行された時のタイムスタンプ
- バーンされているか
- 補助データ
上記の4つのデータを構造体にまとめて返します。
_ownershipAt
function _ownershipAt(uint256 index) internal view virtual returns (TokenOwnership memory) {
return _unpackedOwnership(_packedOwnerships[index]);
}
index
に渡されたインデックス番号を使用し、_ownershipOf
関数と同じ構造体を返す関数。
_initializeOwnershipAt
function _initializeOwnershipAt(uint256 index) internal virtual {
if (_packedOwnerships[index] == 0) {
_packedOwnerships[index] = _packedOwnershipOf(index);
}
}
index
に渡された値が0の時、_packedOwnerships
配列を初期化する関数。
トークン番号が0のトークンは存在しないため、_packedOwnerships
配列のキーが0に空のデータを格納しています。
_packedOwnershipOf
function _packedOwnershipOf(uint256 tokenId) private view returns (uint256 packed) {
if (_startTokenId() <= tokenId) {
packed = _packedOwnerships[tokenId];
// If not burned.
if (packed & _BITMASK_BURNED == 0) {
if (packed == 0) {
if (tokenId >= _currentIndex) _revert(OwnerQueryForNonexistentToken.selector);
for (;;) {
unchecked {
packed = _packedOwnerships[--tokenId];
}
if (packed == 0) continue;
return packed;
}
}
return packed;
}
}
_revert(OwnerQueryForNonexistentToken.selector);
}
tokenId
に渡されたトークンに紐づいた所有者や補助データをunit256に変換した値を返す関数。
条件
tokenId
に渡された値がトークンIDの初期値以上かつ、現在発行されているトークン量よりも小さい値でなければならない。- トークンが燃やされていない状態である。
_unpackedOwnership
function _unpackedOwnership(uint256 packed) private pure returns (TokenOwnership memory ownership) {
ownership.addr = address(uint160(packed));
ownership.startTimestamp = uint64(packed >> _BITPOS_START_TIMESTAMP);
ownership.burned = packed & _BITMASK_BURNED != 0;
ownership.extraData = uint24(packed >> _BITPOS_EXTRA_DATA);
}
以下の4つのデータを構造体にして返す関数。
packedOwnerships構造体
- 所有者アドレス
- トークンが発行された時のタイムスタンプ
- バーンされているか
- 補助データ
_packOwnershipData
function _packOwnershipData(address owner, uint256 flags) private view returns (uint256 result) {
assembly {
owner := and(owner, _BITMASK_ADDRESS)
result := or(owner, or(shl(_BITPOS_START_TIMESTAMP, timestamp()), flags))
}
}
以下の4つのデータをunit256に変換する関数。
packedOwnerships構造体
- 所有者アドレス
- トークンが発行された時のタイムスタンプ
- バーンされているか
- 補助データ
_nextInitializedFlag
function _nextInitializedFlag(uint256 quantity) private pure returns (uint256 result) {
assembly {
result := shl(_BITPOS_NEXT_INITIALIZED, eq(quantity, 1))
}
}
quantity
に発行するトークンの量が渡され、発行するトークンの量が1つなのか複数なのかを確認してunit256に変換して返す関数。
APPROVAL OPERATIONS
approve
function approve(address to, uint256 tokenId) public payable virtual override {
_approve(to, tokenId, true);
}
to
に渡されたアドレスに対して、tokenId
に渡されたトークンIDのトークンを送る許可を与える関数。
getApproved
function getApproved(uint256 tokenId) public view virtual override returns (address) {
if (!_exists(tokenId)) _revert(ApprovalQueryForNonexistentToken.selector);
return _tokenApprovals[tokenId].value;
}
tokenId
に渡されたトークンIDのトークンを送る許可を与えられたアドレスを返す関数。
条件
tokenId
には存在するトークンIDを指定しなければいけない。
setApprovalForAll
function setApprovalForAll(address operator, bool approved) public virtual override {
_operatorApprovals[_msgSenderERC721A()][operator] = approved;
emit ApprovalForAll(_msgSenderERC721A(), operator, approved);
}
operator
に渡されたアドレスに対して、setApprovalForAll
関数実行アドレスが所有するトークン全てを送る許可を与える関数。
isApprovedForAll
function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
return _operatorApprovals[owner][operator];
}
owner
に渡されたアドレスが所有する全てのトークンの送る許可を、operator
に渡されたアドレスに与えているか確認する関数。
_exists
function _exists(uint256 tokenId) internal view virtual returns (bool) {
return
_startTokenId() <= tokenId &&
tokenId < _currentIndex && // If within bounds,
_packedOwnerships[tokenId] & _BITMASK_BURNED == 0; // and not burned.
}
tokenId
に渡されたトークンIDのトークンが発行されていて、バーンされていないことを確認する関数。
_isSenderApprovedOrOwner
function _isSenderApprovedOrOwner(
address approvedAddress,
address owner,
address msgSender
) private pure returns (bool result) {
assembly {
owner := and(owner, _BITMASK_ADDRESS)
msgSender := and(msgSender, _BITMASK_ADDRESS)
result := or(eq(msgSender, owner), eq(msgSender, approvedAddress))
}
}
_isSenderApprovedOrOwner
関数を実行したアドレスが、トークンの所有者、もしくはトークンを送る許可を与えられているアドレスか確認する関数。
_getApprovedSlotAndAddress
function _getApprovedSlotAndAddress(uint256 tokenId)
private
view
returns (uint256 approvedAddressSlot, address approvedAddress)
{
TokenApprovalRef storage tokenApproval = _tokenApprovals[tokenId];
assembly {
approvedAddressSlot := tokenApproval.slot
approvedAddress := sload(approvedAddressSlot)
}
}
tokenId
に渡されたトークンIDのトークンを送る許可を与えられているアドレスと、そのアドレスが格納されているスロットの位置を返す関数。
approvedAddressSlot := tokenApproval.slot
approvedAddress := sload(approvedAddressSlot)
上記の箇所で、アドレスが格納されているストレージの箇所とアドレスを取得しています。
TRANSFER OPERATIONS
transferFrom
function transferFrom(
address from,
address to,
uint256 tokenId
) public payable virtual override {
uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId);
from = address(uint160(uint256(uint160(from)) & _BITMASK_ADDRESS));
if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector);
(uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId);
if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A()))
if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector);
_beforeTokenTransfers(from, to, tokenId, 1);
assembly {
if approvedAddress {
sstore(approvedAddressSlot, 0)
}
}
unchecked {
--_packedAddressData[from]; // Updates: `balance -= 1`.
++_packedAddressData[to]; // Updates: `balance += 1`.
_packedOwnerships[tokenId] = _packOwnershipData(
to,
_BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked)
);
if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) {
uint256 nextTokenId = tokenId + 1;
if (_packedOwnerships[nextTokenId] == 0) {
if (nextTokenId != _currentIndex) {
_packedOwnerships[nextTokenId] = prevOwnershipPacked;
}
}
}
}
uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS;
assembly {
log4(
0, // Start of data (0, since no data).
0, // End of data (0, since no data).
_TRANSFER_EVENT_SIGNATURE, // Signature.
from, // `from`.
toMasked, // `to`.
tokenId // `tokenId`.
)
}
if (toMasked == 0) _revert(TransferToZeroAddress.selector);
_afterTokenTransfers(from, to, tokenId, 1);
}
from
に渡されたアドレスから、to
に渡されたアドレスにtokenId
に渡されたトークンIDのトークンを送る関数。
safeTransferFrom
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) public payable virtual override {
safeTransferFrom(from, to, tokenId, '');
}
safeTransferFrom
関数を実行する関数。
safeTransferFrom
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes memory _data
) public payable virtual override {
transferFrom(from, to, tokenId);
if (to.code.length != 0)
if (!_checkContractOnERC721Received(from, to, tokenId, _data)) {
_revert(TransferToNonERC721ReceiverImplementer.selector);
}
}
transferFrom
関数を実行する関数。
条件
to
に0アドレスを指定できない。- 送り先がコントラクトの場合、送り先のコントラクトでERC721をサポートしている必要がある。
ERC721をサポート(実装)していないコントラクトに対してERC721トークンを送ってしまうと、誰もトークンを取り出すことができなくなってしまいます。
これを防ぐために送る前にチェックをしています。
_beforeTokenTransfers
function _beforeTokenTransfers(
address from,
address to,
uint256 startTokenId,
uint256 quantity
) internal virtual {}
トークンが送られる前に実行される関数。
トークンを送る前に何かしらの処理をしたいときは、この関数に追記します。
_afterTokenTransfers
function _afterTokenTransfers(
address from,
address to,
uint256 startTokenId,
uint256 quantity
) internal virtual {}
トークンが送られた後に実行される関数。
トークンが送られた後に何かしらの処理をしたいときは、この関数に追記します。
_checkContractOnERC721Received
function _checkContractOnERC721Received(
address from,
address to,
uint256 tokenId,
bytes memory _data
) private returns (bool) {
try ERC721A__IERC721Receiver(to).onERC721Received(_msgSenderERC721A(), from, tokenId, _data) returns (
bytes4 retval
) {
return retval == ERC721A__IERC721Receiver(to).onERC721Received.selector;
} catch (bytes memory reason) {
if (reason.length == 0) {
_revert(TransferToNonERC721ReceiverImplementer.selector);
}
assembly {
revert(add(32, reason), mload(reason))
}
}
}
safeTransferFrom
関数の部分でも説明したように、送り先がコントラクトの時ERC721をサポートしているか確認する関数。
MINT OPERATIONS
_mint
function _mint(address to, uint256 quantity) internal virtual {
uint256 startTokenId = _currentIndex;
if (quantity == 0) _revert(MintZeroQuantity.selector);
_beforeTokenTransfers(address(0), to, startTokenId, quantity);
unchecked {
_packedOwnerships[startTokenId] = _packOwnershipData(
to,
_nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0)
);
_packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1);
uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS;
if (toMasked == 0) _revert(MintToZeroAddress.selector);
uint256 end = startTokenId + quantity;
uint256 tokenId = startTokenId;
do {
assembly {
log4(
0, // Start of data (0, since no data).
0, // End of data (0, since no data).
_TRANSFER_EVENT_SIGNATURE, // Signature.
0, // `address(0)`.
toMasked, // `to`.
tokenId // `tokenId`.
)
}
} while (++tokenId != end);
_currentIndex = end;
}
_afterTokenTransfers(address(0), to, startTokenId, quantity);
}
to
に渡されたアドレスに対して、quantity
に渡された回数分ERC721トークンを送る処理をする関数。
一度のトランザクションで複数のERC721トークンを送ることができます。
_mintERC2309
function _mintERC2309(address to, uint256 quantity) internal virtual {
uint256 startTokenId = _currentIndex;
if (to == address(0)) _revert(MintToZeroAddress.selector);
if (quantity == 0) _revert(MintZeroQuantity.selector);
if (quantity > _MAX_MINT_ERC2309_QUANTITY_LIMIT) _revert(MintERC2309QuantityExceedsLimit.selector);
_beforeTokenTransfers(address(0), to, startTokenId, quantity);
unchecked {
_packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1);
_packedOwnerships[startTokenId] = _packOwnershipData(
to,
_nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0)
);
emit ConsecutiveTransfer(startTokenId, startTokenId + quantity - 1, address(0), to);
_currentIndex = startTokenId + quantity;
}
_afterTokenTransfers(address(0), to, startTokenId, quantity);
}
コントラクトのデプロイ時に効率的にERC721トークンを発行する関数。
ERC2309で定義されているConsecutiveTransfer
関数を実行して、複数の連続したERC721トークンを発行します。
コントラクトのデプロイ時以外にこの関数を呼び出すと、ERC721に準拠しなくなってしまいます。
詳しくは以下を参考にしてください。
_safeMint
function _safeMint(
address to,
uint256 quantity,
bytes memory _data
) internal virtual {
_mint(to, quantity);
unchecked {
if (to.code.length != 0) {
uint256 end = _currentIndex;
uint256 index = end - quantity;
do {
if (!_checkContractOnERC721Received(address(0), to, index++, _data)) {
_revert(TransferToNonERC721ReceiverImplementer.selector);
}
} while (index < end);
if (_currentIndex != end) _revert(bytes4(0));
}
}
}
複数のERC721トークンを発行できる関数。
この関数でも送り先がコントラクトの場合、送り先のコントラクトがERC721をサポートしているか確認しています。
_safeMint
function _safeMint(address to, uint256 quantity) internal virtual {
_safeMint(to, quantity, '');
}
_safeMint
関数を実行する関数。
APPROVAL OPERATIONS
_approve
function _approve(address to, uint256 tokenId) internal virtual {
_approve(to, tokenId, false);
}
_approve
関数を実行する関数。
_approve
function _approve(
address to,
uint256 tokenId,
bool approvalCheck
) internal virtual {
address owner = ownerOf(tokenId);
if (approvalCheck && _msgSenderERC721A() != owner)
if (!isApprovedForAll(owner, _msgSenderERC721A())) {
_revert(ApprovalCallerNotOwnerNorApproved.selector);
}
_tokenApprovals[tokenId].value = to;
emit Approval(owner, to, tokenId);
}
to
に渡されたアドレスに、_approve
関数を実行しているアドレスが所有する、tokenId
に渡されたトークンIDのトークンの送る許可を与える関数。
BURN OPERATIONS
_burn
function _burn(uint256 tokenId) internal virtual {
_burn(tokenId, false);
}
_burn
関数を実行する関数。
_burn
function _burn(uint256 tokenId, bool approvalCheck) internal virtual {
uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId);
address from = address(uint160(prevOwnershipPacked));
(uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId);
if (approvalCheck) {
if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A()))
if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector);
}
_beforeTokenTransfers(from, address(0), tokenId, 1);
assembly {
if approvedAddress {
sstore(approvedAddressSlot, 0)
}
}
er overflow is incredibly unrealistic as `tokenId` would have to be 2**256.
unchecked {
_packedAddressData[from] += (1 << _BITPOS_NUMBER_BURNED) - 1;
_packedOwnerships[tokenId] = _packOwnershipData(
from,
(_BITMASK_BURNED | _BITMASK_NEXT_INITIALIZED) | _nextExtraData(from, address(0), prevOwnershipPacked)
);
if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) {
uint256 nextTokenId = tokenId + 1;
if (_packedOwnerships[nextTokenId] == 0) {
if (nextTokenId != _currentIndex) {
_packedOwnerships[nextTokenId] = prevOwnershipPacked;
}
}
}
}
emit Transfer(from, address(0), tokenId);
_afterTokenTransfers(from, address(0), tokenId, 1);
unchecked {
_burnCounter++;
}
}
tokenId
に渡されたトークンIDのトークンを燃やす関数。
EXTRA DATA OPERATIONS
_setExtraDataAt
function _setExtraDataAt(uint256 index, uint24 extraData) internal virtual {
uint256 packed = _packedOwnerships[index];
if (packed == 0) _revert(OwnershipNotInitializedForExtraData.selector);
uint256 extraDataCasted;
assembly {
extraDataCasted := extraData
}
packed = (packed & _BITMASK_EXTRA_DATA_COMPLEMENT) | (extraDataCasted << _BITPOS_EXTRA_DATA);
_packedOwnerships[index] = packed;
}
_packedOwnerships
配列に格納されているデータに対して、直接補助データを設定する関数。
_extraData
function _extraData(
address from,
address to,
uint24 previousExtraData
) internal view virtual returns (uint24) {}
補助データを生成する関数。
デフォルトでは何も書かれていないため、補助データに含めたいものがあればこの関数に追記します。
_nextExtraData
function _nextExtraData(
address from,
address to,
uint256 prevOwnershipPacked
) private view returns (uint256) {
uint24 extraData = uint24(prevOwnershipPacked >> _BITPOS_EXTRA_DATA);
return uint256(_extraData(from, to, extraData)) << _BITPOS_EXTRA_DATA;
}
_packedOwnerships
配列に格納するデータの中の補助データを生成する、extraData
関数を呼び出す関数。
OTHER OPERATIONS
_msgSenderERC721A
function _msgSenderERC721A() internal view virtual returns (address) {
return msg.sender;
}
_msgSenderERC721A
関数を実行したアドレスを返す関数。
_toString
function _toString(uint256 value) internal pure virtual returns (string memory str) {
assembly {
let m := add(mload(0x40), 0xa0)
mstore(0x40, m)
str := sub(m, 0x20)
mstore(str, 0)
let end := str
for { let temp := value } 1 {} {
str := sub(str, 1)
mstore8(str, add(48, mod(temp, 10)))
temp := div(temp, 10)
if iszero(temp) { break }
}
let length := sub(end, str)
str := sub(str, 0x20)
mstore(str, length)
}
}
value
に渡されたunit256
型のデータをstring
型に変換して返す関数。
_revert
function _revert(bytes4 errorSelector) internal pure {
assembly {
mstore(0x00, errorSelector)
revert(0x00, 0x04)
}
}
エラーを返す時に呼び出される関数。
ERC721Aの実装
前章ではERC721Aのコードを確認してきました。
この記事の最後として、ERC721Aを実装していきましょう!
今回は以下のドキュメントを参考にしています。
https://chiru-labs.github.io/ERC721A/#/
環境構築
まずは最初に環境の構築をしていきます。
以下の記事の「環境構築」の章を参考にして実行してください。
$ npm i @openzeppelin/contracts
上記のコマンドは実行せず、代わりに以下を実行してください。
$ npm install --save-dev erc721a
ここまでできれば環境構築は完了です!
コントラクト作成
では次にコントラクトを作成していきましょう。
以下のコマンドを実行してください。
$ touch contracts/ERC721AContract.sol
上記実行したのち、作成したERC721AContract.sol
というファイルの中に以下を貼り付けてください。
pragma solidity ^0.8.4;
import "erc721a/contracts/ERC721A.sol";
import "erc721a/contracts/extensions/ERC721AQueryable.sol";
contract ERC721ANFT is ERC721A, ERC721AQueryable {
constructor() ERC721A("Azuki", "AZUKI") {}
function mint(uint256 quantity) external payable {
// `_mint`'s second argument now takes in a `quantity`, not a `tokenId`.
_mint(msg.sender, quantity);
}
}
簡単にコードを説明していきます。
import "erc721a/contracts/ERC721A.sol";
import "erc721a/contracts/extensions/ERC721AQueryable.sol";
ERC721A.sol
とERC721AQueryable.sol
を使用しています。
このファイルは環境構築時にインストールしたerc721a
というパッケージを使用することで読み込むことができます。
ERC721AQueryable.sol
には、「あるアドレスが所有しているERC721トークンの一覧を取得する」関数が格納されています。
constructor() ERC721A("Azuki", "AZUKI") {}
function mint(uint256 quantity) external payable {
// `_mint`'s second argument now takes in a `quantity`, not a `tokenId`.
_mint(msg.sender, quantity);
}
Azukiという名前、AZUKIというシンボルをコントラクトのデプロイ時に渡しています。
mint
関数は新たにトークンを発行するときに使用します。
引数のquantity
に一度にMintしたいトークンの量を渡すことで、指定した量のトークンを発行します。
これでコントラクトの作成は完了です。
テストファイルの作成
では次にテスト用のファイルを作成していきましょう。
以下のコマンドを実行してください。
$ mkdir test/test-ERC721.js
作成したtest-ERC721.js
に以下を貼り付けてください。
const { expect } = require("chai");
describe("ERC721A", function () {
beforeEach(async function () {
[owner, wallet1, wallet2] = await ethers.getSigners();
// ERC721Aコントラクトを取得。
ERC721A = await ethers.getContractFactory("ERC721ANFT", owner);
// ERC721Aコントラクトをデプロイ。
erc721A = await ERC721A.deploy();
// トークンをmintする。
await erc721A.connect(wallet1).mint(10);
await erc721A.connect(wallet2).mint(20);
await erc721A.connect(wallet1).mint(5);
expect(await erc721A.totalSupply()).to.equal(35);
});
describe("Check ERC721A Token", function () {
// トークンの総供給量を確認
it("Total Supply", async function () {
expect(await erc721A.totalSupply()).to.equal(35);
});
// Wallet1が所有しているトークンの総量を確認
it("Wallet1 BalanceOf", async function () {
expect(await erc721A.balanceOf(wallet1.address)).to.equal(15);
});
// Wallet2が所有しているトークンの総量を確認
it("Wallet2 BalanceOf", async function () {
expect(await erc721A.balanceOf(wallet2.address)).to.equal(20);
});
// トークン1の所有者を確認
it("Token1 ownerOf", async function () {
expect(await erc721A.ownerOf(1)).to.equal(wallet1.address);
});
// トークン15の所有者を確認
it("Token1 ownerOf", async function () {
expect(await erc721A.ownerOf(15)).to.equal(wallet2.address);
});
;
});
describe("Check ERC721A Token List", function () {
// wallet1が所有しているトークンの量を確認
it("wallet1 tokensOfOwner", async function () {
const wallet1Token = await erc721A.tokensOfOwner(wallet1.address);
expect(wallet1Token.length).to.equal(15);
});
// wallet2が所有しているトークンの量を確認
it("wallet1 tokensOfOwner", async function () {
const wallet2Token = await erc721A.tokensOfOwner(wallet2.address);
expect(wallet2Token.length).to.equal(20);
});
;
});
});
何をしているかを簡単にまとめておきます。
処理
wallet1
とwallet2
でトークンをそれぞれ発行。wallet1
とwallet2
が所有しているトークンの総量を確認。wallet1
とwallet2
が所有しているトークンのトークンIDを取得して、その量を確認。
何をやっているかの詳細はコメントでコード内に記載しているので、ぜひ読んでみてください!
テスト実行
テストファイルが作成できたので、最後にテストを実行してみましょう!
と、その前に不要なファイルを削除しましょう。
以下のコマンドを実行してください。
$ rm test/Lock.js
では気を取り直してテストを実行してみましょう。
以下のコマンドを実行してください。
$ npx hardhat test
以下のように出力されていれば無事テスト実行完了です!
ERC721A
Check ERC721A Token
✔ Total Supply
✔ Wallet1 BalanceOf
✔ Wallet2 BalanceOf
✔ Token1 ownerOf
✔ Token1 ownerOf
Check ERC721A Token List
✔ wallet1 tokensOfOwner (41ms)
✔ wallet1 tokensOfOwner (39ms)
7 passing (2s)
コード
コードは以下においているので、参考にしてください。
最後に
今回はERC721Aについて解説していきました。
だいぶ理解するのが難しかったと思いますがいかがだったでしょうか?
少しでも理解が深まっていたり、ERC721Aの実装の参考になれば幸いです!
もし何か質問などがあれば以下のTwitterなどからDMしてください!
普段はPythonやブロックチェーンメインに情報発信をしています。
Twiiterでは図解でわかりやすく解説する投稿をしているのでぜひフォローしてくれると嬉しいです!
Tweets by cardene777
参考
https://media.leadedge-c.com/articles/what-is-erc
https://hashhub-research.com/articles/20220506-erc721-a-r-o-psi
Azukiが開発したNFT規格ERC712Aは何を行なっているのか?