bitbank

Smart Contract Solidity ブロックチェーン

ERC1155について非エンジニアでもわかるように概要から実装まで1から丁寧にわかりやすく解説

かるでね

こんにちは!実務でPython、Djangoを使って、機械学習やWebアプリケーション開発などをしているかるでねです!
最近はSolidityを使ってスマートコントラクト開発やバグを見つけたりしています。

今回は「ERC1155」について1からわかりやすく解説していきます!

ERC1155」がわからなかったり、なんとなく理解している非エンジニアやこれから「ERC1155」を理解しようとしているエンジニアの方向けの記事になります。

丁寧に解説している分だいぶ長くなってしまっているので時間があるときに一気に読んでみるのをお勧めします。

それでは早速中身を見ていきましょう!

ERC1155とは?

前提

ERC1155を理解する上で、ERC20とERC721の理解が必要になります。

ERC20とは、「Ethereumブロックチェーンで動作する統一されたトークン規格の1つ」です。

ERC20について詳しく知るには以下の記事を参考にしてください。

ERC721は「非代替性トークン」と呼ばれていて、NFTを発行できる規格です。

ERC721について詳しく知るには以下の記事を参考にしてください。

ERC1155は、このERC20が持つ特徴とERC721が持つ「非代替性」の2つを兼ね備えたトークン規格になります。

ERC1155の特徴

ERC1155は「マルチスタンダードトークン」と言われていて、簡単にいうと「複数のトークンを複数の相手に一度送ることができる規格」です。

では特徴を1つずつ確認していきましょう。

複数のトークンを送れる

ERC20やERC721では異なる複数のトークンを送る際、1つずつトークンを送る必要がありました。

以下のように異なるトークンごとにコントラクトに送る許可(approve)を与え、許可を与えられたコントラクトがまとめて送る必要がありました。

送りたいトークンの種類が増えると、この1つずつ許可を与える作業が大変になります。

そこでERC1155を使用することで、以下の図のように複数の異なるトークンをひとまとめにして、送る許可をコントラクトに与えることができます。

ここで注意する必要があることは、ERC1155規格のトークンのみをひとまとめにすることができ、ERC20やERC721規格のトークンが混ざっている場合は、ERC20やERC721規格のトークンのみひとまとめにできません。

例えばゲーム内の剣や盾、鎧などの複数アイテムをマーケットプレイスで販売するとします。

ERC721規格であればアイテムを1つずつ出品しなければいけなかったのですが、ERC1155を使用することで一度に複数のアイテムを出品できるようになります。

複数の相手に送れる

ERC20やERC721では複数のユーザーに送る際、1つずつトークンを送る必要がありました。

これは手間であると同時にトランザクションごとにガス代が発生します。

ERC1155を使用することで、以下のように複数の相手に一度のトランザクションでトークンを送ることができます。

ERC20やERC721では1つずつ送るごとにガス代が発生していましたが、ERC1155では一度のトランザクションで済むため、ガス代の節約につながります。

FT/NFTの両方に使用できる

ここまで読んでいて気付いた方もいると思いますが、ERC1155はERC20規格のFT(代替性トークン)とERC721規格のNFT(非代替性トークン)の両方で使用することができます。

本来は1つのコントラクトで1つのトークンを扱っていましたが、ERC1155では1つのコントラクトで複数のトークンを扱うことができます。

各トークンには「アイテムID」が紐づいていて、この「アイテムID」に紐づいている所有者のアドレスを付け替えることで、FTとNFTの交換が可能になります。

EIP1155

EIPとは

前章までERC1155といってきましたが、EIP1155とは何でしょうか?

EIPとは 「Ethereum Improvement Proposals」の略で、「Ethereumの新しい機能やプロセスに関する提案を規定する標準規格」のことです。

EIPの細かい説明は以下に書かれています。

簡単に言うと以下になります。

EIPは、Ethereum Improvement Proposalsの略でイーサリアムをより良いものにするために議論される改善提案のこと。イーサリアムは、特定の誰かによって管理されるものではないため、世界中の誰もがEIPを提出することで、イーサリアムの発展に貢献することができる。

1155という数字は1155番目の提案ということです。

以下がEIP1155についてまとめられているページですが、これをすべて説明するとだいぶ長くなってしまうので、必須機能部分のみこの章でまとめていきます。

EIP1155の必須関数

では次にEIP1155の必須関数について確認していきます。

safeTransferFrom

function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) external;

_idで指定したトークンを_valueで指定した量だけ、_fromで指定したアドレスから_toで指定したアドレスに送る関数。

条件

  • _toには0アドレスを指定できない。
  • _fromで指定したアドレスは_idで指定したトークンの所有者か、_idで指定したトークンの操作権限が与えられたアドレスでなければいけない。
  • _fromで指定したアドレスは、_idで指定したトークンを_value以上所有していないければいけない。
  • _toにコントラクトのアドレスを渡すときは、送り先のコントラクトでIERC1155ReceiverコントラクトのonERC1155Received関数を実装してある必要がある。

safeBatchTransferFrom

function safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external;

