メインコンテンツまでスキップ

「NFT」タグの記事が2件件あります

全てのタグを見る

· 約8分
Thurendous
Polymetis

BYAC

TL;DR

  • ERC721 は一種の NFT に対応しているのに対し、ERC1155 は複数の NFT に対応しているイメージです。
  • ERC1155 の ERC721 との違い
    • token id は種類を規定し、id ごとに amount という数量を定義した
    • バッチ処理を実装した
  • 今回は ERC1155 のコード解説をした

EIP1155

ERC20 にしろ ERC721 にしろ、コントラクトごとに一種のコインにしか対応していないです。例えば、我々がポケモンのゲームを作ろうとすると、ERC21 あるいは ERC721 を使うと装備ごとにコントラクトをデプロイしないと行けないことになります。一千種類のアイテムがあるとすると、一千個のコントラクトを作らなければなりません。これはとんでもないことになってしまいます。課題を解決すく、イーサリアムの EIP1155 では、一個のコントラクトに複数の FT あるいは NFT を含めることにしました。特に GameFi のケースでは非常に有用です。

シンプルに言うと、ERC1155 は前に紹介した NFT の token スタンダード ERC721 と似ている:ERC721 では、token ごとにtokenIdを持っており、この id がユニークです。tokenIdは一個の token を代表している。それに対して、ERC1155 の場合は、token ごとに id がユニークだが、id ごとに数量が定義されている。これで、複数の種類の token は同じコントラクト内で管理することができる様になりました。

種類ごとに URI が存在していて、matadata を保存しています。ERC721 の URI と類似するが、以下のように、ERC1155 のメタデータインターフェイスのコントラクト:

/**
* @dev ERC1155のオプションインターフェイス、URI()でmetadataを返す
*/
interface IERC1155MetadataURI is IERC1155 {
/**
* @dev idのURIを返す
*/
function uri(uint256 id) external view returns (string memory);

では、どのようにトークンは FT か NFT を区別するのか?答え簡単だ、id の数量が 1 の場合ですと、これは NFT であり、ERC721 と似ている。もし id に対応する token の数量が 1 より大きい場合、それは FT となる。同じ id を共有しているので、ERC20 に類似します。

IERC1155 インターフェイス

ERC1155 インターフェイスは EIP1155 の実現すべき機能を定義しています。その中で、4 つのイベントと 6 個の関数を定義しています。ERC721 との違いとしては、ERC1155 は複数の種類の token を含まれます。また、バッチトランスファ、バッチバランスチェックの機能が追加されました。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "https://github.com/AmazingAng/WTF-Solidity/blob/main/40_ERC1155/IERC1155.sol";

/**
* @dev ERC1155のインターフェイス、EIP1155で求められた機能を定義
* 詳細:https://eips.ethereum.org/EIPS/eip-1155[EIP].
*/
interface IERC1155 is IERC165 {
/**
* @dev 単一な種類のtokenのトランスファイベント
* `value`個の`id`種類のtokenが`operator`によって`from`から`to`へトランスファ
*/
event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value);

/**
* @dev バッチトランスファイベント
* ids, valuesはトランスファするtokenの種類、数量の配列
*/
event TransferBatch(
address indexed operator,
address indexed from,
address indexed to,
uint256[] ids,
uint256[] values
);

/**
* @dev バッチアプルーブ
* `account`がすべての権限を`operator`に移譲するときに放出
*/
event ApprovalForAll(address indexed account, address indexed operator, bool approved);

/**
* @dev `id`のtokenのURIが変更となったとき、放出。`value`は新たなURI
*/
event URI(string value, uint256 indexed id);

/**
* @dev バランスを返す,`account`が持っている`id`のtokenの残高を返す
*/
function balanceOf(address account, uint256 id) external view returns (uint256);

/**
* @dev バッチで複数バランスを返す。`accounts`の配列と`ids`配列のlengthがイコールでないといけない
*/
function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids)
external
view
returns (uint256[] memory);

/**
* @dev バッチで複数アプルーブ,callerのtokenの権限を`operator`に渡す
* {ApprovalForAll}イベントを放出
*/
function setApprovalForAll(address operator, bool approved) external;

/**
* @dev バッチでアプルーブをチェックし、boolを返す。`operator`が`account`によってアプルーブされた場合,`true`を返す
*/
function isApprovedForAll(address account, address operator) external view returns (bool);

