はじめに

初めまして。 『DApps開発入門』という本や色々記事を書いているかるでねです。

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!

今回は『ERC1155』についてまとめていきます。

「ERC1155」を理解する前に「ERC20」と「ERC721」を理解しておくことをおすすめします。 以下の記事で「ERC20」と「ERC721」についてまとめているので、是非読んでみてください!

ERC1155とは

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

ERC1155の特徴

ERC1155は「セミファンジブルトークン」と言われていて、簡単にいうと「特定の tokenId のトークンを複数管理することができるトークン」です。 では特徴を1つずつ確認していきましょう。

ERC721では、1つの tokenId に1つのトークンが紐づきます。 しかし、ERC1155は 1つの tokenId にERC20のように複数のトークンを紐づけることができます。 これにより、1つの tokenId に紐づいたトークンをまとめて送付することができるようになります。

例

例えばゲーム内の剣や盾、鎧などの複数アイテムをマーケットプレイスで販売するとします。 ERC721規格であればアイテムを1つずつ管理する必要があります。 例えば、tokenId 1100が剣、tokenId 101200が盾などのように。 ここでERC1155を使用することで、tokenId=1 は剣、tokenId=2 は盾のようにまとめて管理することができます。

EIP1155

EIPとは

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

EIPとは「Ethereum Improvement Proposals」の略で、「Ethereumの新しい機能やプロセスに関する提案を規定する標準規格」のことです。 EIPの細かい説明は以下に書かれています。

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

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

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

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

EIP1155の必須関数

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

safeTransferFrom()
js
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()
js
function safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external;

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

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

例

js
_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()
js
function balanceOf(address _owner, uint256 _id) external view returns (uint256);

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

条件

  • _owner に0アドレスを指定できない
balanceOfBatch()
js
function balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids) external view returns (uint256[] memory);

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

例

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

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

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

条件

  • _owners 配列と _ids 配列の長さは同じでなければならない
setApprovalForAll()
js
function setApprovalForAll(address _operator, bool _approved) external;

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

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

条件

  • _operator のアドレスが setApprovalForAll() 関数を実行したアドレスと異なる必要がある
isApprovedForAll()
js
function isApprovedForAll(address _owner, address _operator) external view returns (bool);

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

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

EIP1155のイベント

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

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

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

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

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

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

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

URI
js
event URI(string _value, uint256 indexed _id);

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

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

ERC1155TokenReceiver

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

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

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

送り先がコントラクトの場合、ERC1155をサポートしていないと永久に送ったERC1155トークンにアクセスできなくなる。 これを防ぐために、送り先のコントラクトがERC1155をサポートしているかを確認しています。

条件

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

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

条件

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

ERC1155Metadata_URI

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

uri()
js
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例

text
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があるとします。

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

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

text
https://token-domain/1000000000000000000000000000000200000000000000000000000000000001.json

ここまでURIについて説明してきましたが、上記の実装は必須ではありません。 そのため1つの例だと認識してもらえると良いです。

メタデータ

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

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

json
{
    "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ファイルは以下のようになります。

json
{
    "name": "Asset Name",
    "description": "Lorem ipsum...",
    "image": "https://s3.amazonaws.com/your-bucket/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 という値を持たせることができます。

json
{
    "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/)."
                }
            }
        }
    }
}

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

json
{
  "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

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

fr.json

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

ja.json

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

ERC1155のコード確認

前章ではメタデータを確認してきました。 ここまで来ればERC1155の理解がだいぶ深まってきたと思います。 この章では実際にERC1155の実装コードを1つずつ確認していきたいと思います。

IERC1155.sol

EIP1155で確認した実装する上で必須の関数をここで定義しています。 コードは以下になります。

js
// 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 で確認した実装する上で必須の関数をここで定義しています。 コードは以下になります。

js
// 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, uint256[] 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 id, uint256 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の章で解説しているため省略しています。

変数

js
// 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トークン全てを送付する権限があるかないかを記録する配列。

_uri

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

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

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

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

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

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

supportsInterface()
js
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()
js
function uri(uint256) public view virtual override returns (string memory) {
    return _uri;
}

_uri 変数を返す関数。

balanceOf()
js
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()
js
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()
js
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トークンの送付権限を与える、もしくは取り除く関数。

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

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

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

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

msg.sender を返す関数。

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

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

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

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

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

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

送付を実行した後に呼び出される関数。

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

_asSingletonArray()
js
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()
js
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()
js
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()
js
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トークンの所有者、もしくは送付権限があるか確認し、_safeTransferFrom() 関数を実行する関数。

_doSafeBatchTransferAcceptanceCheck()
js
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()
js
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()
js
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トークンの所有者、もしくは送付権限があるか確認し、_safeBatchTransferFrom() 関数を実行する関数。

_mint()
js
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() 関数を実装する必要がある
js
require(to != address(0), "ERC1155: mint to the zero address");

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

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

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

js
_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() 関数の実行をしています。

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

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

_mintBatch()
js
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()
js
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()
js
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の実装です。

js
// 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を実装できるので、時間ある方はぜひ触ってみてください。

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

ファイルの作成

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

js
// 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テストネットにデプロイ

2025年11月現在Goerli Testnetは停止しているので、Sepolia Testnetなどを使用してください。

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

名称未設定のデザイン.gif

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

最後に

今回は『ERC1155』についてまとめてきました。 他でも色々記事を書いているのでぜひよろしければ読んでいってください!

参考