前回、第一回コントラクト輪読会レポートについてまとめた記事を書きました。

今回も、先日イーサリアムnaviでおこなった「第二回コントラクトコードの輪読会」という勉強会のレポートを、記事としてまとめながら「ERC721」について解説していきます。
この勉強会は、ERC721のコントラクトを読んで、SolidityならびにNFTについての理解を深めることを目的としたものでした。
勉強会当日は、zoomを使いながら無料分の40分間×2セットでおこないましたが、非常に満足度の高い勉強会となり、これからSolidityの勉強をしたい人にも役に立つ内容だったため、備忘録としてレポートにしておこうと考えました。
さて、本記事では
- ERC721とは?
- NFTの画像はどこに保管されている?
- 実際にコードを読んでみよう
- 実際にコードを読んでみよう(mint編)
- 実際にコードを読んでみよう(approve編)
- 実際にコードを読んでみよう(transfer編)
- まとめ
- 今週読んだ記事一覧
という構成で、第二回コントラクト輪読会レポートとしてまとめつつ、「ERC721」について解説していきたいと思います。
本記事が、皆さんの「ERC721」や「NFT」、「Solidity」への理解の一助となりましたら幸いです。
※本記事は一般的な情報提供を目的としたものであり、法的または投資上のアドバイスとして解釈されることを意図したものではなく、また解釈されるべきではありません。ゆえに、特定のFT/NFTの購入を推奨するものではございませんので、あくまで勉強の一環としてご活用ください。
イーサリアムnaviの活動をサポートしたい方は、「定期購読プラン」をご利用ください。
ERC721とは?

まずはざっくりと、ERC721とは何かについて説明します。
ERC721とは、Ethereumブロックチェーン上でNFT(Non-Fungible Token)を実装するための標準規格のことです。

なぜ規格を統一するの?
例えば、OpenSeaのようなマーケットプレイスが、個別のNFTごとにコードを書いて実装しなくても済んだり、さまざまなメリットがあるからです。
ERC721で必要なもの
ERC721は主に
- IERC721
- IERC721Metadata
- IERC721Enumerable
の3つのインターフェース(Interface)をもとに構成されています。
厳密には、ERC721に準拠するためにコントラクトで必要なのは「IERC721」だけですが、ERC721には上記3つとも実装されています。


ちなみに、3つのインターフェイス以外にも、こんなものがあります。
IERC721Receiver
- トークンがコントラクト内で永遠にロックされてしまうことを防ぐために使用できる
- ERC721トークンを受信する必要のあるコントラクトを書いている場合は、このインターフェイスを含めることが推奨されている
ERC721Pausable
全ユーザーのトークン転送を一時停止できるアドレスの指定
ERC721Burnable
自分のトークンを破棄する
NFTの画像はどこに保管されている?


ERC721では「tokenURI」という領域に、NFTのメタデータを設定できるようになっています。
メタデータの中には
- 名前
- 詳細データ
- デジタルデータの保存先(外部サーバー等)
といった、NFTに関する情報を指定します。
実際のデジタルデータ(画像データなど)は外部に保存して、それを指し示す情報をメタデータとしてブロックチェーン上に記録した情報をNFTとして取り扱うことが多いです。


しかし、フルオンチェーンNFTの場合は例外で、SVGなどの形式でテキストデータを直接書き込みます。
これに関して実際に、有名なNFTプロジェクト
- Pudgy Penguins
- Loot (for Adventurers)
を例に、それぞれ確認してみましょう。
Pudgy Penguins


Pudgy Penguinsのetherscanページから、tokenID=1でクエリしてみると、https://api.pudgypenguins.io/penguin/1と返されました。
これをクリックすると以下のJSON形式の情報が表示されますが、画像に関してはピンク線を引いたimageの部分が該当します。


このhttps://api.pudgypenguins.io/penguin/image/1をクリックすると、サーバー上で管理されたNFTの画像が表示されることが確認できるでしょう。


このことから、Pudgy Penguinsでは画像データを運営が管理するサーバー上に保管していることが分かります。
Loot (for Adventurers)
では、Lootの場合はどうでしょうか。
Loot (for Adventurers)のetherscanページから、tokenID=1でクエリしてみると、なにやら暗号のような文字列が返されました。


これが俗にいうSVGファイル形式というやつで、要はこの文字列そのものが画像ファイルなのです。
先程のPudgy Penguinsの場合は、Ethereumブロックチェーン上に直接画像データを載せるには容量が大きすぎて難しいため、画像データを保管するサーバーのリンクを画像データの代わりに参照していました。
それに対して、Lootのimage情報は文字だけでデータ容量が小さいので、直接Ethereumブロックチェーン上に保管することが可能になっています。


