Alert Source Discuss

EIP-: ```markdown

Authors

---
eip: 1155
title: 多代币标准
author: Witek Radomski <witek@enjin.io>, Andrew Cooke <ac0dem0nk3y@gmail.com>, Philippe Castonguay (@phabc) <pc@horizongames.net>, James Therien <james@turing-complete.com>, Eric Binet <eric@enjin.io>, Ronan Sandford (@wighawag) <wighawag@gmail.com>
type: Standards Track
category: ERC
status: Final
created: 2018-06-17
discussions-to: https://github.com/ethereum/EIPs/issues/1155
requires: 165
---

## 简述

用于管理多种代币类型的合约的标准接口。一个已部署的合约可以包括任意的可互换代币、不可互换代币或其他配置(例如,半可互换代币)的组合。

## 摘要

本标准概述了一种智能合约接口,它可以表示任意数量的可互换和不可互换代币类型。现有的标准,如 ERC-20,要求为每种代币类型部署单独的合约。ERC-721 标准的代币 ID 是一个单独的不可互换的索引,并且这些不可互换代币的组被部署为一个单独的合约,其中包含整个集合的设置。相比之下,ERC-1155 多代币标准允许每个代币 ID 代表一种新的可配置代币类型,该类型可以有自己的元数据、供应量和其他属性。

每个函数参数集中包含的 `_id` 参数表示交易中的特定代币或代币类型。

## 动机

像 ERC-20 和 ERC-721 这样的代币标准需要为每种代币类型或集合部署一个单独的合约。这在以太坊区块链上放置了大量冗余字节码,并且由于将每个代币合约分离到其自己的许可地址中,因此限制了某些功能。随着区块链游戏和像 Enjin Coin 这样的平台的兴起,游戏开发者可能会创建数千种代币类型,因此需要一种新型的代币标准来支持它们。然而,ERC-1155 并非专门针对游戏,许多其他应用程序也可以从此灵活性中受益。

通过这种设计,新的功能成为可能,例如一次传输多种代币类型,从而节省交易成本。交易(托管/原子交换)多种代币可以在此标准之上构建,并且它消除了单独“批准”单个代币合约的需要。在一个合约中描述和混合多种可互换或不可互换的代币类型也很容易。

## 规范

本文档中的关键词“必须”,“禁止”,“需要”,“应”,“不应”,“推荐”,“可以”和“可选”应按照 RFC 2119 中的描述进行解释。

**实现 ERC-1155 标准的智能合约必须实现 `ERC1155` 接口中的所有函数。**

**实现 ERC-1155 标准的智能合约必须实现 ERC-165 `supportsInterface` 函数,并且如果通过 `interfaceID` 参数传递 `0xd9b67a26`,则必须返回常量值 `true`。**

```solidity
pragma solidity ^0.5.9;

/**
    @title ERC-1155 多代币标准
    @dev 参见 https://eips.ethereum.org/EIPS/eip-1155
    Note: The ERC-165 identifier for this interface is 0xd9b67a26.
 */