複数のトークンを効率的に送ることができる関数。

_idsで指定した各トークンを_valuesで指定した配列内の同じインデックス番号の値だけ、_fromで指定したアドレスから_toで指定したアドレスに送ることができます。

  • _ids = [5, 8, 13]
  • _values = [100, 20, 50]

上記の場合以下のようになります。

  • IDが5のトークンを100_fromで指定したアドレスから_toに指定したアドレスへ送る。
  • IDが8のトークンを20_fromで指定したアドレスから_toに指定したアドレスへ送る。
  • IDが13のトークンを50_fromで指定したアドレスから_toに指定したアドレスへ送る。

条件

  • _ids_valuesの配列の長さは同じでなければいけない。
  • _toにコントラクトのアドレスを渡すときは、送り先のコントラクトでIERC1155ReceiverコントラクトのonERC1155Received関数を実装してある必要がある。

balanceOf

function balanceOf(address _owner, uint256 _id) external view returns (uint256);

_ownerに指定したアドレスが、_idで指定したIDを持つトークンをどれくらい所有しているか返す関数。

条件

  • _ownerに0アドレスを指定できない。

balanceOfBatch

function balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids) external view returns (uint256[] memory);

_ownersで指定した各アドレスが所有している_idsで指定した配列内の同じインデックス番号のIDを持つトークンの量を返す関数。

  • _owners = [0x123..., 0xabc..., 0xXYZ...]
  • _ids = [5, 8 , 13]

上記の場合以下を配列にして返します。

  • 0x123...が所有しているIDが5のトークンの量。
  • 0xabc...が所有しているIDが8のトークンの量。
  • 0xXYZ...が所有しているIDが13のトークンの量。

条件

  • _owners配列と_ids配列の長さは同じでなければならない。

setApprovalForAll

function setApprovalForAll(address _operator, bool _approved) external;

setApprovalForAll関数を実行したアドレスが所有するERC1155トークンを、_operatorで指定したアドレスにtransferする権限を与える、もしくは取り除く。

_approvedの値がtrueならばtransferする権限を与え、_approvedの値がfalseならばtransferする権限を取り除く。

条件

  • _operatorのアドレスがsetApprovalForAll関数を実行したアドレスと異なる必要がある。

isApprovedForAll

function isApprovedForAll(address _owner, address _operator) external view returns (bool);

_operatorで指定したアドレスが、_ownerで指定したアドレスが所有するERC1155トークンのtrasnsfer権限があるか確認する関数。

trasnsfer権限があればtrueが返され、なければfalseが返される。

EIP1155のイベント系

次にEIP1155のイベントについて確認していきます。

TransferSingle

event TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value);

ERC1155トークンがtransferされた時に発行される。

TransferBatch

event TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values);

複数のERC1155トークンがtransferされた時に発行される。

ApprovalForAll

event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

_operatorで指定したアドレスに、_ownerアドレスが所有するERC1155トークンのtransferする許可を与えた時、もしくは取り除いた時に発行される。

URI

event URI(string _value, uint256 indexed _id);

_idで指定されたIDのERC1155トークンのURIが_valueで指定した値に変更された時に発行される。

このイベントで発行される値は、IERC1155MetadataURIコントラクトのuri関数が返す値と等しくなる。

ERC1155TokenReceiver

次にERC1155TokenReceiverについて確認していきます。

onERC1155Received

function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data) external returns(bytes4);

safeTransferFrom関数が安全に実行されるか確認する関数。

送り先がコントラクトの場合、ERC1155をサポートしていないと永久に送ったERC1155トークンにアクセスできなくなる。

これを防ぐために、送り先のコントラクトがERC1155をサポートしているかを確認しています。

条件

  • _operatorに渡されたアドレスは、safeTransferFrom関数実行アドレスからtransferを許可されたEOAアドレス、もしくはコントラクトアドレスでなければいけない。
  • _fromに渡されたアドレスは、送るERC1155トークンの所有者でなければならない。
    • ERC1155トークンを発行する場合は_fromには0アドレスが渡されなければならない。
  • _idに渡されたIDを持つERC1155トークンは、送られるERC1155トークンでなければならない。
  • _valueに渡された値は、_fromで指定したアドレスが所有するERC1155のトークン残高が減少し、受信者のERC1155が増加しなければならない。
  • _dataに渡された値は、_fromに渡されたアドレスからtransferによって渡されたデータの内容が変更されていない状態でないといけない。
  • 受信側のコントラクトでは、bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")を返した場合、transferは完了していなければならない。
  • 受信側のコントラクトでrevertを呼び出して、ERC1155のtransferを拒否した場合、トランザクションは元に戻されなければならない。
  • bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")以外が返されたときはトランザクションを元に戻さなければならない。
  • onERC1155Received関数は1回のトランザクションで複数回呼び出して良いが以下の条件を満たす必要がある。
    • 各呼び出しは相互に排他的である必要がある。
    • トランザクション中に発生したERC1155トークンの残高変更は、transfer順に記述する。
  • コントラクト自身にERC1155トークンを送る場合はonERC1155Received関数の呼び出しを省略しても良い。

