第二回コントラクト輪読会レポート〜ERC721のコントラクトを皆で読みながら理解を深めよう〜

contract-study-2-solidity-erc721

どうも、イーサリアムnavi運営のでりおてんちょーです。

イーサリアムnaviでは、毎日大量に流れてくるクリプトニュースを調査し、その中でも面白いトピックやクリプトネイティブな題材を選び出し、それを分かりやすく読みやすい形でお伝えしています。パラパラと内容を眺めているだけでも、事業やリサーチの新たなヒントに繋がることがあります。世界の最先端では、どのようなクリプトコアな試みが行われているのかを認識するだけでも、 思わず狭くなりがちな視野を広げてくれるでしょう。

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

今回も、先日イーサリアムnaviでおこなった「第二回コントラクトコードの輪読会」という勉強会のレポートを、記事としてまとめながら「ERC721」について解説していきます。

この勉強会は、ERC721のコントラクトを読んで、SolidityならびにNFTについての理解を深めることを目的としたものでした。

勉強会当日は、zoomを使いながら無料分の40分間×2セットでおこないましたが、非常に満足度の高い勉強会となり、これからSolidityの勉強をしたい人にも役に立つ内容だったため、備忘録としてレポートにしておこうと考えました。

さて、本記事では

  1. ERC721とは?
  2. NFTの画像はどこに保管されている?
  3. 実際にコードを読んでみよう
  4. 実際にコードを読んでみよう(mint編)
  5. 実際にコードを読んでみよう(approve編)
  6. 実際にコードを読んでみよう(transfer編)
  7. まとめ
  8. 今週読んだ記事一覧

という構成で、第二回コントラクト輪読会レポートとしてまとめつつ、「ERC721」について解説していきたいと思います。

本記事が、皆さんの「ERC721」や「NFT」、「Solidity」への理解の一助となりましたら幸いです。

※本記事は一般的な情報提供を目的としたものであり、法的または投資上のアドバイスとして解釈されることを意図したものではなく、また解釈されるべきではありません。ゆえに、特定のFT/NFTの購入を推奨するものではございませんので、あくまで勉強の一環としてご活用ください。

「定期購読プラン」の詳細はこちら

「定期購読プラン」は現在トライアル期間中のため、初月は30日間無料でプランに登録することができます。この機会にぜひお試しいただけると幸いです。

※ 1ヶ月間の無料トライアル期間終了後に、自動的に有料プランへと移行します。
※ 無料トライアル期間中にご解約いただければ、ご利用料金は発生いたしません。

法人プランでは、「ライトプラン」「ベーシックプラン」「プレミアムプラン」の3種類を提供しております。

  • 社内勉強用
  • 企業ニーズに合わせた情報収集
  • スポットコンサルとしての活用

など、各事業者様ごとに特化した「定期購読プラン」+α のパッケージプランとして、幅広い用途でご活用頂いています。


目次

ERC721とは?

まずはざっくりと、ERC721とは何かについて説明します。

ERC721とは、Ethereumブロックチェーン上でNFT(Non-Fungible Token)を実装するための標準規格のことです。

なぜ規格を統一するの?

例えば、OpenSeaのようなマーケットプレイスが、個別のNFTごとにコードを書いて実装しなくても済んだり、さまざまなメリットがあるからです。

ERC721で必要なもの

ERC721は主に

  1. IERC721
  2. IERC721Metadata
  3. IERC721Enumerable

の3つのインターフェース(Interface)をもとに構成されています。

厳密には、ERC721に準拠するためにコントラクトで必要なのは「IERC721」だけですが、ERC721には上記3つとも実装されています。

出典:OpenZeppelin

ちなみに、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ブロックチェーン上に保管することが可能になっています。

Loot (for Adventurers) Bag #1

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

全ての情報をEthereumチェーン上に載せることの利点は、NFTプロジェクトの運営がいなくなったとしても、Ethereumブロックチェーン自体が停止しない限り画像データなどが残り続けることです。

実際にコードを読んでみよう

ではここからは、ERC721.solのコードを見ていきます。

今回は、OpenZeppelinのgithubページから読んでいきたいと思います。

library

ERC721では、以下2つのlibraryを使っています。

Address

accountがコントラクトであれば、trueを返すlibrary

Strings

uint256をASCIIのstringの10進数表現に変換するlibrary

usingについて詳しく知りたい方は、Solidityのドキュメントを参考にしてみてください。

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が入っていますが、これはダブルマッピングと呼ばれるものです。

mappingについては以下の関連動画でわかりやすく解説されていたので、よければご活用ください。
配列とマッピングの違いと用途【第3回】Solidity 基礎編 (スマートコントラクト プログラミング入門)

constructor

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

fn supportsInterface

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

気になる方は、「IERC165.sol」からご覧ください。

fn balanceOf

address ownerを引数に、uint256をリターンする関数です。

mapping _balancesは、「address××が、tokenを〇〇個保有していることを記録する」ものでしたよね?

つまりこの関数は、『ownerがゼロアドレス(0x0000…)じゃなかったら、ownerが保有しているtokenの数をリターンするよ』というものです。

