こんにちは!CryptoGamesというブロックチェーンゲーム企業でエンジニアをしているかるでねです!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
このブログ以外でも情報発信しているので、よければ他の記事も見ていってください。
https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58
今回は「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についてまとめられているページですが、これをすべて説明するとだいぶ長くなってしまうので、必須機能部分のみこの章でまとめていきます。
https://eips.ethereum.org/EIPS/eip-1155
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アドレスが渡されなければならない。
- ERC1155トークンを発行する場合は
_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アドレスが渡されなければならない。
- ERC1155トークンを発行する場合は
_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つ目は00000000000000000000000000000001
と00000000000000000000000000000000
の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
はよく使用されているので、この機会に理解しておくと後々役に立ちます。
公式ドキュメントは以下になります。
https://solidity-jp.readthedocs.io/ja/latest/contracts.html#interfaces
IERC1155Receiver.sol
IERC1155Receiverで確認した実装する上で必須の関数をここで定義しています。
コードは以下になります。
Openzeppelin-IERC1155Receiver.sol
// 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
関数の実行ユーザーアドレスを取得し、id
とamount
を要素が1つの配列に変換して、それぞれids
とamounts
に格納しています。
_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を実装できるので、時間ある方はぜひ触ってみてください。
https://wizard.openzeppelin.com/#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です。
0x2611e9aa0202d4dfcdede0c067dbfe22e12564a0a4bd027e3fcbb419d76728e3
手順を1から確認したい人は以下の記事を参考にしてください。
最後に
今回は「ERC1155」についてまとめてきました。
いかがだったでしょうか?
ポイント
- 「ERC1155が何か理解できた」
- 「ERC1155が何をしているのか理解できた」
- 「ERC1155の実装方法が理解できた」
上記が当てはまっていれば嬉しいです!
もし何か質問などがあれば以下のTwitterなどから連絡ください!
普段はSolidityやブロックチェーン、Web3についての情報発信をしています。
Twiiterでは気になった記事などを共有しているので、ぜひフォローしてくれると嬉しいです!
参考
https://medium.com/axell-corporation/erc1155-multi-tokenの標準規格-661da7e0cfa1
https://ethereum.org/ja/developers/docs/standards/tokens/erc-1155/
https://docs.openzeppelin.com/contracts/3.x/erc1155
https://mirror.xyz/blueplanet42.eth/VLiQfXCPGNRnYCsJc5yzR_qIcV4kYPy705wHH88o5HY
https://eips.ethereum.org/EIPS/eip-1155
https://docs.openzeppelin.com/contracts/3.x/api/token/erc1155