onERC1155BatchReceived

function onERC1155BatchReceived(address _operator, address _from, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external returns(bytes4);

safeBatchTransferFrom関数が安全に実行されるか確認する関数。

条件

  • _operatorに渡されたアドレスは、safeTransferFrom関数実行アドレスからtransferを許可されたEOAアドレス、もしくはコントラクトアドレスでなければいけない。
  • _fromに渡されたアドレスは、送るERC1155トークンの所有者でなければならない。
    • ERC1155トークンを発行する場合は_fromには0アドレスが渡されなければならない。
  • _idsに渡された各IDを持つERC1155トークンは、送られるERC1155トークンでなければならない。
  • _valuesに渡された各値は、_fromで指定したアドレスが所有するERC1155のトークン残高が減少し、受信者のERC1155が増加しなければならない。
  • _dataに渡された値は、_fromに渡されたアドレスからtransferによって渡されたデータの内容が変更されていない状態でないといけない。
  • 受信側のコントラクトでは、bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")を返した場合、transferは完了していなければならない。
  • 受信側のコントラクトでrevertを呼び出して、ERC1155のtransferを拒否した場合、トランザクションは元に戻されなければならない。
  • bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")以外が返されたときはトランザクションを元に戻さなければならない。
  • onERC1155Received関数は1回のトランザクションで複数回呼び出して良いが以下の条件を満たす必要がある。
    • 各呼び出しは相互に排他的である必要がある。
    • トランザクション中に発生したERC1155トークンの残高変更は、transfer順に記述する。
  • コントラクト自身にERC1155トークンを送る場合はonERC1155Received関数の呼び出しを省略しても良い。

ERC1155Metadata_URI

最後に必須の機能ではないですが、ERC1155Metadata_URIについて説明していきます。

uri

function uri(uint256 _id) external view returns (string memory);

_idで指定されたIDのERC1155トークンのメタデータを返す関数。

メタデータとは、トークンの名前や詳細、画像などの情報がまとめられたものです。

条件

  • イベントが発行されていない場合、メタデータを取得するために実行されるべきである。
  • _idのイベントが発行された場合、そのイベントと同じ値を返さなければならない。
  • _idで指定したトークンが存在するかどうかのために使用してはいけない。
    • 理由としては、トークンが存在しない場合でも何らかの値を返すことができてしまうからである。

メタデータについて

ERC1155のコードを確認する前に一度メタデータについて確認していきます。

前章でも説明したようにメタデータとは、トークンの名前や詳細、画像などの情報がまとめられたものです。

IDについて

メタデータのURI(トークン識別子)に{id}という文字列が存在する場合、トークンのIDを16進数に置き換える必要があります。

この際以下のことに気をつける必要があります。

条件

  • 16進数のIDは小文字の英数字で、先頭に0xをつけてはいけない。
  • 16進数のIDの先頭部分を0でパディングする。

ちなみに置き換えた値は256ビットになります。

実際の値は以下のようになります。

ID

  • 0000000000000000000000000000000100000000000000000000000000000000
  • 1000000000000000000000000000000100000000000000000000000000000003
  • 1000000000000000000000000000000100000000000000000000000000000007
  • 1000000000000000000000000000000200000000000000000000000000000001

読みにくいですね…。

1つずつ確認していきましょう。

まずは32文字(128ビット)ずつで、2つに分割します。

そうすると、1つ目は0000000000000000000000000000000100000000000000000000000000000000の2つに分けることができます。

前半の00000000000000000000000000000001はトークンのIDを表しています。

トークンIDは1で、先ほどの条件から先頭部分を0でパディングしています。

また、FTの場合は先頭が0になり、NFTに場合は先頭に1がつきます。

後半の00000000000000000000000000000000はトークンのindexを表しています。

0番目のERC1155トークンであることがわかります。

では他の3つの値についても確認してみましょう。

1000000000000000000000000000000100000000000000000000000000000003なので、トークンIDが1のNFTで、indexは3です。

1000000000000000000000000000000100000000000000000000000000000007なので、トークンIDが1のNFTで、indexは7です。

1000000000000000000000000000000200000000000000000000000000000001なので、トークンIDが2のNFTで、indexは1です。

以下のURIがあるとします。

https://token-domain/{id}.json

この時トークンIDを置き換えると以下のようになります。

https://token-domain/1000000000000000000000000000000200000000000000000000000000000001.json

ここまでURIについて説明してきましたが、上記の実装は必須ではありません。

そのため1つの例だと認識してもらえると良いです。

メタデータ

メタデータはJSON形式で定義されます。

ERC1155でのJSONスキーマは、以下の「ERC721 Metadata JSON Schema」に基づいています。

{
    "title": "Token Metadata",
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "Identifies the asset to which this token represents"
        },
        "decimals": {
            "type": "integer",
            "description": "The number of decimal places that the token amount should display - e.g. 18, means to divide the token amount by 1000000000000000000 to get its user representation."
        },
        "description": {
            "type": "string",
            "description": "Describes the asset to which this token represents"
        },
        "image": {
            "type": "string",
            "description": "A URI pointing to a resource with mime type image/* representing the asset to which this token represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
        },
        "properties": {
            "type": "object",
            "description": "Arbitrary properties. Values may be strings, numbers, object or arrays."
        }
    }
}

