OpenZeppelinのERC1155の取扱説明書
ブロックチェーンゲームでよく使われているERC1155の規格について、OpenZeppelinとともに説明していきます。
ブロックチェーンゲームでよく使われているERC1155に関して紹介していきます。EIP1155の規格に準じているERC1155の中身をのぞいてみて、使い方や注意点などを紹介していきます。(詳細のEIP1155の規格はこちら)
ERC1155とは?
ERC1155は、複数のトークンタイプ(ERC721とERC20)を一つのスマートコントラクトで効率的に管理できる新しいトークン標準です。このマルチトークンスタンダードは、特にゲームやデジタルアセットの分野での使用に適しており、一度のトランザクションで複数のトークンを送信するバッチ送信機能により、トランザクションコストの削減と効率の向上などをしてます。
ERC1155の中身
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC1155/ERC1155.sol)
pragma solidity ^0.8.20;
import {IERC1155} from "./IERC1155.sol";
import {IERC1155Receiver} from "./IERC1155Receiver.sol";
import {IERC1155MetadataURI} from "./extensions/IERC1155MetadataURI.sol";
import {Context} from "../../utils/Context.sol";
import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol";
import {Arrays} from "../../utils/Arrays.sol";
import {IERC1155Errors} from "../../interfaces/draft-IERC6093.sol";
/**
* @dev Implementation of the basic standard multi-token.
* See https://eips.ethereum.org/EIPS/eip-1155
* Originally based on code by Enjin: https://github.com/enjin/erc-1155
*/
abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IERC1155Errors {
using Arrays for uint256[];
using Arrays for address[];
mapping(uint256 id => mapping(address account => uint256)) private _balances;
mapping(address account => mapping(address operator => 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;
/**
* @dev See {_setURI}.
*/
constructor(string memory uri_) {
_setURI(uri_);
}
/**
* @dev See {IERC165-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);
}
/**
* @dev See {IERC1155MetadataURI-uri}.
*
* This implementation returns the same URI for *all* token types. It relies
* on the token type ID substitution mechanism
* https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP].
*
* Clients calling this function must replace the `\{id\}` substring with the
* actual token type ID.
*/
function uri(uint256 /* id */) public view virtual returns (string memory) {
return _uri;
}
/**
* @dev See {IERC1155-balanceOf}.
*/
function balanceOf(address account, uint256 id) public view virtual returns (uint256) {
return _balances[id][account];
}
/**
* @dev See {IERC1155-balanceOfBatch}.
*
* Requirements:
*
* - `accounts` and `ids` must have the same length.
*/
function balanceOfBatch(
address[] memory accounts,
uint256[] memory ids
) public view virtual returns (uint256[] memory) {
if (accounts.length != ids.length) {
revert ERC1155InvalidArrayLength(ids.length, accounts.length);
}
uint256[] memory batchBalances = new uint256[](accounts.length);
for (uint256 i = 0; i < accounts.length; ++i) {
batchBalances[i] = balanceOf(accounts.unsafeMemoryAccess(i), ids.unsafeMemoryAccess(i));
}
return batchBalances;
}
/**
* @dev See {IERC1155-setApprovalForAll}.
*/
function setApprovalForAll(address operator, bool approved) public virtual {
_setApprovalForAll(_msgSender(), operator, approved);
}
/**
* @dev See {IERC1155-isApprovedForAll}.
*/
function isApprovedForAll(address account, address operator) public view virtual returns (bool) {
return _operatorApprovals[account][operator];
}
/**
* @dev See {IERC1155-safeTransferFrom}.
*/
function safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes memory data) public virtual {
address sender = _msgSender();
if (from != sender && !isApprovedForAll(from, sender)) {
revert ERC1155MissingApprovalForAll(sender, from);
}
_safeTransferFrom(from, to, id, value, data);
}
/**
* @dev See {IERC1155-safeBatchTransferFrom}.
*/
function safeBatchTransferFrom(
address from,
address to,
uint256[] memory ids,
uint256[] memory values,
bytes memory data
) public virtual {
address sender = _msgSender();
if (from != sender && !isApprovedForAll(from, sender)) {
revert ERC1155MissingApprovalForAll(sender, from);
}
_safeBatchTransferFrom(from, to, ids, values, data);
}
/**
* @dev Transfers a `value` amount of tokens of type `id` from `from` to `to`. Will mint (or burn) if `from`
* (or `to`) is the zero address.
*
* Emits a {TransferSingle} event if the arrays contain one element, and {TransferBatch} otherwise.
*
* Requirements:
*
* - If `to` refers to a smart contract, it must implement either {IERC1155Receiver-onERC1155Received}
* or {IERC1155Receiver-onERC1155BatchReceived} and return the acceptance magic value.
* - `ids` and `values` must have the same length.
*
* NOTE: The ERC-1155 acceptance check is not performed in this function. See {_updateWithAcceptanceCheck} instead.
*/
function _update(address from, address to, uint256[] memory ids, uint256[] memory values) internal virtual {
if (ids.length != values.length) {
revert ERC1155InvalidArrayLength(ids.length, values.length);
}
address operator = _msgSender();
for (uint256 i = 0; i < ids.length; ++i) {
uint256 id = ids.unsafeMemoryAccess(i);
uint256 value = values.unsafeMemoryAccess(i);
if (from != address(0)) {
uint256 fromBalance = _balances[id][from];
if (fromBalance < value) {
revert ERC1155InsufficientBalance(from, fromBalance, value, id);
}
unchecked {
// Overflow not possible: value <= fromBalance
_balances[id][from] = fromBalance - value;
}
}
if (to != address(0)) {
_balances[id][to] += value;
}
}
if (ids.length == 1) {
uint256 id = ids.unsafeMemoryAccess(0);
uint256 value = values.unsafeMemoryAccess(0);
emit TransferSingle(operator, from, to, id, value);
} else {
emit TransferBatch(operator, from, to, ids, values);
}
}
/**
* @dev Version of {_update} that performs the token acceptance check by calling
* {IERC1155Receiver-onERC1155Received} or {IERC1155Receiver-onERC1155BatchReceived} on the receiver address if it
* contains code (eg. is a smart contract at the moment of execution).
*
* IMPORTANT: Overriding this function is discouraged because it poses a reentrancy risk from the receiver. So any
* update to the contract state after this function would break the check-effect-interaction pattern. Consider
* overriding {_update} instead.
*/
function _updateWithAcceptanceCheck(
address from,
address to,
uint256[] memory ids,
uint256[] memory values,
bytes memory data
) internal virtual {
_update(from, to, ids, values);
if (to != address(0)) {
address operator = _msgSender();
if (ids.length == 1) {
uint256 id = ids.unsafeMemoryAccess(0);
uint256 value = values.unsafeMemoryAccess(0);
_doSafeTransferAcceptanceCheck(operator, from, to, id, value, data);
} else {
_doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, values, data);
}
}
}
/**
* @dev Transfers a `value` tokens of token type `id` from `from` to `to`.
*
* Emits a {TransferSingle} event.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - `from` must have a balance of tokens of type `id` of at least `value` 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 value, bytes memory data) internal {
if (to == address(0)) {
revert ERC1155InvalidReceiver(address(0));
}
if (from == address(0)) {
revert ERC1155InvalidSender(address(0));
}
(uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
_updateWithAcceptanceCheck(from, to, ids, values, data);
}
/**
* @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_safeTransferFrom}.
*
* Emits a {TransferBatch} event.
*
* Requirements:
*
* - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the
* acceptance magic value.
* - `ids` and `values` must have the same length.
*/
function _safeBatchTransferFrom(
address from,
address to,
uint256[] memory ids,
uint256[] memory values,
bytes memory data
) internal {
if (to == address(0)) {
revert ERC1155InvalidReceiver(address(0));
}
if (from == address(0)) {
revert ERC1155InvalidSender(address(0));
}
_updateWithAcceptanceCheck(from, to, ids, values, data);
}
/**
* @dev Sets a new URI for all token types, by relying on the token type ID
* substitution mechanism
* https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP].
*
* By this mechanism, any occurrence of the `\{id\}` substring in either the
* URI or any of the values in the JSON file at said URI will be replaced by
* clients with the token type ID.
*
* For example, the `https://token-cdn-domain/\{id\}.json` URI would be
* interpreted by clients as
* `https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json`
* for token type ID 0x4cce0.
*
* See {uri}.
*
* Because these URIs cannot be meaningfully represented by the {URI} event,
* this function emits no events.
*/
function _setURI(string memory newuri) internal virtual {
_uri = newuri;
}
/**
* @dev Creates a `value` amount of tokens of type `id`, and assigns them to `to`.
*
* Emits a {TransferSingle} event.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the
* acceptance magic value.
*/
function _mint(address to, uint256 id, uint256 value, bytes memory data) internal {
if (to == address(0)) {
revert ERC1155InvalidReceiver(address(0));
}
(uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
_updateWithAcceptanceCheck(address(0), to, ids, values, data);
}
/**
* @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_mint}.
*
* Emits a {TransferBatch} event.
*
* Requirements:
*
* - `ids` and `values` must have the same length.
* - `to` cannot be the zero address.
* - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the
* acceptance magic value.
*/
function _mintBatch(address to, uint256[] memory ids, uint256[] memory values, bytes memory data) internal {
if (to == address(0)) {
revert ERC1155InvalidReceiver(address(0));
}
_updateWithAcceptanceCheck(address(0), to, ids, values, data);
}
/**
* @dev Destroys a `value` amount of tokens of type `id` from `from`
*
* Emits a {TransferSingle} event.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `from` must have at least `value` amount of tokens of type `id`.
*/
function _burn(address from, uint256 id, uint256 value) internal {
if (from == address(0)) {
revert ERC1155InvalidSender(address(0));
}
(uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
_updateWithAcceptanceCheck(from, address(0), ids, values, "");
}
/**
* @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_burn}.
*
* Emits a {TransferBatch} event.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `from` must have at least `value` amount of tokens of type `id`.
* - `ids` and `values` must have the same length.
*/
function _burnBatch(address from, uint256[] memory ids, uint256[] memory values) internal {
if (from == address(0)) {
revert ERC1155InvalidSender(address(0));
}
_updateWithAcceptanceCheck(from, address(0), ids, values, "");
}
/**
* @dev Approve `operator` to operate on all of `owner` tokens
*
* Emits an {ApprovalForAll} event.
*
* Requirements:
*
* - `operator` cannot be the zero address.
*/
function _setApprovalForAll(address owner, address operator, bool approved) internal virtual {
if (operator == address(0)) {
revert ERC1155InvalidOperator(address(0));
}
_operatorApprovals[owner][operator] = approved;
emit ApprovalForAll(owner, operator, approved);
}
/**
* @dev Performs an acceptance check by calling {IERC1155-onERC1155Received} on the `to` address
* if it contains code at the moment of execution.
*/
function _doSafeTransferAcceptanceCheck(
address operator,
address from,
address to,
uint256 id,
uint256 value,
bytes memory data
) private {
if (to.code.length > 0) {
try IERC1155Receiver(to).onERC1155Received(operator, from, id, value, data) returns (bytes4 response) {
if (response != IERC1155Receiver.onERC1155Received.selector) {
// Tokens rejected
revert ERC1155InvalidReceiver(to);
}
} catch (bytes memory reason) {
if (reason.length == 0) {
// non-ERC1155Receiver implementer
revert ERC1155InvalidReceiver(to);
} else {
/// @solidity memory-safe-assembly
assembly {
revert(add(32, reason), mload(reason))
}
}
}
}
}
/**
* @dev Performs a batch acceptance check by calling {IERC1155-onERC1155BatchReceived} on the `to` address
* if it contains code at the moment of execution.
*/
function _doSafeBatchTransferAcceptanceCheck(
address operator,
address from,
address to,
uint256[] memory ids,
uint256[] memory values,
bytes memory data
) private {
if (to.code.length > 0) {
try IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, values, data) returns (
bytes4 response
) {
if (response != IERC1155Receiver.onERC1155BatchReceived.selector) {
// Tokens rejected
revert ERC1155InvalidReceiver(to);
}
} catch (bytes memory reason) {
if (reason.length == 0) {
// non-ERC1155Receiver implementer
revert ERC1155InvalidReceiver(to);
} else {
/// @solidity memory-safe-assembly
assembly {
revert(add(32, reason), mload(reason))
}
}
}
}
}
/**
* @dev Creates an array in memory with only one value for each of the elements provided.
*/
function _asSingletonArrays(
uint256 element1,
uint256 element2
) private pure returns (uint256[] memory array1, uint256[] memory array2) {
/// @solidity memory-safe-assembly
assembly {
// Load the free memory pointer
array1 := mload(0x40)
// Set array length to 1
mstore(array1, 1)
// Store the single element at the next word after the length (where content starts)
mstore(add(array1, 0x20), element1)
// Repeat for next array locating it right after the first array
array2 := add(array1, 0x40)
mstore(array2, 1)
mstore(add(array2, 0x20), element2)
// Update the free memory pointer by pointing after the second array
mstore(0x40, add(array2, 0x40))
}
}
}
主要な関数をピックアップしていきます。(細々とした処理に関しての詳細は割愛します。)
_update
function _update(address from, address to, uint256[] memory ids, uint256[] memory values) internal virtual {
// idsとvaluesの長さが違う場合はリバート
if (ids.length != values.length) {
revert ERC1155InvalidArrayLength(ids.length, values.length);
}
// 実行者のアドレスを取得
address operator = _msgSender();
// idの数だけループ
for (uint256 i = 0; i < ids.length; ++i) {
// idを取得
uint256 id = ids.unsafeMemoryAccess(i);
// valueを取得
uint256 value = values.unsafeMemoryAccess(i);
// formがゼロアドレスでない場合
if (from != address(0)) {
// fromのidの残高を取得
uint256 fromBalance = _balances[id][from];
// fromの残高がvalueより小さい場合はリバート
if (fromBalance < value) {
revert ERC1155InsufficientBalance(from, fromBalance, value, id);
}
unchecked {
// Overflow not possible: value <= fromBalance
// fromのidの残高からvalueを引く
_balances[id][from] = fromBalance - value;
}
}
// toがゼロアドレスでない場合
if (to != address(0)) {
// toのidの残高を取得
_balances[id][to] += value;
}
}
// idsの数が1の場合
if (ids.length == 1) {
// 各値を取得してシングルイベントを発火
uint256 id = ids.unsafeMemoryAccess(0);
uint256 value = values.unsafeMemoryAccess(0);
emit TransferSingle(operator, from, to, id, value);
} else {
// バッチイベントを発火
emit TransferBatch(operator, from, to, ids, values);
}
}
mintやburnやtrasfer関連で使われる内部処理です。ERC20やERC721はtokenを1つずつ処理するのですが、ERC1155に関しては複数のトークンを同時に扱えるbatch処理があるため内部の_updateも複数のトークンの対応ができるようになっています。
_updateWithAcceptanceCheck
function _updateWithAcceptanceCheck(
address from,
address to,
uint256[] memory ids,
uint256[] memory values,
bytes memory data
) internal virtual {
// _updateを実行
_update(from, to, ids, values);
// toがゼロアドレスでない場合はmintかtransfer
if (to != address(0)) {
// 実行者のアドレスを取得
address operator = _msgSender();
// idsの数が1の場合
if (ids.length == 1) {
// idとvalueを取得
uint256 id = ids.unsafeMemoryAccess(0);
uint256 value = values.unsafeMemoryAccess(0);
// ちゃんと受け取れたか確認(単体)
_doSafeTransferAcceptanceCheck(operator, from, to, id, value, data);
} else {
// ちゃんと受け取れたか確認(複数)
_doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, values, data);
}
}
}
この関数では_updateを呼び出すとともに、受け取りチェックをしてmintやtransferの場合にちゃんと送りから受け取りまでをできているかを確認する関数です。
この関数は少し注意があって、注意書きにて
とのことなので、overrideをするのはやめましょう!
_safeTransferFrom&_safeBatchTransferFrom
function _safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes memory data) internal {
// toがゼロアドレスの場合はリバート
if (to == address(0)) {
revert ERC1155InvalidReceiver(address(0));
}
// fromがゼロアドレスの場合はリバート
if (from == address(0)) {
revert ERC1155InvalidSender(address(0));
}
// 1つのidとvalueを配列にする
(uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
// _updateWithAcceptanceCheckを実行
_updateWithAcceptanceCheck(from, to, ids, values, data);
}
internalの関数で_safeTransferFromでは単一のトークンをtaransferする処理を行っています。
function _safeBatchTransferFrom(
address from,
address to,
uint256[] memory ids,
uint256[] memory values,
bytes memory data
) internal {
// toがゼロアドレスの場合はリバート
if (to == address(0)) {
revert ERC1155InvalidReceiver(address(0));
}
// fromがゼロアドレスの場合はリバート
if (from == address(0)) {
revert ERC1155InvalidSender(address(0));
}
// idsとvaluesの長さが違う場合はリバート
_updateWithAcceptanceCheck(from, to, ids, values, data);
}
internalの関数で_safeBatchTransferFromでは複数のトークンをtransferする処理を行っています。
_mint&_mintBatch
function _mint(address to, uint256 id, uint256 value, bytes memory data) internal {
// toがゼロアドレスの場合はリバート
if (to == address(0)) {
revert ERC1155InvalidReceiver(address(0));
}
// idとvalueを配列にする
(uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
// _updateWithAcceptanceCheckを実行してmint
_updateWithAcceptanceCheck(address(0), to, ids, values, data);
}
internal関数の_mintは単一のトークンをミントする際に使われます。
function _mintBatch(address to, uint256[] memory ids, uint256[] memory values, bytes memory data) internal {
// toがゼロアドレスの場合はリバート
if (to == address(0)) {
revert ERC1155InvalidReceiver(address(0));
}
// _updateWithAcceptanceCheckを実行して複数mint
_updateWithAcceptanceCheck(address(0), to, ids, values, data);
}
internal関数の_mintBatchは複数のトークンをミントする際に使われます。
_burn&_burnBatch
function _burn(address from, uint256 id, uint256 value) internal {
// fromがゼロアドレスの場合はリバート
if (from == address(0)) {
revert ERC1155InvalidSender(address(0));
}
// idとvalueを配列にする
(uint256[] memory ids, uint256[] memory values) = _asSingletonArrays(id, value);
// _updateWithAcceptanceCheckを実行してburn
_updateWithAcceptanceCheck(from, address(0), ids, values, "");
}
internal関数の_burnは単一のトークンをburnする際に使われます。
function _burnBatch(address from, uint256[] memory ids, uint256[] memory values) internal {
// fromがゼロアドレスの場合はリバート
if (from == address(0)) {
revert ERC1155InvalidSender(address(0));
}
// _updateWithAcceptanceCheckを実行して複数burn
_updateWithAcceptanceCheck(from, address(0), ids, values, "");
}
internal関数の_burnBatchは複数のトークンをburnする際に使われます。
使い方
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol";
contract GameItems is ERC1155, Ownable, ERC1155Burnable {
uint256 public constant GOLD = 0;
uint256 public constant SILVER = 1;
uint256 public constant THORS_HAMMER = 2;
uint256 public constant SWORD = 3;
uint256 public constant SHIELD = 4;
constructor(
address initialOwner
) ERC1155("https://example.com/{id}.json") Ownable(initialOwner) {
_mint(initialOwner, GOLD, 10 ** 18, "");
_mint(initialOwner, SILVER, 10 ** 27, "");
_mint(initialOwner, THORS_HAMMER, 1, "");
_mint(initialOwner, SWORD, 10, "");
_mint(initialOwner, SHIELD, 10, "");
}
function setURI(string memory newuri) public onlyOwner {
_setURI(newuri);
}
function mint(
address account,
uint256 id,
uint256 amount,
bytes memory data
) public onlyOwner {
_mint(account, id, amount, data);
}
function mintBatch(
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) public onlyOwner {
_mintBatch(to, ids, amounts, data);
}
}
こんな感じで単純にmintやburnがownerのみできるようにしてゲームのトークンを扱うような仕様になってます。
- GOLD(金貨)
- SILVER(銀貨)
- THORS_HAMMER(ソーのハンマー)これは1つしかないスーパレア
- SWORD(剣)
- SHIELD(盾)
こちらの処理を見てもらうと各トークンを初期にいくつmintしてあるかがわかります。最初からアイテム数を固定にしてmintしないようにして、受け渡しなどをしていくことによって、ゲーム内のレアアイテムなどの区別がしやすいため、このような実装になってます。
もちろん仕様に合わせて色々と
まとめ
OpenZeppelinのERC1155は、ブロックチェーンゲームやデジタルアセットの領域で多様なトークンタイプを効率的に管理するためのマルチトークンスタンダードです。この規格を使用することで、開発者は一つのスマートコントラクト内で複数のトークンを操作し、バッチ送信機能によりトランザクションのコストを削減することができます。OpenZeppelinを利用することで、セキュリティと利便性を兼ね備えたトークンの実装をすることができます。