interface ERC1155 /* is ERC165 */ {
    /**
        @dev 当代币被转移时,必须发出 `TransferSingle` 或 `TransferBatch`,包括零值转移以及铸造或销毁(参见标准的“安全转移规则”部分)。
        `_operator` 参数必须是经过批准可以进行转移的帐户/合约的地址(应为 msg.sender)。
        `_from` 参数必须是余额减少的持有者的地址。
        `_to` 参数必须是余额增加的接收者的地址。
        `_id` 参数必须是被转移的代币类型。
        `_value` 参数必须是持有者余额减少的代币数量,并且与接收者余额增加的数量相匹配。
        当铸造/创建代币时,`_from` 参数必须设置为 `0x0`(即零地址)。
        当销毁/销毁代币时,`_to` 参数必须设置为 `0x0`(即零地址)。
    */
    event TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value);

    /**
        @dev 当代币被转移时,必须发出 `TransferSingle` 或 `TransferBatch`,包括零值转移以及铸造或销毁(参见标准的“安全转移规则”部分)。
        `_operator` 参数必须是经过批准可以进行转移的帐户/合约的地址(应为 msg.sender)。
        `_from` 参数必须是余额减少的持有者的地址。
        `_to` 参数必须是余额增加的接收者的地址。
        `_ids` 参数必须是被转移的代币列表。
        `_values` 参数必须是代币数量的列表(与 _ids 中指定的代币列表和顺序相匹配),持有者余额减少的数量并且与接收者余额增加的数量相匹配。
        当铸造/创建代币时,`_from` 参数必须设置为 `0x0`(即零地址)。
        当销毁/销毁代币时,`_to` 参数必须设置为 `0x0`(即零地址)。
    */
    event TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values);

    /**
        @dev 当启用或禁用第二方/运营商地址管理所有者的所有代币的权限时,必须发出 ApprovalForAll 事件(没有事件假定为禁用)。
    */
    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

    /**
        @dev 当代币 ID 的 URI 更新时,必须发出。
        URI 在 RFC 3986 中定义。
        URI 必须指向符合“ERC-1155 元数据 URI JSON 模式”的 JSON 文件。
    */
    event URI(string _value, uint256 indexed _id);

    /**
        @notice 将指定数量的 `_id` 代币从 `_from` 地址转移到 `_to` 地址(带有安全调用)。
        @dev 调用者必须被授权管理从 `_from` 帐户转移出去的代币(参见标准的“授权”部分)。
        如果 `_to` 是零地址,则必须回滚。
        如果持有者代币 `_id` 的余额低于发送的 `_value`,则必须回滚。
        必须在任何其他错误时回滚。
        必须发出 `TransferSingle` 事件以反映余额变化(参见标准的“安全转移规则”部分)。
        在满足上述条件后,此函数必须检查 `_to` 是否为智能合约(例如,代码大小> 0)。如果是,则必须在 `_to` 上调用 `onERC1155Received` 并采取适当的措施(参见标准的“安全转移规则”部分)。
        @param _from    来源地址
        @param _to      目标地址
        @param _id      代币类型的 ID
        @param _value   转移金额
        @param _data    没有指定格式的附加数据,必须在对 `_to` 的 `onERC1155Received` 的调用中保持不变
    */
    function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) external;

    /**
        @notice 将 `_values` 数量的 `_ids` 代币从 `_from` 地址转移到指定的 `_to` 地址(带有安全调用)。
        @dev 调用者必须被授权管理从 `_from` 帐户转移出去的代币(参见标准的“授权”部分)。
        如果 `_to` 是零地址,则必须回滚。
        如果 `_ids` 的长度与 `_values` 的长度不相同,则必须回滚。
        如果 `_ids` 中代币的持有者的任何余额低于发送给接收者的 `_values` 中的相应金额,则必须回滚。
        必须在任何其他错误时回滚。
        必须发出 `TransferSingle` 或 `TransferBatch` 事件,以便反映所有余额变化(参见标准的“安全转移规则”部分)。
        余额变化和事件必须遵循数组的顺序(在 _ids[1]/_values[1] 之前 _ids[0]/_values[0] 等)。
        在满足批处理中转移的上述条件后,此函数必须检查 `_to` 是否为智能合约(例如,代码大小> 0)。如果是,则必须在 `_to` 上调用相关的 `ERC1155TokenReceiver` 钩子并采取适当的措施(参见标准的“安全转移规则”部分)。
        @param _from    来源地址
        @param _to      目标地址
        @param _ids     每种代币类型的 ID(顺序和长度必须与 _values 数组匹配)
        @param _values  每种代币类型的转移金额(顺序和长度必须与 _ids 数组匹配)
        @param _data    没有指定格式的附加数据,必须在对 `_to` 的 `ERC1155TokenReceiver` 钩子的调用中保持不变
    */
    function safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external;

    /**
        @notice 获取帐户的代币余额。
        @param _owner  代币持有者的地址
        @param _id     代币的 ID
        @return        所请求的代币类型的 _owner 的余额
     */
    function balanceOf(address _owner, uint256 _id) external view returns (uint256);

    /**
        @notice 获取多个帐户/代币对的余额
        @param _owners 代币持有者的地址
        @param _ids    代币的 ID
        @return        所请求的代币类型的 _owner 的余额(即每个 (owner, id) 对的余额)
     */
    function balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids) external view returns (uint256[] memory);

    /**
        @notice 启用或禁用第三方(“运营商”)管理调用者的所有代币的权限。
        @dev 成功时必须发出 ApprovalForAll 事件。
        @param _operator  要添加到授权运营商集合的地址
        @param _approved  如果运营商被批准,则为 True;如果撤消批准,则为 false
    */
    function setApprovalForAll(address _operator, bool _approved) external;

    /**
        @notice 查询给定所有者的运营商的批准状态。
        @param _owner     代币的所有者
        @param _operator  授权运营商的地址
        @return           如果运营商被批准,则为 True;如果未被批准,则为 false
    */
    function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}

ERC-1155 代币接收者

智能合约必须实现 ERC1155TokenReceiver 接口中的所有函数才能接受转移。有关更多详细信息,请参见“安全转移规则”。

智能合约必须实现 ERC-165 supportsInterface 函数,并表示支持 ERC1155TokenReceiver 接口以接受转移。有关更多详细信息,请参见“ERC1155TokenReceiver ERC-165 规则”。

pragma solidity ^0.5.9;

/**
    Note: The ERC-165 identifier for this interface is 0x4e2312e0.
*/
interface ERC1155TokenReceiver {
    /**
        @notice 处理单个 ERC1155 代币类型的接收。
        @dev 符合 ERC1155 的智能合约必须在余额更新后,在 `safeTransferFrom` 结束时在代币接收者合约上调用此函数。
        如果接受转移,则此函数必须返回 `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`(即 0xf23a6e61)。
        如果拒绝转移,则此函数必须回滚。
        返回任何其他值而不是规定的 keccak256 生成的值必须导致交易被调用者回滚。
        @param _operator  启动转移的地址(即 msg.sender)
        @param _from      先前拥有代币的地址
        @param _id        要转移的代币的 ID
        @param _value     要转移的代币数量
        @param _data      没有指定格式的附加数据
        @return           `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`
    */
    function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data) external returns(bytes4);

    /**
        @notice 处理多个 ERC1155 代币类型的接收。
        @dev 符合 ERC1155 的智能合约必须在余额更新后,在 `safeBatchTransferFrom` 结束时在代币接收者合约上调用此函数。
        如果接受转移,则此函数必须返回 `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`(即 0xbc197c81)。
        如果拒绝转移,则此函数必须回滚。
        返回任何其他值而不是规定的 keccak256 生成的值必须导致交易被调用者回滚。
        @param _operator  启动批量转移的地址(即 msg.sender)
        @param _from      先前拥有代币的地址
        @param _ids       包含要转移的每个代币的 ID 的数组(顺序和长度必须与 _values 数组匹配)
        @param _values    包含要转移的每个代币的数量的数组(顺序和长度必须与 _ids 数组匹配)
        @param _data      没有指定格式的附加数据
        @return           `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`
    */
    function onERC1155BatchReceived(address _operator, address _from, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external returns(bytes4);       
}