ちなみに、CryptoPunksやBlitmapのようなドット絵のプロジェクトもこのような形式を採用していますが、このようなNFTを一般的に「フルオンチェーンNFT」と呼びます。


実際にコードを読んでみよう
ではここからは、ERC721.solのコードを見ていきます。
今回は、OpenZeppelinのgithubページから読んでいきたいと思います。
library


ERC721では、以下2つのlibraryを使っています。
Address
accountがコントラクトであれば、trueを返すlibrary
Strings
uint256をASCIIのstringの10進数表現に変換するlibrary
mapping


ERC721では、以下4つのmappingが存在します。
_owners
token ID=○○のERC721トークンは、address××がもっていることを記録する
_balances
address××が、tokenを〇〇個保有していることを記録する
_tokenApprovals
token ID=○○のERC721トークンは、address××がapproveされたアドレスであることを記録する
_operatorApprovals
address××が、address△△をOperatorとしてapproveしているかの真偽値を記録する
ownerからoperatorへapproveされたかどうかのマッピング
よく見ると、最後の_operatorApprovalsだけmappingの中にmappingが入っていますが、これはダブルマッピングと呼ばれるものです。
constructor


name と symbol を設定することで、コントラクトを初期化します。
fn supportsInterface


IERC165なので今回の勉強会ではパスしましたが、インターフェイスをどうこうするものです。
fn balanceOf


address ownerを引数に、uint256をリターンする関数です。
mapping _balancesは、「address××が、tokenを〇〇個保有していることを記録する」ものでしたよね?
つまりこの関数は、『ownerがゼロアドレス(0x0000…)じゃなかったら、ownerが保有しているtokenの数をリターンするよ』というものです。
balanceOfはview関数でデータの読み取り専用なので、関数呼び出しにgas代は必要なく、イーサスキャンのReadからもwallet接続なしで見れますよ。
また、virtualとoverrideというキーワードがついていますが、これは以下のような意味をもちます。
virtualとoverrideはどちらもSolidity 0.6.0で追加されたキーワードで、 virtualは「この関数は後でオーバーライドしていいよ」 overrideは「この関数は前に定義した関数をオーバライドしてるよ」 という意味を表します。
Solidityのvirtualとoverrideキーワードの意味
この二つはセットで使います。
fn ownerOf


tokenIdを引数に、その保有者のaddressをリターンする関数です。
こちらの関数は、Etherscanを使ってNFTをコントラクトから直接mintする際にも、対象のtokenIDのものが他の誰かにmintされていないか確認する方法として使ったりします。


fn name, fn symbol


これは単に、stringのnameやsymbolを返すだけの関数です。
fn tokenURI, fn _baseURI


function tokenURI
93行目のrequire文の中に_exists関数があるので、見てみましょう。


ご覧の通り_existsは、『tokenID=tokenIdを保有するaddressが、ゼロアドレスではないかどうかの真偽値(true または false)を返す関数』です。
つまり、「そのERC721トークン自体が存在するかどうか」という直訳の意味より、『そのERC721トークンを保有するaddress ownerが存在するか(誰かがmint済みか)』を調べていると言った方が分かりやすいですね。


それを踏まえてtokenURI関数は、『そのERC721トークンのtokenID=tokenIdのaddress ownerが存在する(=ゼロアドレスじゃない)こと』が前提条件となっています。


その条件を通過したらbaseURI(デフォルトでは"")を受け取ってbyte型にし、条件分岐で以下の処理をおこないます。
abi.encodePackedに関しては以下の関連記事を見ていただきたいのですが、要はSolidityでは文字の扱いが厄介な仕様のため、文字列はこのようにしてを連結させる必要があるのです。


ちなみに、_baseURI関数とtokenURI関数のどちらにもvirtualがついているため、先ほども述べたように好きに改変して使うことができます。


例えば、_baseURIはデフォルトでは""ですが、この部分が最初の例で出したPudgy Penguinsの場合だと上写真のようにhttps://api.pudgypenguins.io/penguin/になっていますね。
実際にコードを読んでみよう(mint編)
fn _mint


まず、require文にある前提条件は、
- 転送先の
address toがゼロアドレスじゃないこと - tokenID=
tokenIdを誰かがmint済みでないこと
となっています。
この2つの条件をクリアすると次に進めるわけですが、まず_beforeTokenTransferが出てきたので、先にこの関数について触れておきます。
fn _beforeTokenTransfer


