前回、第一回コントラクト輪読会レポートについてまとめた記事を書きました。
今回も、先日イーサリアム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