/**
* @dev セーフトランスファ、`amount`数量の`id`種類のtoken
* {TransferSingle}イベントを放出
* 条件:
* - callerがownerではない場合、権限もっていないと使えない
* - `from`は十分なtokenをもっている
* - 送り先がコントラクトの場合,`IERC1155Receiver`の`onERC1155Received`関数を実装していないと通らない
*/
function safeTransferFrom(
address from,
address to,
uint256 id,
uint256 amount,
bytes calldata data
) external;

/**
* @dev バッチセーフトランスファ
* {TransferBatch}イベントを放出
* 条件:
* - `ids`、`amounts`のlengthが同じ
* - 送り先がコントラクトの場合,`IERC1155Receiver`の`onERC1155Received`関数を実装していないと通らない
*/
function safeBatchTransferFrom(
address from,
address to,
uint256[] calldata ids,
uint256[] calldata amounts,
bytes calldata data
) external;
}

ERC1155 のイベント

  • TransferSingle event: 単一な種類のトランスファイベント、トランスファが起きたときに放出
  • TransferBatch event: バッチトークントランスファのイベント、バッチのトランスファが起きたときに放出
  • ApprovalForAll event: バッチアプルーブのイベント、バッチアプルーブが起きたときに放出
  • URI event: metadata のアドレス変更のイベント、uri変更時に放出

IERC1155

  • balanceOf(): 単一な種類の残高をチェックする。accountの持っているid種類の token のバランス
  • balanceOfBatch(): 多種類のバランスをチェック。チェックするaccountsidsとの length が同じである必要がある
  • setApprovalForAll(): バッチアプルーブ、caller の token を operator に権限を移譲する
  • isApprovalForAll(): バッチアプルーブの情報をチェックする。operatoraccountによって権限をもらっている場合はtrueを返す
  • safeTransferFrom(): セーフな単一の token のトランスファ。amount数量のid種類の token をfromから、toへ送る。toがコントラクトの場合、onERC1155BatchReceived()関数の実装があるかをチェックされる。

ERC1155 を受け取るために、用意するコントラクトの形

ERC721と同じように、NFT を送ってロックされてしまうことを避けるために、ERC1155は受け取る側のコントラクトにIERC1155Receiverコントラクトを継承し、2 つの関数を実装しなければならない。

  • onERC1155Recieved(): 単一な種類の token を受け取るための関数。ERC1155 のセーフトランスファであるsafeTransferFrom関数からのトランスファを受けるためには、自分自身の selector である0xf23a6e61を返す
  • onERC1155BatchReceived(): 複数種類の token を受け取る用の関数。ERC1155のセーフトランスファsafeBatchTransferFromからのトランスファを受け取るために、自分自身で selector0xbc197c81を返す必要がある。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "https://github.com/AmazingAng/WTFSolidity/blob/main/34_ERC721/IERC165.sol";

/**
* @dev ERC1155を受け取るためのコントラクト、ERC1155のtokenを受け取るにはこれを実装しないといけない
*/
interface IERC1155Receiver is IERC165 {
/**
* @dev ERC1155の`safeTransferFrom`のトランスファを受ける
* 0xf23a6e61 あるいは `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`を返す
*/
function onERC1155Received(
address operator,
address from,
uint256 id,
uint256 value,
bytes calldata data
) external returns (bytes4);

/**
* @dev ERC1155の`safeBatchTransferFrom`を受ける
* 0xbc197c81 あるいは `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`を返す
*/
function onERC1155BatchReceived(
address operator,
address from,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
) external returns (bytes4);
}

ERC1155 のメインコントラクト

メインコントラクトはIERC1155のインターフェイス規定の関数を実装しました。また、単一な種類/複数の種類の token のミント、バーン関数も実装しました。

状態変数は4つ

  • name: token 名
  • symbol: token シンボル
  • _balances: token バランスのマッピング。idの owner のアドレスのバランスを記録
  • _operatorApprovals: バッチでアプルーブをするためのマッピング。残高を持っているアドレスのアプルーブ状態を記録

ERC1155 関数

