Metaverseのブランドを標榜し、1月13日に行われたPhase1セールで8700個のNFTが即完売したAzukiプロジェクト。このAzuki NFTは独自のERC721規格であるERC721Aを利用することでセール時のトランザクション手数料を大幅に削減することができました。
イーサリアムをはじめとしたEVMエコシステムにおけるNFTの代表的な規格であるERC721は、暗号資産のようなFungibleなトークンと比べて、多くのトランザクション手数料(いわゆるGas代)が発生します。
AzukiはERC721の内部構造をプロジェクトに最適化することで、発行時の手数料の少ないERC721互換のNFTを実現しています。本記事ではこのERC721Aについて解説していきます。なお、ソースコードはEtherscan上で公開されているもの、およびGitHubで公開されているものをベースにします。
ERC721Aの特徴を3つのパートに分けて説明していきます。
ERC721は、あるトークンから所有するアカウントの情報を持つ規格ではありますが、あるアカウントが所有するトークンの一覧をストレージのデータとして保持しているわけではありません。つまり、自身を含むスマートコントラクト上から、あるアカウントのトークン一覧を参照することができません。
そこで、ERC721EnumerableというERC721の拡張規格が提案されており、OpenZeppelin等にも広く利用されています。ERC721Enumerableでは、トークン一覧参照のためのデータをストレージに保存することで一覧情報を参照できるようにします。したがってシンプルなERC721に比べて、余分にストレージを利用することになるため、ERC721Enumerableを実装する場合は、transferやmintに必要となるgasが増加することになります。
Enumerableつまり、スマートコントラクトでの一覧表示の必要性の低い一点物のアートNFTや、一部ゲームのようにオフチェーンでの参照を前提とするエコシステムを構築する場合には、Enumebrableをあえて選択しないことで、mintや将来的なtransferのコストを下げる選択が考えられます。
Azukiは、将来的なオンチェーンエコシステムを鑑み、ERC721Enumerableを外すという選択はしませんでした。後述するように、単純な所有情報すらも発行時のストレージ利用に手を加えているため、全てのトークンの中から保有しているトークンを走査するというアプローチでERC721Enumerableを実現しています。
ERC721Aの目玉機能として、mintつまり新規発行時に複数同時に発行する場合のgasを大幅に削減できることを謳っています。
まず、スマートコントラクトの記憶領域であるストレージに情報を保存する際にgasを使用します。このgasに対してトランザクション作成時に指定するgas priceを掛け算することでトランザクションの手数料が発生します。
ERC721の場合、トークンIDに対してオーナー情報をスマートコントラクトのストレージ上に記録するため、トークンIDの数だけストレージを使用しmintやtransferの際にgasを使用します。
一方、ERC721Aでは複数のNFTのmintかつ連番での発行を前提としているため、その複数個の処理(バッチサイズ/BatchSize)のまとめ、一番若いトークンIDのみストレージを使用し、バッチサイズ-1のストレージの書き込みを先送りします。
上図の場合、0xAが5つのNFTをmintする際にトークンID 1にのみオーナー情報を記録し、2〜4のIDにオーナー情報は記録しません。オーナー情報を参照する際は、IDが若い順に走査することで、2〜4のトークンのオーナーは1のトークンオーナーと同じ、つまり0xAであることがわかるようになっています。
ただし、この実装は所有情報を書き込みの先送りです。例えばtransferによる譲渡が発生する場合は改めてオーナー情報をストレージに書き込むことになります。また、Azukiが言及しているように複数のトークンを同時にmintする際にのみ効力を発揮することがわかります。
上図では、トークンID 3のトークンを0xCへtransferします。このとき、トークンID 4も同時にストレージに書き込むことで、前述した参照時の実装に整合性を持たせるようになっています。
トークンID 4を0xAと書き込むことで、トークンID 5の所有情報に整合性がとれるようになっています。これを繰り返して全てのトークンが一度以上transferされたとき、ストレージの状況は標準のERC721と同じ状態になります。
このようにトークンごと個別にストレージに書き込むことでmintに負担があった従来のERC721に比べて、mint時の負担を極力減らすことを実現しており、ERC721Aの最大の特徴といえるでしょう。
前述したとおり、ERC721AではERC721Enumerableを実装しているため、スマートコントラクトから所有するトークン一覧を取得することができます。通常であればアカウントごとのトークン所有情報をストレージに追加保存し、tokenOfOwnerByIndex関数を実行することで一覧を取得します。
ERC721Aでは、ERC721Enumerableで発生する追加情報を保存せずにtokenOfOwnerByIndex実行時に全ての発行済みトークンを走査することで実現しています。これにより、Enemerableを実装しながらもmintおよびtransfer時のgasを大幅に削減することができます。
一方で、tokenOfOwnerByIndexが全ての発行済みトークンを走査するため、この関数をトランザクションの一部で実行した場合に、発行済みトークンの数だけ処理が実行されることになります。この処理は、ストレージの更新を伴わないため、1つ1つは大きなgasとなりませんが、膨大な数のトークンがある場合には注意が必要です。
Azukiのコントラクトには、10000以上のトークンがある場合は注意をする旨がコメントされています。
/**
* @dev See {IERC721Enumerable-tokenOfOwnerByIndex}.
* This read function is O(collectionSize). If calling from a separate contract, be sure to test gas first.
* It may also degrade with extremely large collection sizes (e.g >> 10000), test for your use case.
*/
function tokenOfOwnerByIndex(address owner, uint256 index)
public
view
override
returns (uint256)
{
require(index < balanceOf(owner), "ERC721A: owner index out of bounds");
uint256 numMintedSoFar = totalSupply();
uint256 tokenIdsIdx = 0;
address currOwnershipAddr = address(0);
for (uint256 i = 0; i < numMintedSoFar; i++) {
TokenOwnership memory ownership = _ownerships[i];
if (ownership.addr != address(0)) {
currOwnershipAddr = ownership.addr;
}
if (currOwnershipAddr == owner) {
if (tokenIdsIdx == index) {
return i;
}
tokenIdsIdx++;
}
}
revert("ERC721A: unable to get token of owner by index");
}
Azuki NFTをベースにERC721Aの解説を行いました。この規格はどのようなケースに使えるのでしょうか?
AzukiのTwitterの投稿やウェブサイトを見ると、このプロジェクトのモチベーションの一つに、NBA TopShotやポケモンカードパック開封の楽しみがあることがわかります。
トークンを購入する、後日パックを開封する、唯一の存在であるNFTに出会う、そのような体験を前提とした場合、複数個を購入することはある種当然の思想になります。
本記事で記述したとおり、ERC721Aはトランザクション手数料を下げる銀の弾丸のような実装であるわけではなく、プロジェクトの特性に合わせてERC721という規格のgas利用タイミングを最適化したものです。
イーサリアムの手数料の高騰が叫ばれて1年以上立ちますが、NFTを含めイーサリアム上のプロジェクトはまだまだ勢いを持っています。今後もERC規格を満たす特殊な実装が増えてイノベーションが起こっていくのでしょう!