安全转移规则

为了更明确地说明标准 safeTransferFromsafeBatchTransferFrom 函数必须如何相对于 ERC1155TokenReceiver 钩子函数进行操作,下面列出了一系列场景和规则。

场景

场景#1 : 接收者不是合约。

  • onERC1155ReceivedonERC1155BatchReceived 不能在 EOA(外部拥有的帐户)上调用。

场景#2 : 交易不是代币的铸造/转移。

  • onERC1155ReceivedonERC1155BatchReceived 不能在铸造或转移过程之外调用。

场景#3 : 接收者未实现必要的 ERC1155TokenReceiver 接口函数。

  • 转移必须回滚,但有一个例外,如下所示。
    • 如果要发送的代币是另一个标准的混合实现的一部分,则现在可以遵循该特定标准关于发送到合约的规则。请参见“向后兼容性”部分。

场景#4 : 接收者实现了必要的 ERC1155TokenReceiver 接口函数,但返回了一个未知值。

  • 转移必须回滚。

场景#5 : 接收者实现了必要的 ERC1155TokenReceiver 接口函数,但引发了一个错误。

  • 转移必须回滚。

场景#6 : 接收者实现了 ERC1155TokenReceiver 接口,并且是唯一一个余额变化的接收者(例如,调用了 safeTransferFrom)。

  • ERC1155TokenReceiver 钩子在接收者合约上调用之前,必须已更新转移的余额。
  • ERC1155TokenReceiver 钩子在接收者合约上调用之前,必须已发出转移事件以反映余额变化。
  • 必须在接收者合约上调用 onERC1155ReceivedonERC1155BatchReceived 之一。
  • 应该在接收者合约上调用 onERC1155Received 钩子,并遵循其规则。
    • 请参见“onERC1155Received 规则”以获取必须遵循的更多规则。
  • 可以在接收者合约上调用 onERC1155BatchReceived 钩子,并遵循其规则。
    • 请参见“onERC1155BatchReceived 规则”以获取必须遵循的更多规则。

场景#7 : 接收者实现了 ERC1155TokenReceiver 接口,并且是多个余额变化的接收者(例如,调用了 safeBatchTransferFrom)。

  • ERC1155TokenReceiver 钩子在接收者合约上调用之前,必须更新对 ERC1155TokenReceiver 钩子的调用中引用的所有余额转移。
  • ERC1155TokenReceiver 钩子在接收者合约上调用之前,必须已发出所有转移事件以反映当前的余额变化。
  • 必须根据需要多次在接收者上调用 onERC1155ReceivedonERC1155BatchReceived,以便在场景中为接收者考虑每个余额变化。
    • 必须检查每个钩子调用的返回魔术值,并按照“onERC1155Received 规则”和“onERC1155BatchReceived 规则”进行处理。
  • 应该在接收者合约上调用 onERC1155BatchReceived 钩子,并遵循其规则。
    • 请参见“onERC1155BatchReceived 规则”以获取必须遵循的更多规则。
  • 可以在接收者合约上调用 onERC1155Received 钩子,并遵循其规则。
    • 请参见“onERC1155Received 规则”以获取必须遵循的更多规则。

场景#8 : 您是实现 ERC1155TokenReceiver 接口的合约的创建者,并且您在一个或两个 onERC1155ReceivedonERC1155BatchReceived 中将代币转发到另一个地址。

  • 转发应被视为接受,然后在新上下文中启动新的 safeTransferFromsafeBatchTransferFrom
    • 调用接收者钩子所规定的 keccak256 接受值魔术必须在转发成功后返回。
  • _data 参数可以重新用于新上下文。
  • 如果转发失败,则交易可能会回滚。
    • 在这种情况下,如果合约逻辑希望保留代币的所有权,则可能会这样做。

场景#9 : 您正在通过非标准 API 调用(即特定于实现的 API,而不是 safeTransferFromsafeBatchTransferFrom)转移代币。

  • 在此场景中,所有余额更新和事件输出规则与调用了标准转移函数的情况相同。
    • 即外部查看器仍然必须能够通过标准函数查询余额,并且它必须与仅通过 TransferSingleTransferBatch 事件确定的余额相同。
  • 如果接收者是合约,则仍然需要在其上调用 ERC1155TokenReceiver 钩子,并且必须按照调用了标准转移函数的情况一样遵守返回值。
    • 但是,虽然如果接收合约未实现 ERC1155TokenReceiver 接口,则 safeTransferFromsafeBatchTransferFrom 函数必须回滚,但非标准函数可能会继续进行转移。
    • 请参见“特定于实现的转移 API 规则”。

规则

