Smart Contract Solidity ブロックチェーン

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

かるでね

こんにちは!CryptoGamesというブロックチェーンゲーム企業でエンジニアをしているかるでねです!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

このブログ以外でも情報発信しているので、よければ他の記事も見ていってください。

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

ERC721A」は名前の通り「ER721」に関連するものです。

有名NFTプロジェクトの「Azuki」が提案・実装したもので、国内外問わず他の有名NFTプロジェクトでも実装されています。

ER721」にAがついただけですが、中身を理解するのに意外と時間がかかります。

この記事ではわかりやすく丁寧に解説していくので、最後までついてきてください!

2023年2月17日に開催した以下の勉強会で使用したスライドをこちらにも載せておきます。

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を使用したときのガス代を比較してみましょう。

以下の公式のページにて掲載されている画像を使用して解説していきます。

https://www.azuki.com/erc721a

ERC721Enumerableを使用した場合は、トークンを発行するごとにガス代が大きくかかっています。

一方、ERC721Aの場合は半分以下のガス代で済んでいます。

ドルに換算したものも合わせて掲載されているので、そちらも確認しましょう。

https://www.azuki.com/erc721a

記事執筆時点での価格が記載されていますが、ガス代をだいぶ節約できているのが確認できます。

ERC721Aの仕組み

前章ではERC721Aの概要と特徴を確認しました。

この章ではERC721Aの仕組みをもう少し詳しく見ていきます。

この章で使用している画像は以下の記事をめちゃくちゃ参考にして作らせていただきました。

トークン発行

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が45のトークンの所有者が「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.
}

この関数を呼び出したコントラクトがERC165ERC721ERC721Metadataをサポートしているか確認する関数。

interfaceIdに渡されたコントラクトアドレスの先頭4ビットがERC165ERC721ERC721Metadataに一致しているか確認しています。

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を実装していきましょう!

今回は以下のドキュメントを参考にしています。

環境構築

まずは最初に環境の構築をしていきます。

以下の記事の「環境構築」の章を参考にして実行してください。

$ 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.solERC721AQueryable.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);
    });
      ;
  });
});

何をしているかを簡単にまとめておきます。

処理

  • wallet1wallet2でトークンをそれぞれ発行。
  • wallet1wallet2が所有しているトークンの総量を確認。
  • wallet1wallet2が所有しているトークンのトークン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では図解でわかりやすく解説する投稿をしているのでぜひフォローしてくれると嬉しいです!



参考

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