_beforeTokenTransferを見てみると、関数内{}に何も書かれていません。
これは、OpenZeppelinが「こういう関数も用意しているから、必要だったらフックとして使ってね」ということで用意している関数です。
直訳すると「トークンを転送する前の処理」ですが、例えば「転送税」などが使用例として考えられます。
ですが、デフォルトでは空になっています。


つまりここでは空の関数ということで、一旦気にしなくて大丈夫そうですね。
_mintの本質は、この後出てくる2つのmapping(特に後者)にあります。
_balances[to] += 1;
まず_balancesですが、これは『address××が、tokenを〇〇個保有していることを記録する』ものでしたね。
つまり、address toが保有するtoken保有数を+1していて、mintすると対象のERC721トークン保有数が1個増える処理が書かれています。
_owners[tokenId] = to;
そして次の_ownersは、『token ID=○○のERC721トークンは、address××がもっていることを記録する』ものでしたね。


身も蓋もないことを言うと、最近よくあるコレクタブルNFT早押し合戦というものは、289行目のロジックを動かして自分のaddress toを_owners[tokenId]に保存させる合戦ということになります。
つまりmintの本質は、『この番号は俺のもの』をブロックチェーンに刻む作業のことです。



こう聞くと、なんだか一気に夢がなくなりますね。笑
fn _safeMint


ただ、現時点では先程の_mintを使うことは推奨されておらず、代わりに_safeMintを使ってねと明記されています。
_safeMintを式で表すと、_mintと何が違うのか?が理解できます。
_safeMint = _mint + _checkOnERC721Received
つまり、別に_mint自体にバグがある訳ではないので_safemint内でも_mint関数は使われていて、_mintの後に_checkOnERC721Receivedのrequire文の処理を追加したものが_safeMintです。
ということで、この追加された_checkOnERC721Receivedを見れば、_safeMintから何が変わったのか?が見えてきそうですね。
_checkOnERC721Received


色々コードが書かれているので、簡潔にまとめます。
_checkOnERC721Receivedは、ERC721トークンの転送先address toがコントラクトアドレスではない(ユーザーアドレスEOAの)とき、trueを返します。
要は、誤ってコントラクトアドレスに送ってしまうとGOXしてしまうから、それをチェックするための処理を加えた親切設計として、新たにfunction safeMintを使うように推奨されているのです。


話を戻しますと、つまり_safeMintでは、ERC721トークンの転送ミスによるGOXを防いでくれる仕様になっているといえます。
require文でエラーになると、_mintの処理も巻き戻しが起こって、その処理はなかったことになりますからね。
ただし_mintを実行した際にかかったgas代は戻ってきません)
実際にコードを読んでみよう(approve編)
fn approve


approve関数の処理を実行するためには、条件が2つあります。
- 転送先が対象ERC721トークンの保有者ではないこと
- 対象ERC721トークンの保有者がこの関数を呼んでいる or
isApprovedForAll(owner, _msgSender()) == trueである
ここでisApprovedForAllが出てきたので、この流れで見ていきたいと思います。
fn isApprovedForAll


_operatorApprovalsは、『address××が、address△△をOperatorとしてapproveされているかを記録する』ものでしたね。
つまり、「address owner はaddress operator をOperatorとすることをapproveしていますか?」に対してtrue or falseを返す関数です。


では話をfn approveに戻します。
- 転送先が対象ERC721トークンの保有者ではないこと
- 対象ERC721トークンの保有者がこの関数を呼んでいる or
isApprovedForAll(owner, _msgSender()) == trueである
このrequire文を両方満たすと_approve関数が呼ばれます。


_tokenApprovalsは、『token ID=○○のERC721トークンは、address××がapprove済みアドレスであることを記録する』ものでしたね。
つまり、uint256 tokenIdの特定のERC721トークンに対しては、address toがapproveされたアドレスですよ〜と、記録しています。
fn getApproved


_exists(tokenId) == trueである(誰かがtokenID=tokenIdのERC721トークンをmint済みである)ことが前提条件となっています。
それを通過すると、_tokenApprovals(tokenID=tokenIdの特定のERC721トークンに対して、address××がapproveされたアドレスかどうかの真偽値)をリターンするという関数です。
fn setApprovalForAll


よくTwitterなどで、



なんでもかんでも`setApprovalForAll`関数をcallするのは危ない!
と言われていますが、見たことがありませんか?
その理由は、特定のアドレス(address operator)に対して、あなたの保有するすべて(cryptoPunks100個保有している場合100個すべて)のERC721トークンの転送権を与えるからです。
先程のfunction approveと見比べてみると、違いが分かりやすいでしょう。