safeTransferFrom 规则:

  • 调用者必须被批准管理从 _from 帐户转移出去的代币(请参见“批准”部分)。
  • 如果 _to 是零地址,则必须回滚。
  • 如果持有者代币 _id 的余额低于发送给接收者的 _value,则必须回滚。
  • 必须在任何其他错误时回滚。
  • 必须发出 TransferSingle 事件以反映余额变化(请参见“TransferSingle 和 TransferBatch 事件规则”部分)。
  • 在满足上述条件后,此函数必须检查 _to 是否为智能合约(例如,代码大小> 0)。如果是,则必须在 _to 上调用 onERC1155Received 并采取适当的措施(请参见“onERC1155Received 规则”部分)。
    • 发送者为转移提供的 _data 参数必须通过其 _data 参数将其内容原封不动地传递给 onERC1155Received 钩子函数。

safeBatchTransferFrom 规则:

  • 调用者必须被批准管理从 _from 帐户转移出去的所有代币(请参见“批准”部分)。
  • 如果 _to 是零地址,则必须回滚。
  • 如果 _ids 的长度与 _values 的长度不相同,则必须回滚。
  • 如果 _ids 中代币的持有者的任何余额低于发送给接收者的 _values 中的相应数量,则必须回滚。
  • 必须在任何其他错误时回滚。
  • 必须发出 TransferSingleTransferBatch 事件,以便反映所有余额变化(请参见“TransferSingle 和 TransferBatch 事件规则”部分)。
  • 余额变化和事件必须按照提交的数组顺序发生(在 _ids[1]/_values[1] 之前 _ids[0]/_values[0] 等)。
  • 在满足上述条件后,此函数必须检查 _to 是否为智能合约(例如,代码大小> 0)。如果是,则必须在 _to 上调用 onERC1155ReceivedonERC1155BatchReceived 并采取适当的措施(请参见“onERC1155Received 和 onERC1155BatchReceived 规则”部分)。
    • 发送者为转移提供的 _data 参数必须通过其 _data 参数将其内容原封不动地传递给 ERC1155TokenReceiver 钩子函数。

TransferSingle 和 TransferBatch 事件规则:

  • TransferSingle 应该用于指示在 _from_to 对之间发生了单个余额转移。
    • 可能会发出多次以指示在交易中发生了多个余额变化,但请注意,TransferBatch 旨在减少气体消耗。
    • _operator 参数必须是被批准进行转移的帐户/合约的地址(应为 msg.sender)。
    • _from 参数必须是余额减少的持有者的地址。
    • _to 参数必须是余额增加的接收者的地址。
    • _id 参数必须是被转移的代币类型。
    • _value 参数必须是持有者余额减少的代币数量,并且与接收者余额增加的数量相匹配。
    • 当铸造/创建代币时,_from 参数必须设置为 0x0(即零地址)。请参见“铸造/创建和销毁/销毁规则”。
    • 当销毁/销毁代币时,_to 参数必须设置为 0x0(即零地址)。请参见“铸造/创建和销毁/销毁规则”。
  • TransferBatch 应该用于指示在 _from_to 对之间发生了多个余额转移。
    • 可以在列表中发出单个元素,以指示在交易中发生了单个余额变化,但请注意,TransferSingle 旨在减少气体消耗。
    • _operator 参数必须是被批准进行转移的帐户/合约的地址(应为 msg.sender)。
    • 对于 _ids_values 中的每个条目对,_from 参数必须是余额减少的持有者的地址。
    • 对于 _ids_values 中的每个条目对,_to 参数必须是余额增加的接收者的地址。
    • _ids 数组参数必须包含要转移的代币的 ID。
    • _values 数组参数必须包含要转移的每个对应条目的代币数量。
    • _ids_values 必须具有相同的长度。
    • 当铸造/创建代币时,_from 参数必须设置为 0x0(即零地址)。请参见“铸造/创建和销毁/销毁规则”。
    • 当销毁/销毁代币时,_to 参数必须设置为 0x0(即零地址)。请参见“铸造/创建和销毁/销毁规则”。
  • 通过 TransferSingleTransferBatch 事件观察到的从地址 0x0 转移的总价值减去转移到 0x0 的总价值可以被客户端和交易所用来确定给定代币 ID 的“流通供应量”。
  • 为了广播不存在初始余额的代币 ID,合约应发出从 0x00x0TransferSingle 事件,其中代币创建者为 _operator_value 为 0。
  • 在对 onERC1155ReceivedonERC1155BatchReceived 的任何调用之前,必须发出所有 TransferSingleTransferBatch 事件,以反映已发生的所有余额变化。
    • 为了确保在有效重新进入的情况下事件顺序正确(例如,如果接收合约在收到时转发代币),在调用外部合约之前,状态余额和事件余额必须匹配。

onERC1155Received 规则:

  • _operator 参数必须是被批准进行转移的帐户/合约的地址(应为 msg.sender)。
  • _from 参数必须是余额减少的持有者的地址。
    • 对于铸造,_from 必须为 0x0。
  • _id 参数必须是被转移的代币类型。
  • _value 参数必须是持有者余额减少的代币数量,并且与接收者余额增加的数量相匹配。
  • _data 参数必须包含发送者为转移提供的信息,并且其内容保持不变。
    • 即它必须传递通过 safeTransferFromsafeBatchTransferFrom 调用为此转移发送的未更改的 _data 参数。
  • 接收者合约可以通过返回接受魔术值 bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")) 来接受其余额的增加
    • 如果返回值为 bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")),则必须完成转移;如果未满足任何其他成功条件,则必须回滚。
  • 接收者合约可以通过调用 revert 来拒绝其余额的增加。
    • 如果接收者合约抛出/回滚,则必须回滚交易。
  • 如果返回值不是 bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")),则必须回滚交易。
  • onERC1155Received(和/或 onERC1155BatchReceived)可以在单个交易中多次调用,并且必须满足以下要求:
    • 所有回调都代表互斥的余额变化。
    • 所有对 onERC1155ReceivedonERC1155BatchReceived 的调用集合描述了交易期间发生的所有余额变化,并按照提交的顺序排列。
  • 如果转移操作是将代币转移到自身,则合约可以跳过调用 onERC1155Received 钩子函数。