ERC1155でのメタデータのJSONファイルは以下のようになります。

{
	"name": "Asset Name",
	"description": "Lorem ipsum...",
	"image": "https:\/s3.amazonaws.com﹑/your-bucket Filter/images﹑/{id}.png",
	"properties": {
		"simple_property": "example value",
		"rich_property": {
			"name": "Name",
			"value": "123",
			"display_value": "123 Example Value",
			"class": "emphasis",
			"css": {
				"color": "#ffffff",
				"font-weight": "bold",
				"text-decoration": "underline"
			}
		},
		"array_property": {
			"name": "Name",
			"value": [1,2,3,4],
			 "class": "emphasis"
		}
} } }

また、全ての言語間で表現の統一性を高めるために標準化されるべきです。

そのため、以下のようにlocalizationという値を持たせることができます。

{
    "title": "Token Metadata",
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "Identifies the asset to which this token represents",
        },
        "decimals": {
            "type": "integer",
            "description": "The number of decimal places that the token amount should display - e.g. 18, means to divide the token amount by 1000000000000000000 to get its user representation."
        },
        "description": {
            "type": "string",
            "description": "Describes the asset to which this token represents"
        },
        "image": {
            "type": "string",
            "description": "A URI pointing to a resource with mime type image/* representing the asset to which this token represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
        },
        "properties": {
            "type": "object",
            "description": "Arbitrary properties. Values may be strings, numbers, object or arrays.",
        },
        "localization": {
            "type": "object",
            "required": ["uri", "default", "locales"],
            "properties": {
                "uri": {
                    "type": "string",
                    "description": "The URI pattern to fetch localized data from. This URI should contain the substring `{locale}` which will be replaced with the appropriate locale value before sending the request."
                },
                "default": {
                    "type": "string",
                    "description": "The locale of the default data within the base JSON"
                },
                "locales": {
                    "type": "array",
                    "description": "The list of locales for which data is available. These locales should conform to those defined in the Unicode Common Locale Data Repository (http://cldr.unicode.org/)."
                }
            }
        }
    }
}

上記を実際に使用すると以下のようになります。

{
  "name": "Advertising Space",
  "description": "Each token represents a unique Ad space in the city.",
  "localization": {
    "uri": "ipfs://QmWS1VAdMD353A6SDk9wNyvkT14kyCiZrNDYAad4w1tKqT/{locale}.json",
    "default": "en",
    "locales": ["en", "es", "fr", "ja"]
  }
}

es.json

{
  "name": "Espacio Publicitario",
  "description": "Cada token representa un espacio publicitario único en la ciudad."
}

fr.json

{
  "name": "Espace Publicitaire",
  "description": "Chaque jeton représente un espace publicitaire unique dans la ville."
}

ja.json

{
  "name": "Espace Publicitaire",
  "description": "それぞれのトークンは、街で唯一の広告スペースを表しています。"
}

ERC1155のコード確認

前章ではメタデータを確認してきました。

ここまで来ればERC1155の理解がだいぶ深まってきたと思います。

この章では実際にERC1155の実装コードを1つずつ確認していきたいと思います。

IERC1155.sol

EIP1155で確認した実装する上で必須の関数をここで定義しています。

コードは以下になります。

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC1155/IERC1155.sol)

pragma solidity ^0.8.0;

import "../../utils/introspection/IERC165.sol";

/**
 * @dev Required interface of an ERC1155 compliant contract, as defined in the
 * https://eips.ethereum.org/EIPS/eip-1155[EIP].
 *
 * _Available since v3.1._
 */