全部で 16 個の関数を持っています。 もちろん、ERC1155規定の関数を実装しています。

  • constructor: 引数は name, symbol
  • supportsInterface(): ERC165スタンダードを実現、support するインターフェイスを返す。他のコントラクトがチェックする用に準備する
  • balanceOf(): IERC1166の残高をチェックする関数。ERC721と違うのは、引数はaccount及びid
  • balanceOfBatch(): バッチで複数のバランスを返す関数
  • setApprovalForAll(): バッチで複数のアプルーブをする関数。ApprovalForAllイベントを放出する
  • isApprovedForAll(): 全部の権限を持っているアドレスなのかの確認をする関数。
  • safeTransferFrom(): 単一な種類の token のトランスファをする関数。TransferSingleイベントを放出。ERC721と違うのは、引数はfrom, to, id以外にも、amountというのが必要となる。
  • safeBatchTransferFrom(): 複数種類の token をトランスファする関数。TransferBatchを放出。
  • _mint(): 一種類の token を鋳造
  • _mintBatch(): 複数種類の token を鋳
  • _burn(): 一種類の token を burn する
  • _burBatch(): 複数種類の token を burn する
  • doSafeTransferAcceptanceCheck(): 一種類の token のトランスファのセーフチェック。safeTransferFrom()によって使われる。onERC1155received()関数を実装しているかをチェックする。
  • uri(): ERC1155idの種類の metadata のリンクを返す。ERC721tokenURIと似ている。
  • baseURI(): baseURIを返す。uri はbaseURIidを接続するので、通常は開発者が書き換える必要がある。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./IERC1155.sol";
import "./IERC1155Receiver.sol";
import "./IERC1155MetadataURI.sol";
import "https://github.com/AmazingAng/WTFSolidity/blob/main/34_ERC721/Address.sol";
import "https://github.com/AmazingAng/WTFSolidity/blob/main/34_ERC721/String.sol";
import "https://github.com/AmazingAng/WTFSolidity/blob/main/34_ERC721/IERC165.sol";

/**
* @dev ERC1155スタンダードのimplementation
* 詳細はhttps://eips.ethereum.org/EIPS/eip-1155
*/
contract ERC1155 is IERC165, IERC1155, IERC1155MetadataURI {
using Address for address; // library Address
using Strings for uint256; // library Strings
// Token名
string public name;
// Tokenシンボル
string public symbol;
// token種類のid → account → balances のマッピング、残高を記録する用
mapping(uint256 => mapping(address => uint256)) private _balances;
// address → adderss の全権移譲の記録をするマッピング
mapping(address => mapping(address => bool)) private _operatorApprovals;

/**
* コンストラクタ、初期化の値`name` 、`symbol`
*/
constructor(string memory name_, string memory symbol_) {
name = name_;
symbol = symbol_;
}

/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return
interfaceId == type(IERC1155).interfaceId ||
interfaceId == type(IERC1155MetadataURI).interfaceId ||
interfaceId == type(IERC165).interfaceId;
}

/**
* @dev バランスを返す関数、IERC1155のbalanceOf。accountのid種類のtoken数を返す
*/
function balanceOf(address account, uint256 id) public view virtual override returns (uint256) {
require(account != address(0), "ERC1155: address zero is not a valid owner");
return _balances[id][account];
}

/**
* @dev 複数のバランスを返す
* 条件:
* - `accounts` 、 `ids` のlengthが同じでなければならない.
*/
function balanceOfBatch(address[] memory accounts, uint256[] memory ids)
public view virtual override
returns (uint256[] memory)
{
require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch");
uint256[] memory batchBalances = new uint256[](accounts.length);
for (uint256 i = 0; i < accounts.length; ++i) {
batchBalances[i] = balanceOf(accounts[i], ids[i]);
}
return batchBalances;
}

/**
* @dev 複数のアプルーブをする関数。callerはoperatorに全権移譲をする
* {ApprovalForAll}オベントを放出
* 条件:msg.sender != operator
*/
function setApprovalForAll(address operator, bool approved) public virtual override {
require(msg.sender != operator, "ERC1155: setting approval status for self");
_operatorApprovals[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}

/**
* @dev バッチ確認、複数
*/
function isApprovedForAll(address account, address operator) public view virtual override returns (bool) {
return _operatorApprovals[account][operator];
}

/**
* @dev セーフトランスファ,`amount`数量の`id`tokenを`from`から`to`へ送る関数
* {TransferSingle} イベントを放出
* 条件:
* - to ゼロアドレスでないこと
* - fromアドレスは十分なtoken数を持っており、callerは権限を持っていること
* - to がスマートコントラクトの場合、IERC1155Receiver-onERC1155Receivedをサポートしていること
*/
function safeTransferFrom(
address from,
address to,
uint256 id,
uint256 amount,
bytes memory data
) public virtual override {
address operator = msg.sender;
// ownerか、権限をもらっているかのチェック
require(
from == operator || isApprovedForAll(from, operator),
"ERC1155: caller is not token owner nor approved"
);
require(to != address(0), "ERC1155: transfer to the zero address");
// fromは十分なtoken数を持っているかチェック
uint256 fromBalance = _balances[id][from];
require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
// バランスを更新
unchecked {
_balances[id][from] = fromBalance - amount;
}
_balances[id][to] += amount;
// イベント放出
emit TransferSingle(operator, from, to, id, amount);
// セーフなトランスファのチェック
_doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data);
}