onERC1155BatchReceived 规则:

  • _operator 参数必须是被批准进行转移的帐户/合约的地址(应为 msg.sender)。
  • _from 参数必须是余额减少的持有者的地址。
    • 对于铸造,_from 必须为 0x0。
  • _ids 参数必须是要转移的代币列表。
  • _values 参数必须是代币数量的列表(与 _ids 中指定的代币列表和顺序相匹配),持有者余额减少的数量,并且与接收者余额增加的数量相匹配。
  • _data 参数必须包含发送者为转移提供的信息,并且其内容保持不变。
    • 即它必须传递通过 safeBatchTransferFrom 调用为此转移发送的未更改的 _data 参数。
  • 接收者合约可以通过返回接受魔术值 bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)")) 来接受其余额的增加
    • 如果返回值为 bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)")),则必须完成转移;如果未满足任何其他成功条件,则必须回滚。
  • 接收者合约可以通过调用 revert 来拒绝其余额的增加。
    • 如果接收者合约抛出/回滚,则必须回滚交易。
  • 如果返回值不是 bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)")),则必须回滚交易。
  • onERC1155BatchReceived(和/或 onERC1155Received)可以在单个交易中多次调用,并且必须满足以下要求:
    • 所有回调都代表互斥的余额变化。
    • 所有对 onERC1155ReceivedonERC1155BatchReceived 的调用集合描述了交易期间发生的所有余额变化,并按照提交的顺序排列。
  • 如果转移操作是将代币转移到自身,则合约可以跳过调用 onERC1155BatchReceived 钩子函数。

ERC1155TokenReceiver ERC-165 规则:

  • ERC-165 supportsInterface 函数的实现应如下所示:
      function supportsInterface(bytes4 interfaceID) external view returns (bool) {
          return  interfaceID == 0x01ffc9a7 ||    // ERC-165 支持(即 `bytes4(keccak256('supportsInterface(bytes4)'))`)。
                  interfaceID == 0x4e2312e0;      // ERC-1155 `ERC1155TokenReceiver` 支持(即 `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")) ^ bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`)。
      }
    
  • 该实现可能与上述不同,但:
    • 如果通过 interfaceID 参数传递 0x01ffc9a7,则必须返回常量值 true。这表示 ERC-165 支持。
    • 如果通过 interfaceID 参数传递 0x4e2312e0,则必须返回常量值 true。这表示 ERC-1155 ERC1155TokenReceiver 支持。
    • 消耗的气体量不得超过 10,000。
      • 这使其低于 ERC-165 的 30,000 气体要求,减少了气体储备需求,并最大程度地减少了调用期间气体耗尽的可能副作用。