interface IERC1155 is IERC165 {
    /**
     * @dev Emitted when `value` tokens of token type `id` are transferred from `from` to `to` by `operator`.
     */
    event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value);

    /**
     * @dev Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all
     * transfers.
     */
    event TransferBatch(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256[] ids,
        uint256[] values
    );

    /**
     * @dev Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to
     * `approved`.
     */
    event ApprovalForAll(address indexed account, address indexed operator, bool approved);

    /**
     * @dev Emitted when the URI for token type `id` changes to `value`, if it is a non-programmatic URI.
     *
     * If an {URI} event was emitted for `id`, the standard
     * https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[guarantees] that `value` will equal the value
     * returned by {IERC1155MetadataURI-uri}.
     */
    event URI(string value, uint256 indexed id);

    /**
     * @dev Returns the amount of tokens of token type `id` owned by `account`.
     *
     * Requirements:
     *
     * - `account` cannot be the zero address.
     */
    function balanceOf(address account, uint256 id) external view returns (uint256);

    /**
     * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {balanceOf}.
     *
     * Requirements:
     *
     * - `accounts` and `ids` must have the same length.
     */
    function balanceOfBatch(
        address[] calldata accounts,
        uint256[] calldata ids
    ) external view returns (uint256[] memory);

    /**
     * @dev Grants or revokes permission to `operator` to transfer the caller's tokens, according to `approved`,
     *
     * Emits an {ApprovalForAll} event.
     *
     * Requirements:
     *
     * - `operator` cannot be the caller.
     */
    function setApprovalForAll(address operator, bool approved) external;

    /**
     * @dev Returns true if `operator` is approved to transfer ``account``'s tokens.
     *
     * See {setApprovalForAll}.
     */
    function isApprovedForAll(address account, address operator) external view returns (bool);

    /**
     * @dev Transfers `amount` tokens of token type `id` from `from` to `to`.
     *
     * Emits a {TransferSingle} event.
     *
     * Requirements:
     *
     * - `to` cannot be the zero address.
     * - If the caller is not `from`, it must have been approved to spend ``from``'s tokens via {setApprovalForAll}.
     * - `from` must have a balance of tokens of type `id` of at least `amount`.
     * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the
     * acceptance magic value.
     */
    function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external;

    /**
     * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}.
     *
     * Emits a {TransferBatch} event.
     *
     * Requirements:
     *
     * - `ids` and `amounts` must have the same length.
     * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the
     * acceptance magic value.
     */
    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] calldata ids,
        uint256[] calldata amounts,
        bytes calldata data
    ) external;
}

IERC1155.solで定義されている必須関数は以下になります。

関数

  • balanceOf
  • balanceOfBatch
  • setApprovalForAll
  • isApprovedForAll
  • safeTransferFrom
  • safeBatchTransferFrom

これらの関数はEIP1155の章で確認したので、ここでの説明は省きます。

Interface

コードを確認すると、interfaceとなっていますがこれは何でしょうか?

interfaceとは、コントラクトに似ていますが実行することはできません。

interfaceには関数名やひきすうなどのみ定義されていて中身は一切定義されていません。

そのため、interfaceを継承したコントラクト内で関数を再度定義して中身を記述する必要があります。

interfaceいらなくね?

こう思う方もいると思います。

interfaceを使用するメリットは、実装で必要な関数を確認できることにあります。

最初で述べたように、interfaceには実装する上で必要な関数が定義されているので、開発者やコントラクトを実行するユーザーからどんな機能があるのかを簡単に確認できます。

有名なプロジェクトでもinterfaceはよく使用されているので、この機会に理解しておくと後々役に立ちます。

公式ドキュメントは以下になります。

IERC1155Receiver.sol

IERC1155Receiverで確認した実装する上で必須の関数をここで定義しています。

コードは以下になります。

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC1155/IERC1155Receiver.sol)

pragma solidity ^0.8.0;

import "../../utils/introspection/IERC165.sol";

/**
 * @dev _Available since v3.1._
 */
interface IERC1155Receiver is IERC165 {
    /**
     * @dev Handles the receipt of a single ERC1155 token type. This function is
     * called at the end of a `safeTransferFrom` after the balance has been updated.
     *
     * NOTE: To accept the transfer, this must return
     * `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`
     * (i.e. 0xf23a6e61, or its own function selector).
     *
     * @param operator The address which initiated the transfer (i.e. msg.sender)
     * @param from The address which previously owned the token
     * @param id The ID of the token being transferred
     * @param value The amount of tokens being transferred
     * @param data Additional data with no specified format
     * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed
     */
    function onERC1155Received(
        address operator,
        address from,
        uint256 id,
        uint256 value,
        bytes calldata data
    ) external returns (bytes4);

    /**
     * @dev Handles the receipt of a multiple ERC1155 token types. This function
     * is called at the end of a `safeBatchTransferFrom` after the balances have
     * been updated.
     *
     * NOTE: To accept the transfer(s), this must return
     * `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`
     * (i.e. 0xbc197c81, or its own function selector).
     *
     * @param operator The address which initiated the batch transfer (i.e. msg.sender)
     * @param from The address which previously owned the token
     * @param ids An array containing ids of each token being transferred (order and length must match values array)
     * @param values An array containing amounts of each token being transferred (order and length must match ids array)
     * @param data Additional data with no specified format
     * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed
     */
    function onERC1155BatchReceived(
        address operator,
        address from,
        uint256[] calldata ids,
        uint256[] calldata values,
        bytes calldata data
    ) external returns (bytes4);
}

IERC1155Receiver.solに定義されている関数は以下になります。

関数

  • onERC1155Received
  • onERC1155BatchReceived

この部分もEIP1155の章で説明したので、この章での説明は省きます。

ERC1155.sol

では次に大本命のERC1155.solのコードについてみていきましょう。

コードは以下になります。

コードが長すぎるので記事内には全てのコードを載せません。

定義されている関数は以下になります。