/**
* @dev バッチで複数のトランスファ,`amounts`という数量の配列、`ids`というtoke種類の
* 配列を使って`from`から`to`へ送る
* {TransferSingle} イベント
* 条件:
* - to ゼロアドレスでない
* - fromアドレスは十分なtoken数を持っており、callerは権限を持っていること
* - to がスマートコントラクトの場合、IERC1155Receiver-onERC1155Receivedをサポートしていること
* - ids、amountsの配列のlengthが同じである
*/
function safeBatchTransferFrom(
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) public virtual override {
address operator = msg.sender;
// ownerか、権限をもらっているかのチェック
require(
from == operator || isApprovedForAll(from, operator),
"ERC1155: caller is not token owner nor approved"
);
// 配列の長さが一緒である
require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");
// ゼロアドレスでないこと
require(to != address(0), "ERC1155: transfer to the zero address");

// for loopでバランスを更新
for (uint256 i = 0; i < ids.length; ++i) {
uint256 id = ids[i];
uint256 amount = amounts[i];

uint256 fromBalance = _balances[id][from];
require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
unchecked {
_balances[id][from] = fromBalance - amount;
}
_balances[id][to] += amount;
}

emit TransferBatch(operator, from, to, ids, amounts);
// セーフなコントラクトのチェック
_doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, amounts, data);
}

/**
* @dev mint鋳造関数
* {TransferSingle} イベントを放出
*/
function _mint(
address to,
uint256 id,
uint256 amount,
bytes memory data
) internal virtual {
require(to != address(0), "ERC1155: mint to the zero address");

address operator = msg.sender;

_balances[id][to] += amount;
emit TransferSingle(operator, address(0), to, id, amount);

_doSafeTransferAcceptanceCheck(operator, address(0), to, id, amount, data);
}

/**
* @dev バッチで鋳造
* 释放 {TransferBatch} 事件.
*/
function _mintBatch(
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) internal virtual {
require(to != address(0), "ERC1155: mint to the zero address");
require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");

address operator = msg.sender;

for (uint256 i = 0; i < ids.length; i++) {
_balances[ids[i]][to] += amounts[i];
}

emit TransferBatch(operator, address(0), to, ids, amounts);

_doSafeBatchTransferAcceptanceCheck(operator, address(0), to, ids, amounts, data);
}

/**
* @dev バーン関数
*/
function _burn(
address from,
uint256 id,
uint256 amount
) internal virtual {
require(from != address(0), "ERC1155: burn from the zero address");

address operator = msg.sender;

uint256 fromBalance = _balances[id][from];
require(fromBalance >= amount, "ERC1155: burn amount exceeds balance");
unchecked {
_balances[id][from] = fromBalance - amount;
}

emit TransferSingle(operator, from, address(0), id, amount);
}

/**
* @dev バーン関数:複数を同時に実行
*/
function _burnBatch(
address from,
uint256[] memory ids,
uint256[] memory amounts
) internal virtual {
require(from != address(0), "ERC1155: burn from the zero address");
require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");

address operator = msg.sender;

for (uint256 i = 0; i < ids.length; i++) {
uint256 id = ids[i];
uint256 amount = amounts[i];

uint256 fromBalance = _balances[id][from];
require(fromBalance >= amount, "ERC1155: burn amount exceeds balance");
unchecked {
_balances[id][from] = fromBalance - amount;
}
}

emit TransferBatch(operator, from, address(0), ids, amounts);
}

// @dev ERC1155のセーフトランスファのチェック
function _doSafeTransferAcceptanceCheck(
address operator,
address from,
address to,
uint256 id,
uint256 amount,
bytes memory data
) private {
if (to.isContract()) {
try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) {
if (response != IERC1155Receiver.onERC1155Received.selector) {
revert("ERC1155: ERC1155Receiver rejected tokens");
}
} catch Error(string memory reason) {
revert(reason);
} catch {
revert("ERC1155: transfer to non-ERC1155Receiver implementer");
}
}
}

// @dev ERC1155の複数セーフトランスファのチェック
function _doSafeBatchTransferAcceptanceCheck(
address operator,
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) private {
if (to.isContract()) {
try IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, amounts, data) returns (
bytes4 response
) {
if (response != IERC1155Receiver.onERC1155BatchReceived.selector) {
revert("ERC1155: ERC1155Receiver rejected tokens");
}
} catch Error(string memory reason) {
revert(reason);
} catch {
revert("ERC1155: transfer to non-ERC1155Receiver implementer");
}
}
}