特定于实现的转移 API 规则:

  • 如果使用特定于实现的 API 函数将 ERC-1155 代币转移到合约,如果接收者实现了 ERC1155TokenReceiver 接口,则仍必须遵循 safeTransferFromsafeBatchTransferFrom(如适用)规则。如果未实现,则非标准实现应回滚,但可能会继续。
  • 示例:
    1. 经批准的用户调用诸如 function myTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values); 之类的函数。
    2. myTransferFrom 更新所有 _ids_values_from_to 地址的余额。
    3. myTransferFrom 发出 TransferBatch,其中包含从地址 _from 转移到地址 _to 的详细信息。
    4. myTransferFrom 检查 _to 是否为合约地址,并确定它是(否则,可以认为转移成功)。
    5. myTransferFrom_to 上调用 onERC1155BatchReceived,它会回滚或返回未知值(如果它返回 bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)")),则可以认为转移成功)。
    6. 此时,myTransferFrom 应立即回滚交易,因为代币的接收未被 onERC1155BatchReceived 函数明确接受。
    7. 但是,如果 myTransferFrom 希望继续,则它必须在 _to 上调用 supportsInterface(0x4e2312e0),并且如果它返回常量值 true,则必须回滚交易,因为现在已知它是有效的接收者,并且先前的接受步骤失败了。
      • 注意:如果您想更早地收集并处理该信息,例如在混合标准方案中,您可以在之前的步骤中调用 supportsInterface(0x4e2312e0)
    8. 如果上述对 _to 上的 supportsInterface(0x4e2312e0) 的调用回滚或返回不是常量值 true 的值,则 myTransferFrom 函数可以将此转移视为成功。
      • 注意: 如果发送到的地址不希望接收 ERC-1155 代币,这可能会导致无法恢复的代币。
  • 上面的示例并非详尽无遗,但说明了要点(并表明大多数与 safeTransferFromsafeBatchTransferFrom 共享):
    • 更新的余额必须发出等效的转移事件。
    • 必须检查接收者地址是否为合约,如果是,则必须在其上调用相关的 ERC1155TokenReceiver 钩子函数。
    • 在调用 ERC1155TokenReceiver 钩子之前,必须更新(并发出)对 ERC1155TokenReceiver 钩子的调用中引用的余额(和关联的事件)。
    • 如果实现了 ERC1155TokenReceiver 钩子函数,则必须遵守对其的返回值。
    • 只有非标准转移函数才允许将代币发送到不实现必需 ERC1155TokenReceiver 钩子函数的接收者合约铸造/创建和销毁/删除规则:
  • 铸造/创建操作本质上是一种特殊的转移,必须遵循以下规则:
    • 要广播一个没有初始余额的 token ID 的存在,合约应该从 0x00x0 发出 TransferSingle 事件,其中 token 创建者为 _operator_value 为 0。
    • “TransferSingle 和 TransferBatch 事件规则”必须根据铸造(即单笔或批量)的情况进行遵循,但是 _from 参数必须设置为 0x0(即零地址),以将转移标记为对合约观察者的铸造。
      • 注意: 这包括在合约中获得初始余额的 token。合约的余额也必须仅通过事件来确定,这意味着初始合约余额(例如,在构造中)也必须发出事件来反映这些余额。
  • 销毁/删除操作本质上是一种特殊的转移,必须遵循以下规则:
    • “TransferSingle 和 TransferBatch 事件规则”必须根据销毁(即单笔或批量)的情况进行遵循,但是 _to 参数必须设置为 0x0(即零地址),以将转移标记为对合约观察者的销毁。
    • 当销毁/删除时,您不必实际转移到 0x0(这是实现特定的),只有事件中的 _to 参数必须如上设置为 0x0
  • 客户端和交易所可以通过 TransferSingleTransferBatch 事件观察到的从地址 0x0 转移的总价值减去转移到 0x0 的总价值,来确定给定 token ID 的“流通供应量”。
  • 如上所述,铸造/创建和销毁/删除操作是特殊的转移,因此很可能通过自定义转移函数而不是 safeTransferFromsafeBatchTransferFrom 来完成。如果是这样,“特定于实现的转移 API 规则”部分将适用。
    • 即使在非安全 API 和/或混合标准的情况下,在铸造/创建或销毁/删除时,也必须遵守上述事件规则。
  • 如果铸造操作是将 token 转移到自身,则合约可以跳过调用 ERC1155TokenReceiver 钩子函数。在所有其他情况下,ERC1155TokenReceiver 规则必须根据实现(即安全、自定义和/或混合)进行遵循。
用于各种魔术值的 keccak256 生成的常量的 Solidity 示例(这些可以被实现使用):
bytes4 constant public ERC1155_ERC165 = 0xd9b67a26; // 用于主 token 标准的 ERC-165 标识符。
bytes4 constant public ERC1155_ERC165_TOKENRECEIVER = 0x4e2312e0; // 用于 `ERC1155TokenReceiver` 支持的 ERC-165 标识符(即 `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")) ^ bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`)。
bytes4 constant public ERC1155_ACCEPTED = 0xf23a6e61; // 如果合约接受接收,则从 `onERC1155Received` 调用返回的值(即 `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`)。
bytes4 constant public ERC1155_BATCH_ACCEPTED = 0xbc197c81; // 如果合约接受接收,则从 `onERC1155BatchReceived` 调用返回的值(即 `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`)。

元数据

URI 值允许客户端进行 ID 替换。如果字符串 {id} 存在于任何 URI 中,客户端必须将其替换为十六进制格式的实际 token ID。这允许大量 token 通过定义一次 URI 来使用相同的链上字符串,以用于大量 token。

  • 替换的十六进制 ID 的字符串格式必须是小写的字母数字:[0-9a-f],不带 0x 前缀。
  • 如果需要,替换的十六进制 ID 的字符串格式必须前导零填充到 64 个十六进制字符的长度。

此类 URI 的示例:https://token-cdn-domain/{id}.json 将被替换为 https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json,如果客户端引用 token ID 314592/0x4CCE0。

元数据扩展

可选的 ERC1155Metadata_URI 扩展可以使用 ERC-165 标准接口检测 来识别。

如果包含可选的 ERC1155Metadata_URI 扩展:

  • 如果通过 interfaceID 参数传递 0x0e89341c,则 ERC-165 supportsInterface 函数必须返回常量值 true
  • 如果 URI 的_更改_可以用事件表达(即它不是动态/编程的),则必须发出 URI 事件。
    • 实现可以在铸造操作期间发出 URI 事件,但这不是强制性的。如果未发出,观察者可以在铸造时从 uri 函数获取元数据 uri。
  • 如果没有发出事件,则应使用 uri 函数来检索值。
  • 如果已发出,则对于 _iduri 函数必须返回与最新事件相同的值。
  • uri 函数不能用于检查 token 的存在,因为即使 token 不存在,实现也可能返回有效的字符串。
pragma solidity ^0.5.9;

/**
    注意:此接口的 ERC-165 标识符为 0x0e89341c。
*/
interface ERC1155Metadata_URI {
    /**
        @notice 给定 token 的不同统一资源标识符(URI)。
        @dev URI 在 RFC 3986 中定义。
        URI 必须指向符合“ERC-1155 元数据 URI JSON 模式”的 JSON 文件。
        @return URI 字符串
    */
    function uri(uint256 _id) external view returns (string memory);
}

ERC-1155 元数据 URI JSON 模式