関数

  • supportsInterface(bytes4 interfaceId)
  • uri(uint256)
  • balanceOf(address account, uint256 id)
  • balanceOfBatch(address[] memory accounts, uint256[] memory ids)
  • setApprovalForAll(address operator, bool approved)
  • isApprovedForAll(address account, address operator)
  • safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes memory data)
  • safeBatchTransferFrom(address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data)
  • _safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes memory data)
  • _safeBatchTransferFrom(address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data)
  • _setURI(string memory newuri)
  • _mint(address to, uint256 id, uint256 amount, bytes memory data)
  • _mintBatch(address to, unit256[] memory ids, uint256[] amounts, bytes memory data)
  • _burn(address from, uint256 id, uint256 amount)
  • _burnBatch(address from, uint256[] memory ids, uint256[] memory amounts)
  • _setApprovalForAll(address owner, address operator, bool approved)
  • _beforeTokenTransfer(address operator, address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data)
  • _afterTokenTransfer(address operator, address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data)
  • _doSafeTransferAcceptanceCheck(address operator, address from, address to, uint256 memory id, uint256 memory amount, bytes memory data)
  • _doSafeBatchTransferAcceptanceCheck(address operator, address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data)
  • _asSingletonArray(uint256 element)

長いですね…。

それでは1つずつ確認していきましょう。

*IERC1155で定義されている関数の説明は、EIP1155の章で解説しているため省略しています。

変数

// Mapping from token ID to account balances
mapping(uint256 => mapping(address => uint256)) private _balances;

// Mapping from account to operator approvals
mapping(address => mapping(address => bool)) private _operatorApprovals;

// Used as the URI for all token types by relying on ID substitution, e.g. https://token-cdn-domain/{id}.json
string private _uri;

まずは変数から確認していきましょう。

_balances

特定のアドレスがトークンIDをどれだけ所有するか記録する配列。

_operatorApprovals

指定したアドレスに、特定のアドレスが所有するERC1155トークン全てをtransferする権限があるかないかを記録する配列。

_uri

URIを格納するプライベートな変数。

Constructor

constructor(string memory uri_) {
    _setURI(uri_);
}

uri_で指定した文字列をURIにセットする関数。

constructorは、コントラクトがデプロイされた際に一度だけ実行されます。

setURI
function _setURI(string memory newuri) internal virtual {
    _uri = newuri;
}

newuriに指定された文字列を_uri変数に格納する関数。

supportsInterface

function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
    return
        interfaceId == type(IERC1155).interfaceId ||
        interfaceId == type(IERC1155MetadataURI).interfaceId ||
        super.supportsInterface(interfaceId);
}

コントラクトがinterfaceIdに渡されたinterfaceを実装しているか確認する関数。

IERC1155か、IERC1155MetadataURI、もしくは継承元がinterfaceIdに渡されたinterfaceを実装しているか確認しています。

uri

function uri(uint256) public view virtual override returns (string memory) {
    return _uri;
}

_uri変数を返す関数。

balanceOf

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];
}

idで指定したIDのERC1155トークンをaccountで指定したアドレスがどれくらい所有しているか返す関数。

balanceOfBatch

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;
}

ids配列で指定した各IDのERC1155トークンをaccountsで指定したアドレスの配列内の同じindexのアドレスがどれくらい所有しているか返す関数。

_setApprovalForAll

function _setApprovalForAll(address owner, address operator, bool approved) internal virtual {
    require(owner != operator, "ERC1155: setting approval status for self");
    _operatorApprovals[owner][operator] = approved;
    emit ApprovalForAll(owner, operator, approved);
}

operatorに指定したアドレスに、ownerで指定したアドレスが所有するERC1155トークンのtransfer権限を与える、もしくは取り除く関数。

ApprovalForAllイベントを発行しています。

setApprovalForAll

function setApprovalForAll(address operator, bool approved) public virtual override {
    _setApprovalForAll(_msgSender(), operator, approved);
}

setApprovalForAllを実行したユーザーのアドレスを渡して、_setApprovalForAll関数を実行する関数。

_msgSender
function _msgSender() internal view virtual returns (address) {
    return msg.sender;
}

msg.senderを返す関数。

isApprovedForAll

function isApprovedForAll(address account, address operator) public view virtual override returns (bool) {
    return _operatorApprovals[account][operator];
}

operatorに指定したアドレスが、accountで指定したアドレスが所有するERC1155トークンのtransfer権限があるか確認する関数。

_beforeTokenTransfer

function _beforeTokenTransfer(
    address operator,
    address from,
    address to,
    uint256[] memory ids,
    uint256[] memory amounts,
    bytes memory data
) internal virtual {}

transferを実行する前に呼び出される関数。

何かオプションで処理を追加したい場合は、この関数に処理を追記してください。

_afterTokenTransfer

function _afterTokenTransfer(
    address operator,
    address from,
    address to,
    uint256[] memory ids,
    uint256[] memory amounts,
    bytes memory data
) internal virtual {}

transferを実行する後に呼び出される関数。

何かオプションで処理を追加したい場合は、この関数に処理を追記してください。

_asSingletonArray

function _asSingletonArray(uint256 element) private pure returns (uint256[] memory) {
    uint256[] memory array = new uint256[](1);
    array[0] = element;

    return array;
}

要素が1つの配列を作成し、elementに渡された値を格納する関数。