/**
* @dev ERC1155のidのuriを返す、metadata、ERC721のtokenURIに同じ
*/
function uri(uint256 id) public view virtual override returns (string memory) {
string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, id.toString())) : "";
}

/**
* {uri}のBaseURIを返す,uriはbaseURI、tokenIdをつないだもの
*/
function _baseURI() internal view virtual returns (string memory) {
return "";
}
}

remix で作成

今回 remix で作るのは ERC1155 版の BAYC。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "./ERC1155.sol";

contract BAYC1155 is ERC1155{
uint256 constant MAX_ID = 10000;
// コンストラクタ
constructor() ERC1155("BAYC1155", "BAYC1155"){
}

//BAYCのbaseURIはこれ:ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/
function _baseURI() internal pure override returns (string memory) {
return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/";
}

// ミント関数
function mint(address to, uint256 id, uint256 amount) external {
// id は10,000を超えてはならない
require(id < MAX_ID, "id overflow");
_mint(to, id, amount, "");
}

// バッチでミント
function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts) external {
// id は10,000を超えてはならない
for (uint256 i = 0; i < ids.length; i++) {
require(ids[i] < MAX_ID, "id overflow");
}
_mintBatch(to, ids, amounts, "");
}

}

実際の remix については、また今度の機会に書きますので、今回は割愛。

最後に

今回はERC1155について学習しました。いかがでしょうか。このスタンダードは同じコントラクトに複数の NFT や FT の共存を許したコントラクトを可能にしたので、個人的には大きなステップだと思っています。また、BYAC の改造をして無理やりERC1155にしました。ほんじゃ、またね。

THE END

· 約7分
Thurendous
Polymetis

Hello, everybody!

TL;DR

  • NFT は非代替性トークンのこと
  • シンプルにいうとデジタル所有権のこと
  • コードで理解しましょう

BTC や ETH のようなトークンは FT と呼ばれ、代替性トークンのことです。特徴としては、お互いに交換可能で大きな違いはないことです(厳密には違いもありますが、ここではその議論をしない)。また、アート、コレクション、不動産などのようなお互い違いをかなり持っているようなものは非代替性トークンで代表されることがほとんどです。

イーサリアムでは EIP721 が提案されて、ERC721 のスタンダードが形成されました。

EIP, ERC とは

結論:EIP が ERC を含んでいる

まず、理解しなければならないのは、ERC721 です。この2つの間にはなんの関係があるのでしょうか。EIP とは、Ethereum Improvement Proposals のことで、イーサリアムコミュニティが提案したプロトコルなどを改善するための提案のことです。EIP はイーサリアムの中の任意の分野の改善で、例えば新たな機能、ERC、プロトコル改善などがあります。

ERC とは、Ethereum Request For Comment のことで、イーサリアム上の各種アプリケーションのプロトコルとスタンダードのこととなっています。典型的な ERC20, ERC721、あるいは URI のスタンダード ERC67、あるいはウォレットのフォーマット EIP75, EIP85 などがある。

ERC スタンダードはイーサリアムの発展における重要な構成要素で、ERC20, ERC721, ERC223, ERC777 などのスタンダードがイーサリアムのエコシステムに多大な影響を与えていました。

ERC165

まず ERC165 について理解しましょう。 スマートコントラクトはインターフェースを宣言して他のスマートコントラクトがチェックするためにやっているのが ERC165 のことです。

シンプルにいうと、ERC165 を通してとあるコントラクトが ERC721, ERC1155 をサポートしているかチェックできるという仕組みです。

interface IERC165 {
/**
* @dev コントラクトが当該スタンダードの`interfaceId`を実装していればtrueを返す
* 詳細はこちら:https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
*
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

次に実際には ERC721 がどのように supportsInterface()を実現したのか見てみましょう。

function supportsInterface(bytes4 interfaceId) external pure override returns (bool)
{
return
interfaceId == type(IERC721).interfaceId ||
interfaceId == type(IERC165).interfaceId;
}

この関数supportsInterfaceは IERC721 あるいは IERC165 の interfaceId が引数として入力された場合に、trueを返し、そうでない場合はfalseを返します。

IERC721

IERC721の中身を見てみましょう。 IERC721ERC721のインターフェーススタンダードのコントラクトで、ERC721 が実現すべき一般的な関数を定義してます。tokenIdを使って非代替性トークンを代表しています。アプルーブあるいはトランスファに際して、tokenIdは必ず出番があります。しかし、ERC20 はトランスファにおける数量だけを定義すればよくて、tokenId はありません。

/**
* @dev ERC721スタンダードのインターフェース
*/
interface IERC721 is IERC165 {
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

function balanceOf(address owner) external view returns (uint256 balance);

function ownerOf(uint256 tokenId) external view returns (address owner);

function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes calldata data
) external;

function safeTransferFrom(
address from,
address to,
uint256 tokenId
) external;

function transferFrom(
address from,
address to,
uint256 tokenId
) external;

function approve(address to, uint256 tokenId) external;

function setApprovalForAll(address operator, bool _approved) external;

function getApproved(uint256 tokenId) external view returns (address operator);

function isApprovedForAll(address owner, address operator) external view returns (bool);
}