此 JSON 模式大致基于“ERC721 元数据 JSON 模式”,但包括可选的格式,允许客户端进行 ID 替换。如果字符串 {id} 存在于任何 JSON 值中,则所有遵循此标准的客户端软件必须将其替换为实际的 token ID。

  • 替换的十六进制 ID 的字符串格式必须是小写的字母数字:[0-9a-f],不带 0x 前缀。
  • 如果需要,替换的十六进制 ID 的字符串格式必须前导零填充到 64 个十六进制字符的长度。
{
    "title": "Token Metadata",
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "标识此 token 代表的资产"
        },
        "decimals": {
            "type": "integer",
            "description": "token 数量应显示的十进制位数 - 例如,18 表示将 token 数量除以 1000000000000000000 以获得其用户表示。"
        },
        "description": {
            "type": "string",
            "description": "描述此 token 代表的资产"
        },
        "image": {
            "type": "string",
            "description": "指向具有 mime 类型 image/* 的资源的 URI,表示此 token 代表的资产。考虑使任何图像的宽度在 320 到 1080 像素之间,宽高比在 1.91:1 和 4:5 之间(含)。"
        },
        "properties": {
            "type": "object",
            "description": "任意属性。值可以是字符串、数字、对象或数组。"
        }
    }
}

以下是 ERC-1155 元数据 JSON 文件的示例。属性数组提出了 token 特定显示属性和元数据的一些建议格式。

{
	"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"
		}
	}
}
本地化

元数据本地化应标准化,以提高所有语言的演示文稿统一性。因此,提出了一种简单的覆盖方法来实现本地化。如果元数据 JSON 文件包含 localization 属性,则可以使用其内容为需要的字段提供本地化值。localization 属性应为一个包含三个属性的子对象:uridefaultlocales。如果字符串 {locale} 存在于任何 URI 中,则所有客户端软件必须将其替换为选择的语言环境。

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/)."
                }
            }
        }
    }
}
本地化样本

基本 URI:

{
  "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"]
  }
}

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."
}

授权

函数 setApprovalForAll 允许操作员代表授权人管理其整个 token 集合。为了允许授权 token ID 的子集,建议使用诸如 ERC-1761 范围授权接口 之类的接口。 与之对应的 isApprovedForAll 提供了对 setApprovalForAll 设置的任何状态的自省。

应该假定所有者始终能够对其自己的 token 进行操作,而不管授权状态如何,因此不应该必须调用 setApprovalForAll 来批准他们自己作为操作员,然后才能对其进行操作。

理论依据

元数据选择

未包括 symbol 函数(在 ERC-20 和 ERC-721 标准中找到),因为我们不认为这是识别通用虚拟物品/资产的全局有用的数据,并且也容易发生冲突。简写符号用于股票代码和货币交易,但在该空间之外它们不太有用。

从标准中删除了 name 函数(用于人类可读的资产名称,在链上),以允许元数据 JSON 成为明确的资产名称,并减少数据重复。这也允许名称本地化,否则如果每个语言字符串都存储在链上,那将是过高的成本,更不用说膨胀标准接口了。虽然此决定可能会给实施者增加一小部分负担来托管包含元数据的 JSON 文件,但我们认为任何 ERC-1155 的严肃实施都将已经使用 JSON 元数据。

升级

在余额更改时发出 TransferSingleTransferBatch 的要求意味着,重新部署到新合约地址的 ERC-1155 的有效实施必须从新合约地址发出事件,以复制已弃用的合约最终状态。仅发出最少数量的事件来反映最终余额并省略导致该状态的所有交易是有效的。事件发出要求是为了确保合约的当前状态始终只能通过事件来追溯。为了减轻在更改合约地址时发出事件的需要,请考虑使用代理模式,例如 EIP-2535 中所述。这也将具有为用户提供稳定合约地址的额外好处。

设计决策:支持非批量处理

该标准支持 safeTransferFromonERC1155Received 函数,因为对于单 token 类型转移,它们明显更便宜,这可以说是一种常见的用例。

设计决策:仅安全转移

该标准仅支持安全风格的转移,使接收者合约可以依赖于 onERC1155ReceivedonERC1155BatchReceived 函数,以便始终在转移结束时调用。

保证日志跟踪

随着以太坊生态系统的持续发展,许多 dapps 依赖于传统的数据库和浏览器 API 服务来检索和分类数据。ERC-1155 标准保证由智能合约发出的事件日志将提供足够的数据来创建所有当前 token 余额的准确记录。数据库或浏览器可以侦听事件,并能够提供合约中每个 ERC-1155 token 的索引和分类搜索。

因此,ERC-1155 合约必须仔细地在创建、铸造、转移或销毁 token 的任何实例中发出 TransferSingleTransferBatch 事件。

授权

函数 setApprovalForAll 允许操作员代表授权人管理其整个 token 集合。它使与交易所和交易合约的无摩擦交互成为可能。

可以使用其他接口或外部合约来限制对特定 token ID、数量或其他规则的授权。这样做的理由是使 ERC-1155 标准尽可能通用,适用于所有用例,而不会对可能不需要它的实现施加特定的授权方案。可以使用标准 token 授权接口,例如建议的 ERC-1761 范围授权接口,该接口与 ERC-1155 兼容。

向后兼容性