1種類のERC1155トークンを送る場合に、safeBatchTransferFrom関数と同じ引数で_beforeTokenTransfer関数や_afterTokenTransfer関数を実行できるようにするために使用されます。

_doSafeTransferAcceptanceCheck

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");
        }
    }
}

受け取りてがコントラクトの時、ERC1155を実装しているコントラクトであるか確認する関数。

ERC1155ReceiverコントラクトのonERC1155Received関数を実行して確認しています。

この記事の初めの方でも説明しましたが、ERC1155をサポートしていないコントラクトにERC1155トークンを送ってしまうと2度と誰も触れない状態になってしまいます。

_safeTransferFrom

function _safeTransferFrom(
    address from,
    address to,
    uint256 id,
    uint256 amount,
    bytes memory data
) internal virtual {
    require(to != address(0), "ERC1155: transfer to the zero address");

    address operator = _msgSender();
    uint256[] memory ids = _asSingletonArray(id);
    uint256[] memory amounts = _asSingletonArray(amount);

    _beforeTokenTransfer(operator, from, to, ids, amounts, data);

    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);

    _afterTokenTransfer(operator, from, to, ids, amounts, data);

    _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data);
}

_balances配列の値を更新し、_doSafeTransferAcceptanceCheck関数を実行して、fromで指定したアドレスからtoに指定したアドレスへ、idで指定したIDのERC1155トークンをamount分送る関数。

safeTransferFrom

function safeTransferFrom(
    address from,
    address to,
    uint256 id,
    uint256 amount,
    bytes memory data
) public virtual override {
    require(
        from == _msgSender() || isApprovedForAll(from, _msgSender()),
        "ERC1155: caller is not token owner or approved"
    );
    _safeTransferFrom(from, to, id, amount, data);
}

fromで指定したアドレスがidで指定したIDのERC1155トークンの所有者、もしくはtransfer権限があるか確認し、_safeTransferFrom関数を実行する関数。

_doSafeBatchTransferAcceptanceCheck

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");
        }
    }
}

受け取りてがコントラクトの時、ERC1155を実装しているコントラクトであるか確認する関数。

ERC1155ReceiverコントラクトのonERC1155BatchReceived関数を実行して確認しています。

この記事の初めの方でも説明しましたが、ERC1155をサポートしていないコントラクトにERC1155トークンを送ってしまうと2度と誰も触れない状態になってしまいます。

_safeBatchTransferFrom

function _safeBatchTransferFrom(
    address from,
    address to,
    uint256[] memory ids,
    uint256[] memory amounts,
    bytes memory data
) internal virtual {
    require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");
    require(to != address(0), "ERC1155: transfer to the zero address");

    address operator = _msgSender();

    _beforeTokenTransfer(operator, from, to, ids, amounts, data);

    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);

    _afterTokenTransfer(operator, from, to, ids, amounts, data);

    _doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, amounts, data);
}

ids配列とamounts配列の要素分、_balances配列の値を更新し、_doSafeTransferAcceptanceCheck関数を実行して、fromで指定したアドレスからtoに指定したアドレスへ、idに渡されたIDのERC1155トークンをamountに渡された分送る関数。

safeBatchTransferFrom

function safeBatchTransferFrom(
    address from,
    address to,
    uint256[] memory ids,
    uint256[] memory amounts,
    bytes memory data
) public virtual override {
    require(
        from == _msgSender() || isApprovedForAll(from, _msgSender()),
        "ERC1155: caller is not token owner or approved"
    );
    _safeBatchTransferFrom(from, to, ids, amounts, data);
}

fromで指定したアドレスがidで指定したIDのERC1155トークンの所有者、もしくはtransfer権限があるか確認し、_safeBatchTransferFrom関数を実行する関数。

_mint

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 = _msgSender();
    uint256[] memory ids = _asSingletonArray(id);
    uint256[] memory amounts = _asSingletonArray(amount);

    _beforeTokenTransfer(operator, address(0), to, ids, amounts, data);

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

    _afterTokenTransfer(operator, address(0), to, ids, amounts, data);

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

新たにERC1155トークンを発行する関数。

条件

  • toに0アドレスは指定できない。
  • 送り先のアドレスがコントラクトの場合、送り先のコントラクトではIERC1155ReceiverコントラクトのonERC1155Received関数を実装する必要がある。
require(to != address(0), "ERC1155: mint to the zero address");

送り先のアドレスが0アドレスではないか確認しています。

address operator = _msgSender();
uint256[] memory ids = _asSingletonArray(id);
uint256[] memory amounts = _asSingletonArray(amount);

_mint関数の実行ユーザーアドレスを取得し、idamountを要素が1つの配列に変換して、それぞれidsamountsに格納しています。

_beforeTokenTransfer(operator, address(0), to, ids, amounts, data);

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

_afterTokenTransfer(operator, address(0), to, ids, amounts, data);

_balances配列の値の更新と、_beforeTokenTransfer関数と_afterTokenTransfer関数の実行をしています。

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

_doSafeTransferAcceptanceCheck関数を実行して、toで指定したアドレスにamount分のidで指定したIDのERC1155トークンを送っています。