balanceOfはview関数でデータの読み取り専用なので、関数呼び出しにgas代は必要なく、イーサスキャンのReadからもwallet接続なしで見れますよ。

また、virtualoverrideというキーワードがついていますが、これは以下のような意味をもちます。

virtualとoverrideはどちらもSolidity 0.6.0で追加されたキーワードで、 virtualは「この関数は後でオーバーライドしていいよ」 overrideは「この関数は前に定義した関数をオーバライドしてるよ」 という意味を表します。
この二つはセットで使います。

Solidityのvirtualとoverrideキーワードの意味

virtualの目印がついていることで、この関数の中身は好きに上書きしても良いよ!ということを、OpenZeppelinが示してくれています。

fn ownerOf

tokenIdを引数に、その保有者のaddressをリターンする関数です。

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

fn name, fn symbol

これは単に、stringのnamesymbolを返すだけの関数です。

fn tokenURI, fn _baseURI

function tokenURI

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

ご覧の通り_existsは、『tokenID=tokenIdを保有するaddressが、ゼロアドレスではないかどうかの真偽値(true または false)を返す関数』です。

つまり、「そのERC721トークン自体が存在するかどうか」という直訳の意味より、『そのERC721トークンを保有するaddress ownerが存在するか(誰かがmint済みか)』を調べていると言った方が分かりやすいですね。

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

その条件を通過したらbaseURI(デフォルトでは"")を受け取ってbyte型にし、条件分岐で以下の処理をおこないます。

その長さが0より大きい
その長さが0以下
  • abi.encodePackedで文字列を連結(baseURI + tokenId)してリターン
  • ""をリターン

abi.encodePackedに関しては以下の関連記事を見ていただきたいのですが、要はSolidityでは文字の扱いが厄介な仕様のため、文字列はこのようにしてを連結させる必要があるのです。

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

例えば、_baseURIはデフォルトでは""ですが、この部分が最初の例で出したPudgy Penguinsの場合だと上写真のようにhttps://api.pudgypenguins.io/penguin/になっていますね。

実際にコードを読んでみよう(mint編)

fn _mint

「mintとは何か」の本質は、コードを読むと見えてきます。

まず、require文にある前提条件は、

  • 転送先のaddress toがゼロアドレスじゃないこと
  • tokenID=tokenIdを誰かがmint済みでないこと

となっています。

この2つの条件をクリアすると次に進めるわけですが、まず_beforeTokenTransferが出てきたので、先にこの関数について触れておきます。

fn _beforeTokenTransfer

_beforeTokenTransferを見てみると、関数内{}に何も書かれていません。

これは、OpenZeppelinが「こういう関数も用意しているから、必要だったらフックとして使ってね」ということで用意している関数です。

直訳すると「トークンを転送する前の処理」ですが、例えば「転送税」などが使用例として考えられます。

「このERC721トークンを誰かに送る際は、つくった人にETHを○○%送ります」みたいなロジックを書けそう

ですが、デフォルトでは空になっています。

つまりここでは空の関数ということで、一旦気にしなくて大丈夫そうですね。

_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の本質は、『この番号は俺のもの』をブロックチェーンに刻む作業のことです。

derio

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

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が呼ばれた後で呼ぶ必要があるからです。

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を返す関数です。

ちなみにこの関数は、後で出てくるsetApprovalForAllでも使われています。

では話を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されたアドレスかどうかの真偽値)をリターンするという関数です。

こちらの関数はpublicでviewがついているので、EtherscanのReadからも呼び出すことが可能ですね。

fn setApprovalForAll

よくTwitterなどで、

なんでもかんでも`setApprovalForAll`関数をcallするのは危ない!

と言われていますが、見たことがありませんか?

その理由は、特定のアドレス(address operator)に対して、あなたの保有するすべて(cryptoPunks100個保有している場合100個すべて)のERC721トークンの転送権を与えるからです。

先程のfunction approveと見比べてみると、違いが分かりやすいでしょう。

approve関数の場合、個別のtokenIdごとにapproveしていますが、setApprovalForAll関数の場合は_operatorApprovalsによって、すべてのERC721トークンを一括approveしています。

これこそが、無闇矢鱈にsetApprovalForAll関数をcallしたらマズい理由です。

例えば、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を初期化(ゼロアドレスに設定)しています。

ちなみにinternal関数なので、etherscanなど外部から呼び出すことはできません。

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」』という勉強会のレポートを、記事としてまとめました。

前回同様、「もっとコードを読めるようになりたい!」という気持ちが強くなり、Solidityの勉強に対するモチベーションが非常に高くなりました。

昨今はコロナ禍ということもあり、なかなかオフラインでのイベントも開催しづらいので、こういった形式の勉強会は今後も企画していきたいと考えています!

イーサリアムnaviを運営するSTILL合同会社では、web3/crypto関連のリサーチ代行、アドバイザー業務、その他(ご依頼・ご提案・ご相談など)に関するお問い合わせを受け付けております。

まずはお気軽に、こちらからご連絡ください。

みんなにも読んでほしいですか?
  • URLをコピーしました!
目次