在设计讨论期间,一直有要求该标准在发送到合约地址时与现有标准兼容,特别是在撰写本文时与 ERC-721 兼容。 为了适应这种情况,如果合约未按照上述“安全转移规则”部分中的规定实现 ERC1155TokenReceiver,则在还原逻辑方面存在一些回旋余地,特别是“场景#3:接收者未实现必要的 ERC1155TokenReceiver 接口函数”。

因此,在混合 ERC-1155 合约实现中,必须在接收者合约上进行额外调用并进行检查,然后才能进行对 onERC1155ReceivedonERC1155BatchReceived 的任何挂钩调用。 因此,操作顺序必须为:

  1. 该实现必须在接收者合约上调用函数 supportsInterface(0x4e2312e0),至少提供 10,000 gas。
  2. 如果函数调用成功并且返回值是常量值 true,则实现将像常规 ERC-1155 实现一样进行,并调用 onERC1155ReceivedonERC1155BatchReceived 挂钩以及关联的规则。
  3. 如果函数调用失败或返回值不是常量值 true,则实现可以假定接收者合约不是 ERC1155TokenReceiver,并遵循其其他标准的转移规则。

请注意,建议使用单个标准的纯实现,而不是混合解决方案,但是在实现下的参考部分中链接了一个混合 ERC-1155/ERC-721 合约的示例。

一个重要的考虑因素是,即使使用另一个标准的规则发送了 token,也必须仍然发出 ERC-1155 转移事件。 这样才能根据 ERC-1155 标准规则仅通过事件来确定余额。

用法

此标准可用于表示整个域的多种 token 类型。同质化和非同质化 token 都可以存储在同一智能合约中。

批量转移

safeBatchTransferFrom 函数允许批量转移多个 token ID 和值。ERC-1155 的设计使得批量转移成为可能,而无需像现有 token 标准那样使用包装合约。与具有多个交易的单个转移相比,当批量转移中包含多个 token 类型时,这降低了 gas 成本。

标准化批量转移的另一个优点是,智能合约可以使用 onERC1155BatchReceived 在单个操作中响应批量转移。

建议客户端和钱包在发布批量转移时对 token ID 和关联值(按升序)进行排序,因为某些 ERC-1155 实现在 ID 排序时可显着节省 gas 成本。有关示例,请参见 Horizon Games - 多 Token 标准 的“打包余额”实现。

批量余额

balanceOfBatch 函数允许客户端通过单个调用检索多个所有者和 token ID 的余额。

从事件中枚举

为了保持实现 ERC-1155 的合约的低存储要求,必须使用事件日志完成枚举(发现 token 的 ID 和值)。建议交易所和区块链浏览器等客户端至少维护一个包含 token ID、供应和 URI 的本地数据库。这可以从每个 TransferSingle、TransferBatch 和 URI 事件构建,从智能合约部署的区块到最新区块。

因此,ERC-1155 合约必须在创建、铸造、转移或销毁 token 的任何实例中仔细地发出 TransferSingleTransferBatch 事件。

非同质化 Token

以下策略是如何在同一合约中混合同质化和非同质化 token 的示例。该标准不强制实施如何执行此操作。

拆分 ID 位

任何 ERC-1155 函数中的 uint256 _id 参数的顶部 128 位可以表示基本 token ID,而底部 128 位可以表示非同质化的索引,使其唯一。

可以使用基于索引的访问器与非同质化 token 进行交互,以访问合约/token 数据集。因此,要访问混合数据合约中的特定 token 集和该集中特定的非同质化 token,可以将 _id 作为 <uint128: base token id><uint128: index of non-fungible> 传递。

要将非同质化集/类别作为一个整体(或一个同质化 token)进行标识,您可以仅通过 _id 参数传递基本 ID,如 <uint128: base token id><uint128: zero> 所示。如果您的实现使用了该技术,则自然地意味着非同质化 token 的索引应从 1 开始。

在合约代码内部,可以使用 uint128(~0) 和相同 Mask 移位 128 来提取访问单个非同质化 token 所需的两段数据。

uint256 baseTokenNFT = 12345 << 128;
uint128 indexNFT = 50;

uint256 baseTokenFT = 54321 << 128;

balanceOf(msg.sender, baseTokenNFT); // 获取非同质化集 12345 的基本 token 的余额(如果实现希望作为一种便利,这可以用于获取用户对所有 token 集的余额)。
balanceOf(msg.sender, baseTokenNFT + indexNFT); // 获取非同质化集 12345 的索引 50 处的 token 的余额(如果用户拥有单个非同质化 token,则应为 1;如果他们没有,则应为 0)。
balanceOf(msg.sender, baseTokenFT); // 获取同质化基本 token 54321 的余额。

请注意,128 是一个任意数字,实现可以选择如何根据其用例进行拆分。合约的观察者将仅查看显示余额转移和铸造的事件,并且可以使用该信息单独跟踪余额。 为了使观察者能够仅从 ID 来确定类型(非同质化或同质化),他们必须以逐个实现为基础地了解拆分 ID 位的格式。

ERC-1155 参考实现 是拆分 ID 位策略的一个示例。

天然非同质化 token

表示非同质化 token 的另一种简单方法是允许每个非同质化 token 的最大值为 1。这将自然地反映现实世界,其中唯一的项目数量为 1,而同质化项目的数量大于 1。

参考

标准

实现

文章和讨论

版权

通过 CC0 放弃版权和相关权利。

Citation

Please cite this document as:

, "EIP-: ```markdown," Ethereum Improvement Proposals, no. , . [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-.