この記事は『ドワンゴ Advent Calendar 2022』 9日目の記事です。
はじめに
この記事は?
何かと話題の NFT について、結局のところどういうものなのかを調べた記事です。
現実に今動いている NFT の仕様と実装を見ることで、 実際、いま何ができるのか を理解します。
特にこの記事では以下の2つにフォーカスし、実装の一部をかいつまんで紹介します。
- NFT は どういう情報が保存されている のか?
- NFT には 何の機能がある のか?
なお、この記事では Ethreum または Ethereum 互換ブロックチェーン上で実現されている ERC-721 形式の NFT について説明します。
この記事で説明しないこと
この記事では背景技術などの詳しい説明はしません。1
以下くらいのざっくり認識があれば読める記事を目指します。
- Ethereum とは
- Ethereum
- ブロックチェーンの種類
- EVMとスマートコントラクト
- Ethereum 上でアプリケーションなどが動く仕組み
- NFT をつくる=スマートコントラクトを実装してデプロイするという意味
- Solidity
- Solidity — Solidity 0.5.4 ドキュメント
- スマートコントラクトを書くときのプログラミング言語
- NFT のメタデータ
- NFT のタイトルや画像みたいな部分ね!
- トークン1つ1つに JSON や JSON の URL が紐付けられてるだけです
「現実のNFT」って何?
一般的に流通している NFT は、ほぼほぼ @openzeppelin/contracts
というライブラリを使って実装されているものかと思います。
@openzeppelin/contracts
には様々な規格を満たしたコントラクトのベースとなる実装が含まれており、NFT や 独自コインを作りたいなぁと思ったら、@openzeppelin/contracts
を import するだけでほぼコードを書かずに実装できます。
この @openzeppelin/contracts
の中に含まれている NFT を作る際に利用するのが @openzeppelin/contracts/token/ERC721/ERC721.sol
です。
この中にはNFTの規格を満たすために必要なコードのすべてが入っているため、このコードを読めば現実に動く NFT の仕組みを大体理解することができます。
つまり、この記事は実質的に @openzeppelin/contracts
の ERC721.sol
をコードリーディングする記事です。
本題:NFT の実装の中身を見る
それでは早速、中身を見ていきます。
@openzeppelin/contracts
のコードは GitHub 上で公開されているため、以下のページからもコードの中身を確認することができます。
約500行ほどのSolidityのコードで実装されています。
このコードに加えて、自分で「NFT を発行する処理(=誰がどうやって NFT を発行できるか)」などを実装するだけでオリジナルの NFT を作成できます。
NFTに保存されている情報を見る
Solidity で書かれたコードでは、一見メンバー変数のように見える値は永続的な状態として保存されています。状態変数(State Variables)と呼びます。
つまり、ここで宣言されている状態変数を見れば、NFTに何が保存されているのかがわかります。
以下に ERC721.sol
から状態変数の宣言部分を抜粋します。
contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
// Token name
string private _name;
// Token symbol
string private _symbol;
// Mapping from token ID to owner address
mapping(uint256 => address) private _owners;
// Mapping owner address to token count
mapping(address => uint256) private _balances;
// Mapping from token ID to approved address
mapping(uint256 => address) private _tokenApprovals;
// Mapping from owner to operator approvals
mapping(address => mapping(address => bool)) private _operatorApprovals;
}
name / symbol : コントラクト名とトークンのシンボル
このコントラクトの名前と、このコントラクトで発行される NFT を示す単位(Symbol)が保存されています。
感覚的には「名前=BitCoin」「単位=BTC」と同じようなイメージです。
owners : トークンの所有者
// Mapping from token ID to owner address
mapping(uint256 => address) private _owners;
mapping
は連想配列のようなデータです。
ここでは数値で示される トークンID
をキーに、そのトークンの持ち主のアドレスを保持しています。
NFTでよく言われる「所有」という概念はこの部分のことです。
1番のNFTはAさん、2番のNFTはBさん、3番の…
のような連想配列を記録することで、誰がどのNFTを所有しているのかを表現しています。
余談:NFT はどこにある?
よく「NFT を手に入れると自分のウォレットに NFT が入ります」という説明をする人がいますが、実装を見ればわかる通りそんなことは起こっていません。
(そういう 表示UI を持つアプリケーションはあるため、混同されてそう)
もし無理やり例え話をするのであれば「美術館(コントラクト)にある絵画(トークン)の下に持ち主のアドレスが書いてある」みたいな表現のほうがまだ近そうです。間違っても美術館から絵画を持ち出したりはしていないため注意です。
balances : トークンの所有数
// Mapping owner address to token count
mapping(address => uint256) private _balances;
先程の _owners
に似ている mapping
です。
こちらは持ち主のアドレスをキーに、その人がいくつの NFT を持っているかを保持しています。
tokenApprovals / operatorApprovals : トークンや操作の認可
// Mapping from token ID to approved address
mapping(uint256 => address) private _tokenApprovals;
// Mapping from owner to operator approvals
mapping(address => mapping(address => bool)) private _operatorApprovals;
ちょっと見慣れないモノが出てきました。
これについては、この部分だけを見てもわかりにくいため、後述の機能とあわせて読み解いていきます。
NFTの機能を見る
ここまではNFTに保存されている情報を見ていましたが、ここからはNFTの機能を見ていきます。
その前に:ERC-721という規格
NFTが持つ機能(メソッドやイベント)は規格が定められています。
この記事で説明しているNFTは以下のERC-721という規格に従ったものです。
一言で言えばインタフェースが定義されており、そのインタフェースを満たす実装を書けばERC-721規格のNFTになれるということです。
この規格では以下の9つのメソッドが定義されており、@openzeppelin/contracts
でもこの9つのメソッドは全て実装されています。
function balanceOf(address _owner) external view returns (uint256);
function ownerOf(uint256 _tokenId) external view returns (address);
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
function approve(address _approved, uint256 _tokenId) external payable;
function setApprovalForAll(address _operator, bool _approved) external;
function getApproved(uint256 _tokenId) external view returns (address);
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
このセクションでは、この9つのメソッドについて詳しく見ていきます。
balanceOf : 指定した人のトークン所有数
function balanceOf(address owner) public view virtual override returns (uint256) {
require(owner != address(0), "ERC721: address zero is not a valid owner");
return _balances[owner];
}
誰かのアドレスを引数に受け取り、そのアドレスの人のトークン所有数を返すメソッドですね。
_balances
は先ほど見た通り、所有者のアドレスをキーに所有数を保持している mapping
であるため、このような実装で返すことができます。
require(~)
の部分はバリデーションのようなものです。
第一引数が false
になると、第二引数の文字列をエラーとして出力します。
ここでは指定したアドレスが 0
、つまり無のアドレスを指定したらエラーになるよう設定されています。
ownerOf : 指定したトークンの所有者
function ownerOf(uint256 tokenId) public view virtual override returns (address) {
address owner = _ownerOf(tokenId);
require(owner != address(0), "ERC721: invalid token ID");
return owner;
}
function _ownerOf(uint256 tokenId) internal view virtual returns (address) {
return _owners[tokenId];
}
指定されたトークンIDの持ち主のアドレスを返すメソッドです。
「1番のNFTを持ってる人おしえてください!」的なやつですね。
こちらも先ほど見た _owners
がトークンIDをキーに所有者のアドレスを保持しているため、シンプルに返すことができています。
approve系 : NFTの操作の認可
ここから分量が多く、記事に該当コードをすべて引用するには長すぎるため、説明に必要な最低限に絞って引用します。
approve系のメソッドとして以下の4つが定義されています。
function approve(address _approved, uint256 _tokenId) external payable;
function getApproved(uint256 _tokenId) external view returns (address);
function setApprovalForAll(address _operator, bool _approved) external;
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
approve はNFTの操作を認可するための仕組みです。
例えば、NFTの売り買いをしたい場合に、売る人と買う人がリアルタイムにお互い操作をしないと売買できなかったりすると不便です。
approve を使うことで、販売プラットフォームなどに自分のNFTの操作……つまり人への送信等をする権限を与えることができます。
setApprovalForAll
/ isApprovedForAll
は自分が持つすべてのNFTの認可を与える仕組みです。
信頼する相手のアドレスを指定することで、そのアドレスに対して自分のNFTをすべて操作する権限を付与します。
前セクションで読んだ状態変数の定義部分を含めて、実装を見てみます。
// Mapping from owner to operator approvals
mapping(address => mapping(address => bool)) private _operatorApprovals;
function setApprovalForAll(address operator, bool approved) public virtual override {
_setApprovalForAll(_msgSender(), operator, approved);
}
function _setApprovalForAll(
address owner,
address operator,
bool approved
) internal virtual {
require(owner != operator, "ERC721: approve to caller");
_operatorApprovals[owner][operator] = approved;
emit ApprovalForAll(owner, operator, approved);
}
function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
return _operatorApprovals[owner][operator];
}
_operatorApprovals
という状態変数の中に、各所有者ごとに認可を与えている先が記録されています。
「Aさんはショップ1に、Bさんはショップ2とショップ3に、Cさんは……」のようなイメージですね。
isApprovedForAll
を呼び出すことで、現在の認可情報が取得できます。
一方で approve
/ getApproved
はトークンごとに認可を与えるための仕組みです。
例えば、1番のNFTの権限をショップAに、2番のNFTの権限をショップBに……のように個別に許可を与える仕組みです。
// 注:説明に不要な部分は一部省略しています
mapping(uint256 => address) private _tokenApprovals;
function approve(address to, uint256 tokenId) public virtual override {
address owner = ERC721.ownerOf(tokenId);
require(to != owner, "ERC721: approval to current owner");
require(
_msgSender() == owner || isApprovedForAll(owner, _msgSender()),
"ERC721: approve caller is not token owner or approved for all"
);
_approve(to, tokenId);
}
function _approve(address to, uint256 tokenId) internal virtual {
_tokenApprovals[tokenId] = to;
emit Approval(ERC721.ownerOf(tokenId), to, tokenId);
}
function getApproved(uint256 tokenId) public view virtual override returns (address) {
return _tokenApprovals[tokenId];
}
こちらはトークンごとに認可を与えているアドレスを _tokenApprovals
に保持しています。
余談:NFTが盗まれた!?
時々 NFT が盗まれた~という事件が起きることがあります。
よくあるのが「 Web ページを開いたときによくわからない認可を求められたから OK 押しちゃった」というやつで、要は setApprovalForAll
で怪しい人を認可してしまったが故に、 NFT を自由に移動できるようにしてしまい発生する問題です。
Twitter などの OAuth の認可でも、怪しいアプリケーションを認可してアカウントを乗っ取られるケースなどがありますが、 NFT に関してはより慎重になる必要があり、一般に利用が広がる上ではなかなか難しい部分になるな……と個人的に感じています。人間は間違える生き物ですからね。
transferFrom / safeTransferFrom : NFTの移動
transferFrom 系のメソッドはNFTの移動処理を行うメソッドです。
「1番のNFTをAさんからBさんに送る」のような処理です。
transferFrom
と safeTransferFrom
がありますが、これは送る先のアドレスが不正なアドレスでないかのチェック機構の有無でメソッドが分かれています。2
そのため、最終的に行われる処理は同じです。
transfer の処理は基本的にトークンの所有者、または前述の approve で認可を与えられた人だけが呼び出すことができます。
以下に、最終的に呼び出される共通処理の _transfer
メソッドを引用します。
// 注:説明に不要な部分は一部省略しています
function _transfer(
address from,
address to,
uint256 tokenId
) internal virtual {
require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
require(to != address(0), "ERC721: transfer to the zero address");
// Clear approvals from the previous owner
delete _tokenApprovals[tokenId];
_balances[from] -= 1;
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
}
まず、そのトークン自体の Approval 情報が削除されます。
前の持ち主が許可していたからといって、次の持ち主が許可するかは別の問題ですしね。
その後、送り元・送り先の所有トークン数を更新、トークンの持ち主を送り先のアドレスに更新という形で処理が進みます。非常に明解です。
ここまでが ERC721.sol
に実装されている NFT の仕組みとして必須な仕組みです。
- NFT所有者の判定
- NFTの移動
- NFTの操作権の移譲
あたりが『NFTの機能』と言えそうです。
他にも ERC-721 の規格で定義されているわけではないが実際必要になる機能……例えばNFTを発行する仕組みなども ERC721.sol
の中には実装されています。
まとめ
この記事では現実のNFTとは何なのかを理解するため、数多くのNFTの実装で使われているライブラリである @openzeppelin/contracts
の ERC721.sol
を読むことで、具体的にどういった仕組みなのかを読み解きました。
最後に補足ですが、あくまで @openzeppelin/contracts
の実装は実装方法の一例であることにご注意ください。
ERC-721 の規格で定義されているのはあくまでインタフェースです。そのため、各 NFT の実装では他の規格で定義されている処理や、オリジナルの何らかの処理が含まれていることは多々あります。3
NFT はアプリケーションです。「NFT だから●●」と一括りに考えるのではなく、そのNFTはどういった実装がされているのか、という点を意識すると技術的にいろいろ楽しい世界が見えてくるのではないかなと感じました。