approve関数の場合、個別のtokenIdごとにapproveしていますが、setApprovalForAll関数の場合は_operatorApprovalsによって、すべてのERC721トークンを一括approveしています。
例えば、address operatorがコントラクトアドレスではなく悪意ある人のアドレス(EOA)になっていた場合、その人はあなたのERC721トークンをを全て転送できる権利をもつことになります。
なので、OpenSeaのような”信用できる”NFTマーケットプレイスのコントラクトアドレスがaddress operatorに入っているときだけ、callするようにしましょう。
もしくは、気になるプロジェクトがあってsetApprovalForAllを求められた場合、コントラクトからコードを確認してみると良いでしょう。
setApprovalForAllって、何かメリットあるの?
ここまでの話だけ聞くと



「setApprovalForAll」絶対いらないじゃん
と思われたかもしれませんね。
しかし、setApprovalForAll関数にももちろん使命がありますよ。
OpenSeaを例にすると、あなたがあるシリーズのERC721トークンを転送するとき、その都度gas代のかかるapproveを求められると面倒ですよね?
それを解消するために、setApprovalForAllには以下のようなメリットがあります。
approve関数にかかるgas代を削減できる- 面倒な手間が省けて利便性が高まる
ただし、利便性とセキュリティはトレードオフなので、無闇矢鱈にsetApprovalForAllをやっておけば良いのではないかという「思考停止」は、やめておいた方が賢明ですね。
実際にコードを読んでみよう(transfer編)
fn transferFrom


tokenIDがuint256 tokenIdのERC721トークンを、 address from から address to に転送する関数です。
ただ、_mintと同じく現時点ではtransferFromを使うことは推奨されておらず、代わりにsafeTransferFromを使ってね!されています。
要はmint・safemintのケースと同じで、GOXチェックがある親切設計の方を使ってねということです。
transferFromの処理の中身を理解するために、_transferも確認する必要がありそうなので見ていきましょう。
fn _transfer


いろいろ書かれていますが、ERC721トークンをfromからtoに転送して、それぞれの保有数_balancesを-1, +1しています。
また、340行目_approve(address(0), tokenId);の部分で、mapping_tokenApprovalsを初期化(ゼロアドレスに設定)しています。


fn safeTransferFrom


こちらのtransfer関数はpublicなので、ユーザーがtransferを呼び出す際はこの関数を使うことになりそうですね。
この関数を理解するために、まずは184行目に書かれている_safeTransfer関数について見ていきます。
fn _safeTransfer


これもmintの時と同じで、式で表すと以下になります。
_safeTransfer = _transfer + _checkOnERC721Received
つまり、_transfer自体にバグがある訳ではないので_safeTransferでも_transferは使われていて、_transferの後に_checkOnERC721Receivedのrequire文の処理を追加したものが_safeTransferです。
ちなみにこちらもinternal関数なので、Etherscanなど外部から呼び出すことはできません。


それを踏まえてsafeTransferFromを見てみます。
safeTransferFrom = require(_isApprovedOrOwner) + _safeTransfer
という式で表せるので、要は外部の誰かが呼び出しても問題ないように、approvedされているアカウント、もしくは、Ownerアカウントに対して、_safeTransfer呼び出す権利を与えている関数ですね。
こちらはpublic関数なので、etherscanなど外部から呼び出すことが可能です。
fn _burn


最後にtransfer系の関数ではないですが、_burnについて。
_burn関数の中身の処理は、
_beforeTokenTransfer関数を呼ぶ- mapping
_tokenApprovalsを初期化(ゼロアドレスに設定) - ownerのERC721トークン保有数
_balancesを1減らす delete(変数にデフォルト値を割り当てる)- eventを吐き出す
です。
つまり、_burnは名前の通りERC721トークンを削除するというわけではなく、単にゼロアドレスに転送するだけということが分かりますね。


ということで、_burnによってburnしたERC721トークンはOpenSeaなどでも見ることができて、「0x0000000000000000000000000000000000000000」のNullAddressというアカウントに溜められていきます。
まとめ
本記事では、先日イーサリアムnaviでおこなった『第二回コントラクトコードの輪読会「ERC721」』という勉強会のレポートを、記事としてまとめました。
昨今はコロナ禍ということもあり、なかなかオフラインでのイベントも開催しづらいので、こういった形式の勉強会は今後も企画していきたいと考えています!


イーサリアムnaviを運営するSTILL合同会社では、web3/crypto関連の記事執筆業務やリサーチ代行、その他(ご依頼・ご提案・ご相談など)に関するお問い合わせを受け付けております。
まずはお気軽に、こちらからご連絡ください。
- 法人プランLP:https://ethereumnavi.com/lp/corporate/
- Twitter:@STILL_Corp
- メールアドレス:info@still-llc.com