IERC721 の event

3 つの event を含まれています。TransferApprovalは ERC20 にも存在します。

  • Transfer: トークントランスファのときに放出。送り元from、送り先totokenId
  • Approvel: アプルーブするときに放出。アプルーブする側のアドレスowner、アプルーブした先のアドレスapprovedtokenId
  • ApprovalForAll: 一気に大量にアプルーブするときに放出するイベント。アプルーブする側のアドレスowner、アプルーブされる側のアドレスoperator、アプルーブする場合approvedは true で、逆に権限を剥奪する場合、approvedは false にする

IERC721 関数

  • balanceOf: とあるアドレスの持っている NFT の数量を返す
  • ownerOf: tokenId のオーナーを返す
  • transferFrom: 普通のトランスファ、引数は from は送り元で、to は送り先、tokenId も必要
  • safeTransferFrom:安全なトランスファ、もし受ける側はコントラクトの場合、ERC721Receiverの実装が求められる。引数は送り元のfrom、送り先のtotokenId
  • approve: 他のアドレスにあなたの NFT を使用する権利を渡す。権利を付与するアドレスはto、そしてtokenIdも引数に
  • getApproved: tokenIdがどのアドレスに権限を付与したのか確認する
  • setApprovalFroAll: 自分の持っているこのコントラクトのすべての NFT をとあるアドレスに対して全権移譲する
  • isApprovedForAll: 全権移譲しているアドレスがあるか確認する
  • safeTransferFrom: 安全なトランスファ関数のオーバーライド関数、引数にdata`が含まれている

IERC721Receiver

コントラクトがもし ERC721 の実現をしていない場合、送られてきた NFT はブラックホールに送ったように、永遠に取り出せなくなります。これを防止するために、ERC721 はsafetransferFrom()関数を実装している。ターゲットコントラクトがIERCReceiverインターフェースを実装している場合のみ、ERC721 トークンを受け取ることができます。そうでない場合はrevertされます。IERC721Receiver インターフェースは一個のonERC721Receiver()関数しかありません。

// ERC721Receiverのインターフェース: コントラクトはこれを実装して安全なトランスファを受けることができる
interface IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint tokenId,
bytes calldata data
) external returns (bytes4);
}

次にERC721がどうやって_checkOnERC721Receivedを使ってコントラクトがonERC721Receiver()関数を実装していることを確認しているのを見てみましょう。

function _checkOnERC721Received(
address from,
address to,
uint tokenId,
bytes memory _data
) private returns (bool) {
if (to.isContract()) {
return
IERC721Receiver(to).onERC721Received(
msg.sender,
from,
tokenId,
_data
) == IERC721Receiver.onERC721Received.selector;
} else {
return true;
}
}

IERC721Metadata

IERC721Metadata は ERC721 の拡張インターフェイス。3つの metadata 用関数を用いている。

  • name():トークン名を返す
  • symbol():トークン符号
  • tokenURI():tokenId を使って metadata の url をとってくる。ERC721 特有の関数
interface Ierc721Metadata is IERC721 {
function name() external view returns (string memory);

function symbol() external view returns (string memory);

function tokenURI(uint256 tokenId) external view returns (string memory);
}

ERC721 メインコントラクト

ERC721 メインコントラクトは IERC721, IERC165, IERC721Metadata のすべての機能を定義した。4 つの状態変数、17 個の関数を含まれている。シンプルに実装されている。詳細はコメントを読んでください。

// SPDX-License-Identifier: MIT
// by 0xAA
pragma solidity ^0.8.4;

import "./IERC165.sol";
import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "./IERC721Metadata.sol";
import "./Address.sol"; // library
import "./String.sol"; // library

contract ERC721 is IERC721, IERC721Metadata{
using Address for address; // Address libraryを使う(isContractを使うため)
using Strings for uint256; // String library

// Token名
string public override name;
// Tokenシンボル
string public override symbol;
// tokenId -> owner address のマッピング
mapping(uint => address) private _owners;
// address -> バランスのマッピング
mapping(address => uint) private _balances;
// tokenID -> アプルーブされたアドレスのマッピング
mapping(uint => address) private _tokenApprovals;
// owner -> operatorアドレス -> bool(権限渡した場合はtrue)
mapping(address => mapping(address => bool)) private _operatorApprovals;

/**
* constructor, name, symbolを初期化する
*/
constructor(string memory name_, string memory symbol_) {
name = name_;
symbol = symbol_;
}

// ERC165のsupportsInterface関数
function supportsInterface(bytes4 interfaceId)
external
pure
override
returns (bool)
{
return
interfaceId == type(IERC721).interfaceId ||
interfaceId == type(IERC165).interfaceId ||
interfaceId == type(IERC721Metadata).interfaceId;
}

// IERC721のbalanceOf関数、_balancesを使って調べたいアドレスの残高を返す
function balanceOf(address owner) external view override returns (uint) {
require(owner != address(0), "owner = zero address");
return _balances[owner];
}

// IERC721のownerOf関数、_owners変数を使ってtokenIdのownerを返す
function ownerOf(uint tokenId) public view override returns (address owner) {
owner = _owners[tokenId];
require(owner != address(0), "token doesn't exist");
}

// IERC721のisApprovedForAll、_operatorApprovals変数を
// 使ってownerがoperatorに権限を移譲したかどうかをチェックする。
// 権限移譲した場合はtrueを返す
function isApprovedForAll(address owner, address operator)
external
view
override
returns (bool)
{
return _operatorApprovals[owner][operator];
}

// IERC721のsetApprovalForAllを実装。持っているトークンをすべてoperatorに権限を渡す(true),
// あるいはoperatorの権限を剥奪する(false)。_setApprovalForAll関数を呼び出す。
function setApprovalForAll(address operator, bool approved) external override {
_operatorApprovals[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}

// 实现IERC721的getApproved,利用_tokenApprovals变量查询tokenId的授权地址。
function getApproved(uint tokenId) external view override returns (address) {
require(_owners[tokenId] != address(0), "token doesn't exist");
return _tokenApprovals[tokenId];
}

// _approve関数。_tokenApprovalsを書き換えて,toアドレスに tokenIdをいじる権限を渡す。
// Approvalイベントを放出。
function _approve(
address owner,
address to,
uint tokenId
) private {
_tokenApprovals[tokenId] = to;
emit Approval(owner, to, tokenId);
}

// IERC721のapprove関数,tokenIdの権限を to アドレスに渡す。
// 条件:toはownerではないこと,かつmsg.senderはownerあるいはapproveされたアドレス。
// _approve関数を呼び出す
function approve(address to, uint tokenId) external override {
address owner = _owners[tokenId];
require(
msg.sender == owner || _operatorApprovals[owner][msg.sender],
"not owner nor approved for all"
);
_approve(owner, to, tokenId);
}

// spenderアドレスがtokenIdを使う権限があるかないかを調べる。(あるのはownerか
// approveされたかのいずれだ。approveされた場合は通常のapproveあるいは
// setApprovalForAllの2パターン)
function _isApprovedOrOwner(
address owner,
address spender,
uint tokenId
) private view returns (bool) {
return (spender == owner ||
_tokenApprovals[tokenId] == spender ||
_operatorApprovals[owner][spender]);
}

/*
* トランスファ関数。_balances、_ownerのバランスを調整して tokenId を from から toに
* トランスファする。同時にTransferイベントを放出。
* 前提条件:
* 1. tokenId は from によって所有されている
* 2. to はゼロアドレスでない
* 条件を満たさない場合はrevert
*/
function _transfer(
address owner,
address from,
address to,
uint tokenId
) private {
require(from == owner, "not owner");
require(to != address(0), "transfer to the zero address");

_approve(owner, address(0), tokenId); // トランスファするので権限をリセットする

_balances[from] -= 1;
_balances[to] += 1;
_owners[tokenId] = to;

emit Transfer(from, to, tokenId);
}

// IERC721のtransferFrom関数の実装,セーフトランスファではないので、
// この関数を使うのは推奨されていない。
function transferFrom(
address from,
address to,
uint tokenId
) external override {
address owner = ownerOf(tokenId);
require(
_isApprovedOrOwner(owner, msg.sender, tokenId),
"not owner nor approved"
);
_transfer(owner, from, to, tokenId);
}

/**
* セーフトランスファ,安全にtokenIdをfromからtoへトランスファする。スマートコントラクトが
* erc721に対応しているかどうかをチェックした上で、トランスファをするので、NFTが永遠に
* ロックされる実態を回避する。_transfer、_checkOnERC721Received関数を呼び出す。
* 条件:
* from はゼロアドレスではない
* to はゼロアドレスではない
* tokenId が存在してしかもfromアドレスが所有
* toがスマートコントラクトの場合、必ずIERC721Receiver-onERC721Receivedをサポートされる
* ことが求められる
*/
function _safeTransfer(
address owner,
address from,
address to,
uint tokenId,
bytes memory _data
) private {
_transfer(owner, from, to, tokenId);
require(_checkOnERC721Received(from, to, tokenId, _data), "not ERC721Receiver");
}

/**
* IERC721のsafeTransferFromを実装。セーフトランスファ関数,_safeTransfer関数を呼び出している
*/
function safeTransferFrom(
address from,
address to,
uint tokenId,
bytes memory _data
) public override {
address owner = ownerOf(tokenId);
require(
_isApprovedOrOwner(owner, msg.sender, tokenId),
"not owner nor approved"
);
_safeTransfer(owner, from, to, tokenId, _data);
}

// safeTransferFromのオーバライド関数、引数が違う
function safeTransferFrom(
address from,
address to,
uint tokenId
) external override {
safeTransferFrom(from, to, tokenId, "");
}

/**
* mint関数。_balances、_ownersのバランスをいじることで、tokenIdをtoへトランスファする。
* 同時にTransferイベントを放出する。
* 現在の状態では、誰でもミントできるので、開発者は普通この関数を書き換える
* 条件:
* 1. tokenIdがまだ存在しない
* 2. toはゼロアドレスでない
*/
function _mint(address to, uint tokenId) internal virtual {
require(to != address(0), "mint to zero address");
require(_owners[tokenId] == address(0), "token already minted");

_balances[to] += 1;
_owners[tokenId] = to;

emit Transfer(address(0), to, tokenId);
}

// バーン関数,_balances、_owners変数を調整してtokenIdをバーンする。同時にTransferイベントを放出
// 条件:tokenId存在。
function _burn(uint tokenId) internal virtual {
address owner = ownerOf(tokenId);
require(msg.sender == owner, "not owner of token");

_approve(owner, address(0), tokenId); // 権限を更新

_balances[owner] -= 1;
delete _owners[tokenId];

emit Transfer(owner, address(0), tokenId);
}

// _checkOnERC721Received:IERC721Receiver-onERC721Received関数, 送り先はERC721互換かどうかをチェックするため
function _checkOnERC721Received(
address from,
address to,
uint tokenId,
bytes memory _data
) private returns (bool) {
if (to.isContract()) {
return
IERC721Receiver(to).onERC721Received(
msg.sender,
from,
tokenId,
_data
) == IERC721Receiver.onERC721Received.selector;
} else {
return true;
}
}

/**
* IERC721MetadataのtokenURI関数,metadataを返す
*/
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
require(_owners[tokenId] != address(0), "Token Not Exist");

string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
}

/**
* {tokenURI}のBaseURI。tokenURIはbaseURI、tokenIdをつないでできたもの。
* 開発者がこの関数を書きかえる
* 例えばBAYCのbaseURIはipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/
*/
function _baseURI() internal view virtual returns (string memory) {
return "";
}
}

free mint の APE を作ろう

それでは、free mint の APE を作りましょうか。totalSupply を 10000 個にして、mint 関数や baseURI を書き換えるだけで済みます。

baseURI()の設定を BAYS と全く同じようにすることで、BAYC の猿が表示されるはずです。


// SPDX-License-Identifier: MIT
// by 0xAA
pragma solidity ^0.8.4;

import "./ERC721.sol";

contract OmaenoApe is ERC721{
uint public MAX_APES = 10000; // 総数

// コンストラクタ
constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_){
}

//BAYCのbaseURIはipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/
function _baseURI() internal pure override returns (string memory) {
return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/";
}

// mint関数
function mint(address to, uint tokenId) external {
require(tokenId >= 0 && tokenId < MAX_APES, "tokenId out of range");
_mint(to, tokenId);
}
}

それでは remix を開いてコードを書きましょう。

ERC721 を発行しよう

ERC721 スタンダードがあれば、ブロックチェーンにて NFT を発行することは非常にシンプルになります。 今、上記のコードができたので、remix にて発行しましょう。

実際の手順はまたの機会で書きます。

ERC165 と ERC721

NFT を NFT についてコントロールできないコントラクトへ送付してしまうと、永遠に消失してしまうため、これを防ぐためにERC721TokenReceiverインターフェイスの実装が求められます。

interface ERC721TokenReceiver {
function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);
}

実はこのインターフェイスとは、この onERC721REceived 関数のことです。この関数を実装していれば、NFT を処理する能力があるという宣言になります。

THE END