_mintBatch

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 = _msgSender();

    _beforeTokenTransfer(operator, address(0), to, ids, amounts, data);

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

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

    _afterTokenTransfer(operator, address(0), to, ids, amounts, data);

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

新たに複数のERC1155トークンを発行する関数。

処理は_mint関数と同じで、ids配列とamounts配列分_mint関数と同じ処理を繰り返しています。

条件

  • ids配列とamounts配列の長さは同じでなければならない。
  • 送り先のアドレスがコントラクトの場合、送り先のコントラクトではIERC1155ReceiverコントラクトのonERC1155Received関数を実装する必要がある。

_burn

function _burn(address from, uint256 id, uint256 amount) internal virtual {
    require(from != address(0), "ERC1155: burn from the zero address");

    address operator = _msgSender();
    uint256[] memory ids = _asSingletonArray(id);
    uint256[] memory amounts = _asSingletonArray(amount);

    _beforeTokenTransfer(operator, from, address(0), ids, amounts, "");

    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);

    _afterTokenTransfer(operator, from, address(0), ids, amounts, "");
}

fromで指定したアドレスが所有するERC1155トークンを燃やす関数。

_balances配列の値は更新しているが、実際にERC1155トークンを燃やしていないです。

そのため、_afterTokenTransfer関数に具体的な処理を書くことでERC1155トークンを燃やすことができます。

条件

  • fromに0アドレスを指定できない。
  • fromで指定したアドレスはidで指定したIDのERC1155トークンをamountで指定した量以上所有していないといけない。

_burnBatch

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 = _msgSender();

    _beforeTokenTransfer(operator, from, address(0), ids, amounts, "");

    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);

    _afterTokenTransfer(operator, from, address(0), ids, amounts, "");
}

ids配列とamounts配列の要素分、fromで指定したアドレスが所有するERC1155トークンを燃やす関数。

_balances配列の値は更新しているが、実際にERC1155トークンを燃やしていないです。

そのため、_afterTokenTransfer関数に具体的な処理を書くことでERC1155トークンを燃やすことができます。

条件

  • ids配列とamounts配列の長さは同じでなければならない。

ERC1155の実装

ではERC1155の実装をしていきましょう。

以下がERC1155の実装です。

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

import "@openzeppelin/contracts@4.8.0/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts@4.8.0/access/Ownable.sol";

contract CardeneToken is ERC1155, Ownable {

    constructor() ERC1155("https://cardene.net/erc1115/{id}.json") {
        _mint(msg.sender, 0, 10000, "");
        _mint(msg.sender, 1, 1, "");
        _mint(msg.sender, 2, 10**9, "");
    }
}

え、すくな…

と思った方も少なくないはずです。

ここまで説明してきた関数の数々がパッとみないように見えます。

しかし、7行目のコントラクト定義部分で、ERC1155.solをしっかり継承しています。

そのためここまで説明してきた関数を使用することができます。

最低限の実装はこれでできるので、ぜひ自分でカスタマイズなどしてみてください。

ERC1155の実行

ERC1155の実装を確認したところで、最後に実行をしていきましょう!

今回はRemix.ideというブラウザで使用できるエディタを使用していきます。

以下のOpenzeppelinのサイトから簡単にERC1155を実装できるので、時間ある方はぜひ触ってみてください。

では早速コードを書いていきましょう!

ファイルの作成

以下を参考にファイルを作成してください。

ファイルが作成できたら以下を作成したファイルに貼り付けてください。

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

import "@openzeppelin/contracts@4.8.0/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts@4.8.0/access/Ownable.sol";

contract CardeneToken is ERC1155, Ownable {

    constructor() ERC1155("https://cardene.net/erc1115/{id}.json") {
        _mint(msg.sender, 0, 10000, "");
        _mint(msg.sender, 1, 1, "");
        _mint(msg.sender, 2, 10**9, "");
    }
}

実行

では実行していきます。

手順は以下の記事のコンパイルとデプロイを参考にしてください。

いかがでしょうか?

しっかりデプロイできましたでしょうか?

ERC1155.sol内の関数がちゃんと実行できることも確認できたはずです。

Goerliテストネットにデプロイ

最後にGoerliテストネットにデプロイして、ERC1155トークンを作成してみましょう!

いかがだったでしょうか?

しっかりテストネットにデプロイできているのが確認できましたね。

以下がEtherscanのURLです。

手順を1から確認したい人は以下の記事を参考にしてください。

最後に

今回は「ERC1155」についてまとめてきました。

いかがだったでしょうか?

ポイント

  • ERC1155が何か理解できた
  • ERC1155が何をしているのか理解できた
  • ERC1155の実装方法が理解できた

上記が当てはまっていれば嬉しいです!

もし何か質問などがあれば以下のTwitterなどから連絡ください!

普段はSolidityやブロックチェーン、Web3についての情報発信をしています。

Twiiterでは気になった記事などを共有しているので、ぜひフォローしてくれると嬉しいです!

参考

-Smart Contract, Solidity, ブロックチェーン
-, ,