{"content":{"title":"详解 ERC-1155 多代币标准","body":">- 原文链接：[www.rareskills.io/post...](https://www.rareskills.io/post/erc-1155)\r\n>- 译者：[AI翻译官](https://learnblockchain.cn/people/19584)，校对：[翻译小组](https://learnblockchain.cn/people/412)\r\n>- 本文链接：[learnblockchain.cn/article…](https://learnblockchain.cn/article/10524)\r\n    \r\nERC-1155 标准描述了如何创建可替代和不可替代的代币，并将它们整合到一个智能合约中。当涉及多个代币时，这可以节省大量的部署成本。\r\n\r\n想象一下，你是一名游戏开发者，试图将 NFT 和 ERC-20 代币整合到你的平台中，代表各种类型的资产，如鞋子、剑、帽子和游戏内货币。\r\n\r\n使用像 ERC-721 和 ERC-20 这样的标准将要求你为每个 NFT 和 ERC-20 的集合开发多个代币合约。部署所有这些合约将是昂贵的。\r\n\r\n如果你可以在一个合约中定义和管理所有的 NFT 资产和代币，那不是很方便吗？然后，你甚至可以创建一个机制来一次性批准或转移多个 NFT。\r\n\r\n这个用例就是为什么 Enjin，一个 NFT 和游戏开发组织，向以太坊的 Github 仓库提交了 ERC-1155 多代币标准的第一个提案。在 2018 年 6 月 17 日，Enjin 的 ERC-1155 代币标准被以太坊基金会正式采纳。\r\n\r\n## ERC-1155 的关键特性\r\n\r\n### 处理多种代币类型：可替代和不可替代\r\n\r\n为了在单个合约中处理多种类型的代币（可替代和/或不可替代），ERC-1155 实现必须使用唯一的 `uint256` 代币 ID 来区分每种代币类型。这允许合约为每个代币定义独特的属性，如总供应量、URI、名称、符号等，并确保每个代币的配置彼此独立。\r\n\r\n以下是 ERC-1155 代币 ID 结构的示例：\r\n\r\n*   **代币 ID: 0**\r\n*   **代币 ID: 1**\r\n*   **代币 ID: 2**\r\n*   …\r\n\r\n代币 ID 不必是连续的。它们只需是唯一的。标准并未规定代币 ID 应如何创建，因此 **“铸造”函数并不是规范的一部分。**\r\n\r\n### 可替代性定义\r\n\r\n以下是可替代和不可替代代币的定义；ERC-1155 支持两者。\r\n\r\n*   **可替代**\r\n    \r\n    这些代币彼此相同，如货币单位。要在 ERC-1155 中定义可替代代币集，你只需为给定的代币 ID 铸造多个代币。\r\n    \r\n    当每个代币共享相同的 ID 时，它们也将具有相同的名称和符号。这将使代币以与 ERC-20 相同的方式运作，因为它将具有多个相同的单位，使用相同的名称和符号。与 ERC-20 不同的是，没有小数来解释可替代代币的数量。所有可替代代币余额以整数单位表示。\r\n    \r\n*   **不可替代**\r\n    \r\n    ERC-1155 中的不可替代代币（NFT）是独特的代币，每个代币与其他代币不同。这些代币通过为每个独特项目分配其自己的代币 ID 来表示，该 ID 是一个唯一的 `uint256` 值。\r\n    \r\n\r\n## 如何将多个不可替代代币放入 ERC-1155\r\n\r\n在单个 ERC-1155 合约中管理多个 NFT 集合时，分配随机唯一的代币 ID 可能会使识别特定代币 ID 属于哪个集合变得具有挑战性。\r\n\r\n为了解决这个问题，一个解决方案是以一种方式构造代币 ID，使得 ID 编码了集合和单个项目的信息：我们只需将两个数字连接在一起，连接形成的数字就是 ID。\r\n\r\n以下是实现方法：\r\n\r\n我们将 `uint256` 代币 ID 分为两部分：\r\n\r\n1.  集合 ID：代币 ID 的高位（最重要的 128 位）表示特定集合。\r\n2.  项目 ID：低位（最不重要的 128 位）表示该集合中的单个项目。\r\n\r\n这种方案使我们能够轻松识别代币 ID 属于哪个集合，以及它在该集合中的哪个项目。所有不可替代代币将通过这种编码彼此不同。\r\n\r\n下图显示了代币 ID 被分为集合 ID（`X` 值）和项目 ID（`Y` 值）：\r\n\r\n![image shows the token ID divided into the collection id (`X` values) and item ID (`Y` values)](https://img.learnblockchain.cn/attachments/migrate/1736159466049)\r\n\r\n为了将集合和项目信息编码到单个 `uint256` 代币 ID 中，我们可以使用位移和加法操作。\r\n\r\n**位移**\r\n\r\n位移是将零位添加到位序列的开头或结尾的过程，基本上是将现有位向左（Solidity 操作 `<<`）或向右（`>>`）移动。\r\n\r\n通过位移，我们可以将一个 128 位数字“注入”到 256 位数字的最重要的 128 位中。默认情况下，如果我们将一个 128 位数字转换为 256 位数字，128 位数字将位于最不重要的 128 位中。\r\n\r\n考虑这个表示十进制数字 `2` 的 256 位（或 32 字节）值，向左移动 128 位（或 16 字节）：\r\n\r\nhttps://img.learnblockchain.cn/2025/mp4/movie-bitshift-only.mp4\r\n\r\n在将十进制值 `2` 向左移动 128 位（`2 << 128`）后，我们得到新的十进制值 `680564733841876926926749214863536422912` 或 0x0000000000000000000000000000000200000000000000000000000000000000\r\n\r\n以十六进制表示。\r\n\r\n使用这种位移技术，我们能够用零填充最不重要的 128 位。由于 NFT ID 存储为 `uint256` 类型，我们可以将 `项目 ID` 添加到 `位移的集合 ID`。以下是一个简单的公式来说明这一点：\r\n\r\n`uint256 token_ID = shifted_collection_id + individual_token_id`\r\n\r\n以下动画展示了位移和加法操作在后台是如何发生的：\r\n\r\nhttps://img.learnblockchain.cn/2025/mp4/movie-bitshift-add.mp4\r\n\r\n\r\n想象这个场景，一个 ERC-1155 合约有两个不同的不可替代代币集合：CoolPhotos 和 RareSkills，分别具有 `collectionID` 1 和 2。如果 Bob 想检查他是否拥有来自 RareSkills 集合的 `itemID` 7 的项目，则用于此检查的有效代币 ID 将是 `collectionID` 和 `itemID` 的组合：\r\n\r\n$$  \r\n\\texttt{0x\\textcolor{orange}{00000000000000000000000000000002}\\textcolor{lightgreen}{00000000000000000000000000000007}}\r\n $$\r\n\r\n其中橙色位表示 RareSkills 集合 ID，绿色位表示该集合的项目 ID。\r\n\r\n以下是上述示例中的 ERC-1155 合约可能如何存储和检索给定代币 ID 的账户余额：\r\n```\r\n    // 嵌套映射以存储余额\r\n    // tokenID => owner => balance\r\n    mapping(uint256 => mapping(address => uint256)) balances;\r\n    \r\n    // 检索特定代币 ID 的地址余额\r\n    function balanceOf(address owner, uint256 tokenid) public view returns (uint256) {\r\n        return balances[tokenid][owner];\r\n    }\r\n\r\n```\r\n使用上述代码，Bob 可以调用 `balanceOf` 函数，使用代币 ID `(2 << 128) + 7` 来检查他的所有权：\r\n\r\n```\r\n    uint256 rareSkillsTokenCollectionID = 2 << 128; // collection id is 2\r\n    uint256 rsNFT = 7; // item id\r\n    \r\n    // 如果 Bob 拥有传递的 tokenid，则返回 1，否则返回 0\r\n    uint256 bobBalance = balanceOf(\r\n                            address(Bob), \r\n                            rareSkillsTokenCollectionID + rsNFT  // (2 << 128) + 7 \r\n                        );\r\n```\r\n\r\n如果 `bobBalance = 1`，则 Bob 拥有来自 RareSkills 集合的 `itemID` 7 的项目。至关重要的是，合约必须强制该代币的总供应量不能超过 1，否则该代币将变为可替代代币而不是不可替代代币。\r\n\r\n我们之前讨论了使用位移方法唯一计算代币 ID。要反转此过程并从 `tokenId` 获取 `collectionId` 和 `itemId`，我们将 `tokenId` 向右移动 128 位以检索 `collectionId`，并将 `tokenId` 转换为 128 位以获得 `itemId`。\r\n\r\n以下是计算的示例代码：\r\n\r\n1. 给定 NFT 集合 ID 和 itemId 计算 ERC-1155 代币 ID\r\n2. 给定 ERC-1555 代币 ID 计算集合 ID 和 item ID\r\n\r\n```\r\n    contract A {\r\n    \r\n        // 1. 计算代币 ID\r\n        function getTokenId(\r\n            uint256 collectionId, \r\n            uint256 itemId \r\n            ) public pure returns (bytes32 tokenId) {\r\n    \r\n            // 将集合 ID 左移 128 位\r\n            uint256 shiftedCollectionId = collectionId << 128;\r\n    \r\n            // 将 item ID 加到移位后的集合 ID 上\r\n            tokenId =  bytes32(shiftedCollectionId + itemId);\r\n        }\r\n    \r\n        // 2. 获取集合 ID 和 item ID\r\n        function getCollectionIdAndItemId(\r\n            uint256 tokenId\r\n            ) public pure returns (uint256 collectionId, uint256 itemId) {\r\n    \r\n            // 将代币 ID 右移 128 位\r\n            collectionId = tokenId >> 128;\r\n    \r\n            // 将代币 ID 转换为 128\r\n            itemId = uint128(tokenId);\r\n        }\r\n    \r\n    }\r\n```\r\n\r\n来自 [Remix](https://remix.ethereum.org/?#code=Ly8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEdQTC0zLjAKcHJhZ21hIHNvbGlkaXR5IF4wLjguMjg7Cgpjb250cmFjdCBBIHsKCiAgICBmdW5jdGlvbiBnZXRUb2tlbklkKAogICAgICAgIHVpbnQyNTYgY29sbGVjdGlvbklkLCAKICAgICAgICB1aW50MjU2IGl0ZW1JZCAKICAgICAgICApIHB1YmxpYyBwdXJlIHJldHVybnMgKGJ5dGVzMzIgdG9rZW5JZCkgewogICAgICAgIC8vIHNoaWZ0IHRoZSBjb2xsZWN0aW9uIGlkIGJ5IDEyOCB0byB0aGUgbGVmdAogICAgICAgIHVpbnQyNTYgc2hpZnRlZENvbGxlY3Rpb25JZCA9IGNvbGxlY3Rpb25JZCA8PCAxMjg7CgogICAgICAgIC8vIGFkZCB0aGUgaXRlbSBpZCB0byB0aGUgc2hpZnRlZCBjb2xsZWN0aW9uIGlkCiAgICAgICAgdG9rZW5JZCA9ICBieXRlczMyKHNoaWZ0ZWRDb2xsZWN0aW9uSWQgKyBpdGVtSWQpOwogICAgfQoKICAgIGZ1bmN0aW9uIGdldENvbGxlY3Rpb25JZEFuZEl0ZW1JZCgKICAgICAgICB1aW50MjU2IHRva2VuSWQKICAgICAgICApIHB1YmxpYyBwdXJlIHJldHVybnMgKHVpbnQyNTYgY29sbGVjdGlvbklkLCB1aW50MjU2IGl0ZW1JZCkgewogICAgICAgIC8vIGNhc3QgdGhlIHRva2VuIGlkIHRvIDEyOCBiaXRzCiAgICAgICAgaXRlbUlkID0gdWludDEyOCh0b2tlbklkKTsKCiAgICAgICAgLy8gc2hpZnQgdGhlIHRva2VuIGlkIHRvIHRoZSByaWdodCBieSAxMjggCiAgICAgICAgY29sbGVjdGlvbklkID0gdG9rZW5JZCA+PiAxMjg7CiAgICB9Cgp9&lang=en&optimize=false&runs=200&evmVersion=null&version=soljson-v0.8.28+commit.7893614a.js) 的屏幕截图，显示了两个函数的测试代码：\r\n\r\n![Remix 代码截图调用两个函数](https://img.learnblockchain.cn/attachments/migrate/1736159466043)\r\n\r\n结构化代币 ID 技术是一种实现多个非同质化代币与 ERC1155 的方法，因为该标准并未规定必须如何实现。然而，有一种名为 [ERC1155D](https://medium.com/donkeverse/introducing-erc1155d-the-most-efficient-non-fungible-token-contract-in-existence-c1d0a62e30f1) 的 ERC1155 实现，它是对原始标准的迭代，旨在优化铸造非同质化代币的 gas 效率，如果合约只需要支持单个 NFT 集合。\r\n\r\n### ERC-1155D\r\n\r\nERC-1155D 专为非同质化代币（与 ERC-721 相同）设计，其中每个代币都有唯一的标识符和唯一的拥有者。它与 ERC-1155 完全向后兼容。\r\n\r\n**何时使用 ERC1155D？**\r\n\r\n当你在合约中不需要多个非同质化代币集合（如 CoolPhotos RareSkills 示例）时，使用 ERC1155D，同时强制代币的供应量为 1，并且最多只有一个拥有者。\r\n\r\n总之，所有代币都在单个合约下管理，使用 `uint256` 值作为代币 ID。然而，特定代币 ID 如何分配给不同类型的代币完全取决于合约的用例。\r\n\r\n## 核心 ERC1155 函数\r\n\r\n这些是实现 ERC1155 标准的合约必须实现的 ERC1155 接口中的函数。每个函数的代码片段来自标准的规范。\r\n\r\n### 余额检索\r\n\r\n*   **balanceOf**\r\n    \r\n    在 ERC-721 中，`balanceOf(address _owner)` 返回整个代币 ID 集合的地址余额。因此，如果一个地址拥有代币 1、5 和 7，则该地址的 `balanceOf(address _owner)` 将返回 `3`。\r\n    \r\n    然而，在 ERC-1155 中，`balanceOf` 函数的结构是为了检索特定代币 ID 对于特定账户地址的代币余额。\r\n    \r\n```\r\n          /**\r\n              @notice 获取账户代币的余额。\r\n              @param _owner  代币持有者的地址\r\n              @param _id     代币的 ID\r\n              @return        请求的代币类型的 _owner 的余额\r\n          */\r\n          function balanceOf(address _owner,uint256 _id) external view returns (uint256);\r\n```\r\n    \r\n一个地址可以持有不同数量的各种代币 ID，例如 1 个代币 ID 1，20 个代币 ID 5 等等。然而，在 ERC-1155 合约中，没有直接的方法来衡量一个地址在所有代币 ID 中拥有的代币总数，因为 `balanceOf` 函数的设计仅用于检查 **你拥有的特定 tokenID 的数量**，而不是你在整个合约中拥有的 tokenIDs 的数量。\r\n    \r\n*   **balanceOfBatch**\r\n    \r\n    还存在一种批量机制，称为 `balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids)`。此方法通过循环调用 `balanceOf` 一次性检索每个地址每个 ID 的多个余额。\r\n\r\n```\r\n          /**\r\n              @notice 获取多个账户/代币对的余额\r\n              @param _owners  代币持有者的地址\r\n              @param _ids     代币的 ID\r\n              @return        请求的代币类型的 _owner 的余额（即每个 (owner, id) 对的余额）\r\n          */\r\n          function balanceOfBatch(\r\n                  address[] calldata _owners,\r\n                  uint256[] calldata _ids\r\n                  ) external view returns (uint256[] memory);\r\n    \r\n```\r\n\r\nERC-1155 不支持列出所有现有代币 ID 的机制。\r\n    \r\n 要获取 1155 合约的所有现有 ID，我们必须解析链下的日志（稍后我们将展示如何做到这一点）。\r\n    \r\n\r\n### 全部授权\r\n\r\nERC-1155 允许所有者通过调用 `setApprovalForAll(address _operator, bool _approved)` 方法在单个交易中授予操作员管理其所有代币的权限。此函数接受操作员的 `address` 和表示批准状态的 `bool` 作为参数：\r\n\r\n```\r\n    /**\r\n        @notice 启用或禁用第三方（“操作员”）管理调用者所有代币的批准。\r\n        @dev 成功时必须发出 ApprovalForAll 事件。\r\n        @param _operator  要添加到授权操作员集合的地址\r\n        @param _approved  如果操作员被批准则为真，撤销批准则为假\r\n        */\r\n    function setApprovalForAll(address _operator,bool _approved) external;\r\n\r\n```\r\n请注意，此方法实际上批准用户在 ERC-1155 合约中拥有的所有内容。这就像为 ERC-20 设置最大批准并为 ERC-721 调用 setApprovalForAll。操作员可以转移 ERC-1155 合约中所有者的任何代币，无论数量多少。\r\n\r\n### 安全转移 safeTransfer\r\n\r\n遵循 ERC-721 模式，ERC-1155 还具有 [“安全转移”机制](https://www.rareskills.io/post/erc721#viewer-7r7m6)，该机制检查确保代币的接收者是有效的接收者。实际上，ERC-1155 仅支持安全转移。\r\n\r\n*   **safeTransferFrom**\r\n    \r\n```\r\n          /**\r\n              @param _from    来源地址\r\n              @param _to      目标地址\r\n              @param _id      代币类型的 ID\r\n              @param _value   转账金额\r\n              @param _data    附加数据，格式不指定，必须以原样发送到 `_to` 的 `onERC1155Received` 调用中\r\n          */\r\n          function safeTransferFrom(\r\n                      address _from,\r\n                      address _to,\r\n                      uint256 _id,\r\n                      uint256 _value,\r\n                      bytes calldata _data) external;\r\n```\r\n    \r\n如果接收方是一个 EOA，那么 `safeTransferFrom` 会检查地址是否不是零地址。如果接收方是一个智能合约，则会调用 `onERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data)` 回调函数，并期望返回魔法值 `bytes4(keccak256(\"onERC1155Received(address,address,uint256,uint256,bytes)\"))`。\r\n    \r\n一个 ERC-1155 代币不能转移到一个未实现 `onERC1155Received` 或错误实现 `onERC1155Received` 的智能合约。\r\n    \r\n*   **safeBatchTransferFrom**\r\n    \r\n```\r\n          /**\r\n              @param _from    来源地址\r\n              @param _to      目标地址\r\n              @param _ids     每种代币类型的 ID（顺序和长度必须与 _values 数组匹配）\r\n              @param _values  每种代币类型的转账金额（顺序和长度必须与 _ids 数组匹配）\r\n              @param _data    附加数据，格式不指定，必须以原样发送到 `_to` 的 `ERC1155TokenReceiver` 钩子中\r\n          */\r\n          function safeBatchTransferFrom(\r\n                      address _from,\r\n                      address _to,\r\n                      uint256[] calldata _ids,\r\n                      uint256[] calldata _values,\r\n                      bytes calldata _data) external;\r\n```\r\n    \r\n此外，该标准允许所有者和操作员执行批量转账。可以在一个交易中将多组代币从源地址转移到目标地址。\r\n    \r\n批量转账可以通过调用：\r\n    \r\n    *   `safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data)`,\r\n        *   它将调用接收方的 `onERC1155BatchReceived(address _operator, address _from, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data)` 回调\r\n            *   并期望返回魔法值 `bytes4(keccak256(\"onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)\"))`。\r\n    \r\n    **SafeTransferFrom vs SafeBatchTransferFrom**\r\n    \r\n    使用 OpenZeppelin 的 ERC1155 实现，下面的图像比较了调用 `safeTransferFrom` 三次与将转账批量处理为一次交易的 gas 使用情况：\r\n    \r\n    ![Gas benchmark of SafeTransferFrom and SafeBatchTransferFrom](https://img.learnblockchain.cn/attachments/migrate/1736159466415)\r\n    \r\n\r\n使用 `safeBatchTransferFrom`，如红框所示，消耗 132,437 gas，这显著低于蓝框中三个独立 `safeTransferFrom` 调用所使用的 189,861 gas。\r\n\r\n## 核心数据结构\r\n\r\nERC-1155 实现通常使用映射来保存核心数据的状态，例如前述的余额、批准和 URI。例如，ERC-1155 可以使用以下存储变量：\r\n\r\n```\r\n    mapping(uint256 id => mapping(address account => uint256 balance)) internal _balances;\r\n    \r\n    mapping(address account => mapping(address operator => bool isApproved)) internal _operatorApprovals;\r\n    \r\n    string private _uri;\r\n\r\n```\r\n让我们在接下来的部分中检查每个数据结构。\r\n\r\n### 余额\r\n\r\n余额存储在一个具有两个级别的嵌套映射中。外部映射的键表示 `token ID`，指向另一个将 `address`（所有者）映射到 `_balances` 的映射。\r\n\r\n为了返回该结构下给定代币的账户余额，`balanceOf` 实现将以如下方式访问值：\r\n\r\n    function balanceOf(address account, uint256 id) public view returns (uint256) {\r\n        return _balances[id][account];\r\n    }\r\n\r\n### 批准\r\n\r\n类似地，批准存储在一个嵌套映射中，因为一个账户可以授予多个操作员批准。外部映射的键是所有者，指向一个映射，该映射将操作员映射到他们的批准状态。\r\n\r\n考虑以下 `isApprovedForAll` 函数的示例实现，通过它访问批准状态：\r\n```\r\n    function isApprovedForAll(address account, address operator) public view returns (bool) {\r\n        return _operatorApprovals[account][operator];\r\n    }\r\n\r\n```\r\n### 日志和事件\r\n\r\nERC-1155 标准保证通过观察智能合约发出的事件日志，可以创建所有当前代币余额的准确记录，因为每次代币铸造、销毁和转移都被记录。\r\n\r\n必须发出事件的场景列表如下：\r\n\r\n*   当某个地址授予或撤销另一个地址管理其所有代币的操作员批准时，必须发出 `ApprovalForAll` 事件：\r\n    \r\n```\r\n          event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);\r\n```    \r\n*   当代币从一个地址转移到另一个地址时，包括铸造和销毁，必须发出 `TransferSingle` 或 `TransferBatch` 事件。\r\n    \r\n```\r\n          // 当单个代币转移时发出\r\n          event TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value);\r\n        \r\n          // 当一批代币转移时发出\r\n          event TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values);\r\n```\r\n    \r\n如果调用 `safeBatchTransferFrom` 函数时使用了单个 tokenID，则会发出 `TransferSingle` 事件，否则会发出 `TransferBatch` 事件。\r\n    \r\n*   如果特定代币 ID 的元数据 URI 变化，必须发出 `URI` 事件：\r\n    \r\n```\r\n          event URI(string _value, uint256 indexed _id);\r\n```    \r\n\r\n随着这些事件在与之相关的函数调用时被记录/发出，我们可以在 JavaScript 中获取以下离线信息：\r\n\r\n*   **1155 合约的现有代币 ID：**\r\n    \r\n    下面的代码使用 ethers.js 库与 ERC-1155 合约进行交互，并在指定的区块范围内获取在 `TransferSingle` 和 `TransferBatch` 事件中发出的所有代币 ID 的列表。\r\n```javascript    \r\n          import { ethers } from \"ethers\"; // v6\r\n        \r\n          // 连接到以太坊提供者\r\n          const provider = new ethers.JsonRpcProvider(\"rpc-url\");\r\n        \r\n          // ERC-1155 合约地址和 ABI\r\n          const erc1155ContractAddress = \"YourContractAddress\";\r\n          const abi = [\r\n            /* ERC-1155 ABI 这里 */\r\n            \"event TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value)\",\r\n            \"event TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values)\",\r\n          ];\r\n          const contract = new ethers.Contract(erc1155ContractAddress, abi, provider);\r\n\r\n\r\n(async (startBlockNumber) => {\r\n  // Fetch `TransferSingle` and `TransferBatch` events\r\n  const singleEvents = await erc1155ContractInstance.queryFilter(\r\n    \"TransferSingle\", // event\r\n    startBlockNumber, // start block\r\n    startBlockNumber + 100000, // end block\r\n  );\r\n  const batchEvents = await erc1155ContractInstance.queryFilter(\r\n    \"TransferBatch\", // event\r\n    startBlockNumber, // start block\r\n    startBlockNumber + 100000, // end block\r\n  );\r\n\r\n  const tokenIds = new Set();\r\n\r\n  // Get token IDs from TransferSingle events\r\n  singleEvents.forEach((event) => {\r\n    // Destructure the `args` field\r\n    const { operator, from, to, id, value } = event.args;\r\n\r\n    // Add `id` to the `tokenIds` set\r\n    tokenIds.add(id);\r\n  });\r\n\r\n  // Get token IDs from TransferBatch events\r\n  batchEvents.forEach((event) => {\r\n    // Destructure the `args` field\r\n    const { operator, from, to, ids, values } = event.args;\r\n\r\n    // Loop through `ids` then add `id` to the `tokenIds` set\r\n    ids.forEach((id) => tokenIds.add(id.toString()));\r\n  });\r\n\r\n  console.log(\"Token IDs in existence:\", Array.from(tokenIds));\r\n})();\r\n```\r\n\r\n*   **用户拥有的所有代币 ID：**\r\n\r\n    以下代码列出用户拥有的所有 ID。它通过跟踪转移事件 `to` 和 `from` 来实现。为了确保准确，`startBlockNumber` 需要在该地址的最早交互之前设置。\r\n\r\n```javascript\r\nasync function getUserTokenIds(userAddress, startBlockNumber) {\r\n  const singleEvents = await erc1155ContractInstance.queryFilter('TransferSingle', startBlockNumber, startBlockNumber + 100000);\r\n  const batchEvents = await erc1155ContractInstance.queryFilter('TransferBatch', startBlockNumber, startBlockNumber + 100000);\r\n\r\n  const balances = {};\r\n\r\n  // Process TransferSingle events\r\n  singleEvents.forEach(event => {\r\n    const { operator, from, to, id, value } = event.args;\r\n\r\n    if (to.toLowerCase() === userAddress.toLowerCase()) {\r\n      balances[id] = (balances[id] || 0) + parseInt(value.toString());\r\n    }\r\n\r\n    if (from.toLowerCase() === userAddress.toLowerCase()) {\r\n      balances[id] = (balances[id] || 0) - parseInt(value.toString());\r\n    }\r\n  });\r\n\r\n  // Process TransferBatch events\r\n  batchEvents.forEach(event => {\r\n    const { operator, from, to, ids, values } = event.args;\r\n\r\n    ids.forEach((id, index) => {\r\n      const value = parseInt(values[index].toString());\r\n\r\n      if (to.toLowerCase() === userAddress.toLowerCase()) {\r\n        balances[id] = (balances[id] || 0) + value;\r\n      }\r\n\r\n      if (from.toLowerCase() === userAddress.toLowerCase()) {\r\n        balances[id] = (balances[id] || 0) - value;\r\n      }\r\n    });\r\n  });\r\n\r\n  // Filter out IDs with a balance greater than zero\r\n  const ownedTokenIds = Object.keys(balances).filter(id => balances[id] > 0);\r\n\r\n  console.log(ownedTokenIds);\r\n}\r\n```\r\n\r\n### 统一资源标识符 (URIs)\r\n\r\nERC-1155 仅有一个 `uri` 函数，如标准所规定。该标准并未规定 `uri` 函数是否应使用或忽略代币 ID。如何获取 uri 取决于合约的实现。例如，如果合约实现需要共享 URI，我们可以忽略 id 直接返回基础 uri `_uri`，否则，我们可以对代币 ID 和基础 uri 进行编码。\r\n\r\n**代币 ID 的共享 URI 的示例实现：**\r\n\r\n```solidity\r\nstring private _uri;\r\n\r\nfunction uri(uint256 /* id */) public view virtual returns (string memory) {\r\n  return _uri;\r\n}\r\n```\r\n\r\n以上 `uri` 函数将始终返回相同的 URI，忽略代币 ID。\r\n\r\n**每个代币 ID 唯一 URI 的示例实现：**\r\n\r\n如果我们希望根据代币 ID 更改返回的字符串，`Strings` 库将非常有用，但它不是 Solidity 的本地库，而是 [OpenZeppelin Strings library](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Strings.sol) 的一部分。在下面的示例实现中，它用于将一个 uint256 的 `tokenID` 转换为编码为 Solidity 字符串的十六进制数。\r\n\r\n下面是如何使用 `Strings` 库根据 ID 更改 URI 的示例：\r\n\r\n```solidity\r\nimport \"@openzeppelin/contracts/utils/Strings.sol\";\r\n\r\nstring private _uri;\r\n\r\nfunction uri(uint256 id) public view virtual returns (string memory) {\r\n  return string(abi.encodePacked(\r\n    _uri,\r\n    Strings.toHexString(id, 32), // Convert tokenId to hex with fixed length\r\n    \".json\"\r\n  ));\r\n}\r\n```\r\n\r\n`uri` 函数通过将传入的代币 ID 附加到基础 URI，为每个代币返回唯一的 URI。例如，如果基础 URI 是 `https://token-cdn-domain/`，使用代币 ID `314592`（十六进制为 0x4CCE0）调用该函数将返回 `https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json`。\r\n\r\n该标准要求客户端用实际代币 ID 的十六进制字符串表示替换 `{id}` 参数（如果存在）。替换的字符串必须是小写字母数字：[0-9a-f]，并且没有“0x”前缀，如果需要，前导零填充到 64 个十六进制字符长度。\r\n\r\n代币 ID 替换方法通过将传入的代币 ID 附加到基础 uri，减少了存储大量代币的唯一 URI 所需的开销。\r\n\r\n**URIs 的结构**\r\n\r\n标准并不要求 ERC-1155 代币必须具有 URI 元数据。然而，如果 ERC-1155 实现合约定义了任何代币的 URI，则必须指向符合“ERC-1155 元数据 URI JSON schema”的 JSON 文件。\r\n\r\n该 URI 通常指向一个离线资源，例如一个服务器或 IPFS，其中存储元数据。\r\n\r\nERC-1155 元数据 URI JSON 架构如下所示：\r\n\r\n```json\r\n{\r\n    \"title\": \"Token Metadata\",\r\n    \"type\": \"object\",\r\n    \"properties\": { \r\n        \"name\": {\r\n            \"type\": \"string\",\r\n            \"description\": \"Identifies the asset to which this token represents\"\r\n        },\r\n        \"decimals\": {\r\n            \"type\": \"integer\",\r\n            \"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.\"\r\n        },\r\n        \"description\": {\r\n            \"type\": \"string\",\r\n            \"description\": \"Describes the asset to which this token represents\"\r\n        },\r\n        \"image\": {\r\n            \"type\": \"string\",\r\n            \"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.\"\r\n        },\r\n        \"properties\": {\r\n            \"type\": \"object\",\r\n            \"description\": \"Arbitrary properties. Values may be strings, numbers, object or arrays.\"\r\n        }\r\n    }\r\n}\r\n```\r\n\r\n一个符合上述 JSON 元数据架构的汽车 NFT 示例 JSON：\r\n\r\n```\r\n    {\r\n      \"title\": \"RareSkills Car Metadata\",\r\n      \"type\": \"object\",\r\n      \"properties\": {\r\n        \"name\": \"RareSkills Car #1\",\r\n        \"description\": \"一款具有尖端科技的高性能电动车。\",\r\n        \"image\": \"https://image-uri/rareskills-car1.png\",\r\n        \"year\": 2024,\r\n        \"topSpeed\": \"200 mph\",\r\n        \"batteryCapacity\": \"100 kWh\",\r\n        \"features\": [\"自动驾驶\", \"完全自驾\", \"高级音响系统\"],\r\n      }\r\n    }\r\n```\r\n    \r\n\r\n`title` 字段描述了元数据的目的，`type` 字段指定了元数据的数据格式，`properties` 字段定义了关于汽车的额外属性或元数据。\r\n\r\n### URI JSON Schema 中的本地化字段\r\n\r\n支持本地化的客户端可能通过利用 JSON 格式的 ERC-1155 中的 `localization` 属性来显示多种语言的代币信息（如果存在的话）。\r\n\r\n`localization` 元数据的架构如下：\r\n\r\n ```\r\n   {\r\n        \"title\": \"Token Metadata\",\r\n        \"type\": \"object\",\r\n        \"properties\": {\r\n    \r\n                ...\r\n    \r\n            \"localization\": {\r\n                \"type\": \"object\",\r\n                \"required\": [\"uri\", \"default\", \"locales\"],\r\n                \"properties\": {\r\n                    \"uri\": {\r\n                        \"type\": \"string\",\r\n                        \"description\": \"用于获取本地化数据的 URI 模式。此 URI 应包含子字符串 `{locale}`，在发送请求之前将被替换为适当的语言环境值。\"\r\n                    },\r\n                    \"default\": {\r\n                        \"type\": \"string\",\r\n                        \"description\": \"基本 JSON 中默认数据的语言环境\"\r\n                    },\r\n                    \"locales\": {\r\n                        \"type\": \"array\",\r\n                        \"description\": \"可用数据的语言环境列表。这些语言环境应符合 Unicode 通用语言环境数据仓库中定义的内容 (http://cldr.unicode.org/)。\"\r\n                    }\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n```\r\n以下是包含 `localization` 属性的元数据 JSON 文件示例：\r\n```\r\n\r\n    {\r\n     \"name\": \"RareSkills Token\",\r\n     \"description\": \"每个代币代表 RareSkills 社区中的一个独特通行证。\",\r\n     \"properties\": {\r\n         \"localization\": {\r\n           \"uri\": \"ipfs://xxx/{locale}.json\",\r\n           \"default\": \"en\",\r\n           \"locales\": [\"en\", \"es\", \"fr\"]\r\n         }\r\n     }\r\n    }\r\n\r\n```\r\n`locales` 属性是一个包含三个元素的数组：`en`、`es` 和 `fr`，`en` 设置为默认语言。数组中的每个元素都有其各自语言的元数据 JSON 文件。\r\n\r\nes.json：\r\n\r\n```\r\n    {\r\n      \"name\": \"RareSkills simbólico\",\r\n      \"description\": \"每个代币代表 RareSkills 社区中的一个独特通行证。\"\r\n    }\r\n```\r\n\r\nfr.json：\r\n```\r\n\r\n    {\r\n      \"name\": \"RareSkills Jeton\",\r\n      \"description\": \"每个代币代表 RareSkills 社区中的一个独特通行证。\"\r\n    }\r\n```\r\n\r\n类似于代币 ID 替换，如果 `uri` 包含字符串 `{locale}`，客户端必须将其替换为 `locales` 数组中定义的可用语言环境之一，然后指向目标语言的元数据 JSON 文件。\r\n\r\n**获取法语元数据的示例步骤**\r\n\r\n1.  调用代币 ID `314592` 的 `uri` 函数以获取代币元数据 JSON 的 URI\r\n```\r\n    \r\n         // 返回的 uri: https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json\r\n   \r\n``` \r\n2.  从步骤 1 中返回的 uri 中读取 JSON 内容，以获取所需语言的基本 URI\r\n    \r\n```json\r\n         {\r\n          \"name\": \"RareSkills Token\",\r\n          \"description\": \"每个代币代表 RareSkills 社区中的一个独特通行证。\",\r\n          \"properties\": {\r\n              \"localization\": {\r\n                \"uri\": \"https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0/{locale}.json\",\r\n                \"default\": \"en\",\r\n                \"locales\": [\"en\", \"es\", \"fr\"]\r\n              }\r\n          }\r\n         }\r\n```\r\n    \r\n3.  在 `localization → uri` 字段中用 `fr` 替换 `{locale}` 字符串，以获取法语版本的元数据 URI\r\n    \r\n```\r\n         // 法语语言 URI: \r\n         // [https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0/fr.json](https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json)\r\n    \r\n```\r\n\r\n在与不受信任的元数据交互时，请务必在解析之前清理结果。前端呈现的任何 JSON 可能是跨站脚本攻击的载体。\r\n\r\n### OpenSea 如何解释元数据\r\n\r\nERC-1155 合约得到 OpenSea 的支持，本节展示了 OpenSea 如何解释 ERC-1155 元数据。一个实时示例是来自一个名为 [Common Ground World](https://opensea.io/collection/commongroundworld) 的区块链游戏：\r\n\r\n![OpenSea common worlds nft 截图](https://img.learnblockchain.cn/attachments/migrate/1736159466462)\r\n\r\n截至撰写时，Common Ground World 定义了 681 个集合作为游戏内资产，OpenSea 在上面的图像中将其称为“独特物品”（红框）。每个集合中所有资产的总和大约为 `9 百万`（绿色框）。\r\n\r\n以下是该游戏的一部分集合示例：\r\n\r\n![OpenSea common worlds collection 截图](https://img.learnblockchain.cn/attachments/migrate/1736159466455)\r\n\r\n[水箱](https://opensea.io/assets/ethereum/0xc36cf0cfcb5d905b8b513860db0cfe63f6cf9f5c/232753138973921909008948231483329456635904) 集合的总供应量约为 4,800 件（绿色框），由大约 2,900 个地址（红框）拥有。\r\n\r\n请注意，OpenSea 不提供任何给定 ERC-721 代币的总供应信息，因为每个 tokenID 的供应量为一个且恰好有一个所有者。以下是一个随机的由 `F15C93` 拥有的无聊猿游艇俱乐部 NFT，仅作比较：\r\n\r\n![无聊猿游艇俱乐部随机 NFT 截图，由 F15C93 拥有](https://img.learnblockchain.cn/attachments/migrate/1736159466623)\r\n\r\n通过查看 OpenSea 的详情部分，可以更清楚地看出此代币符合 ERC-1155 标准，见下方红框：\r\n\r\n![OpenSea common world 元数据截图](https://img.learnblockchain.cn/attachments/migrate/1736159467573)\r\n\r\nOpenSea 能够通过从代币的元数据中提取数据来显示描述和特征信息，点击 [Token ID](https://tokens.gala.games/metadata/0xc36cf0cfcb5d905b8b513860db0cfe63f6cf9f5c/232753138973921909008948231483329456635904) 可观察到：\r\n\r\n```\r\n    {\r\n        \"decimalPlaces\": 0,\r\n        \"description\": \"永远不要低估为你的作物提供被动、按需水源的能力。你的农民会感谢你！\",\r\n        \"image\": \"https://tokens.gala.games/images/sandbox-games/town-star/storage/water-tank.gif\",\r\n        \"name\": \"水箱\",\r\n        \"properties\": {\r\n            \"category\": \"存储\",\r\n            \"game\": \"镇星\",\r\n            \"rarity\": {\r\n                \"hexcode\": \"#939393\",\r\n                \"icon\": \"https://tokens.gala.games/images/sandbox-games/rarity/common.png\",\r\n                \"label\": \"普通\",\r\n                \"supplyLimit\": 5159\r\n            },\r\n            \"tokenRun\": \"storage\"\r\n        }\r\n    }\r\n```\r\n\r\nOpenSea 定义了 [元数据标准](https://docs.opensea.io/docs/metadata-standards)，以使 URIs 符合要求，从而允许 OpenSea 提取 ERC721 和 ERC1155 资产的链外元数据。\r\n\r\n## 实现示例\r\n\r\n以下是一个简单游戏的 ERC-1155 实现合约示例。它实例化了 [OpenZeppelin 的 ERC-1155 抽象合约](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC1155/ERC1155.sol)，并具有额外的函数作为包装器和助手来改变游戏状态：\r\n\r\n*   `initializePlayer`：通过铸造由常量 `INITIAL_IN_GAME_CURRENCY_BALANCE` 定义的数量来初始化玩家账户。\r\n*   `mintInGameCurrency`：为特定玩家铸造额外的游戏内货币。\r\n*   `mintCar`：允许玩家铸造独特的基于 NFT 的汽车。\r\n```\r\n    // SPDX-License-Identifier: MIT\r\n    pragma solidity 0.8.24;\r\n    \r\n    import { ERC1155 } from \"@openzeppelin/contracts/token/ERC1155/ERC1155.sol\";\r\n    \r\n    contract GameAssets is ERC1155 {\r\n        uint256 constant TOKEN_ID_IN_GAME_CURRENCY = 0;    // 可替代代币 ID \r\n        uint256 constant TOKEN_ID_BASE_CAR_COLLECTION = 1; // 非可替代代币 ID \r\n    \r\n        uint256 constant INITIAL_IN_GAME_CURRENCY_BALANCE = 1000;\r\n        uint256 constant MINIMUM_AMOUNT = 1500;\r\n    \r\n        uint256 public nextTokenIndex;\r\n    \r\n        constructor(string memory uri) ERC1155(uri) {}\r\n    \r\n        function initializePlayer(address to, bytes memory data) public {\r\n            mintInGameCurrency(to, INITIAL_IN_GAME_CURRENCY_BALANCE, data);\r\n        }\r\n    \r\n        function mintInGameCurrency(address to, uint256 value, bytes memory data) public {\r\n            _mint(to, TOKEN_ID_IN_GAME_CURRENCY, value, data);\r\n        }\r\n    \r\n        function mintCar(address player, bytes memory data) public returns (uint256 carId) {\r\n            // 确保玩家的代币 ID `TOKEN_ID_IN_GAME_CURRENCY` 的余额\r\n            // 大于或等于 `MINIMUM_AMOUNT`\r\n            require(balanceOf(player, TOKEN_ID_IN_GAME_CURRENCY) >= MINIMUM_AMOUNT, \"\");\r\n    \r\n            // 非可替代的魔法\r\n            carId = (TOKEN_ID_BASE_CAR_COLLECTION << 128) + nextTokenIndex++;\r\n    \r\n            // 铸造汽车\r\n            _mint(player, carId, 1, data);\r\n        }\r\n    }\r\n```\r\n\r\n**注意：** 此合约仅用于演示目的，省略了关键的安全特性和优化。\r\n\r\n游戏将拥有两种类型的令牌：\r\n\r\n1.  玩家通过完成任务可以赚取的游戏内货币（$IGC）。这将是一个可替代代币。\r\n2.  表示玩家可以铸造的汽车集合的非可替代代币。\r\n\r\n当我们部署此合约时，我们的合约地址为 `0xCc3958FE4Beb3bcb894c184362486eBEc2E1fD4D`，我们将使用 `0x5B38Da6a701c568545dCfcB03FcB875f56beddC4` 作为玩家地址。\r\n\r\n在接下来的几个部分中，我们将演示如何与此合约交互以管理其代币资产。\r\n\r\n## 使用 ERC-1155 的游戏示例\r\n\r\n下方的流程图说明了玩家如何与游戏的 ERC-1155 合约进行交互，包括铸造游戏内货币和汽车。\r\n\r\n![游戏合约工作流程图](https://img.learnblockchain.cn/attachments/migrate/1736159467577)\r\n\r\n### ERC-1155 代币 0：铸造 $IGC\r\n\r\n![IGC 代币图像](https://img.learnblockchain.cn/attachments/migrate/1736159467707)\r\n\r\n假设我们希望玩家从 `1000` $IGC 开始。有了这个，我们可以通过在合约中调用 `initializePlayer` 函数来将这些代币铸造给每位玩家。这将把 IGC 的代币 ID（`0`）和铸造数量发送到 OpenZeppelin 基合约的 `_mint(address to, uint256 id, uint256 value, bytes memory data)`。\r\n\r\n这个 `_mint` 函数是 OpenZeppelin 创建代币的方法，最终会执行接受检查，调用 `safeTransferFrom` 并发出 `TransferSingle` 事件（下方蓝框），这是标准所要求的。\r\n\r\n在调用 `initializePlayer` 函数后，我们可以看到以下日志：\r\n\r\n![显示转移单个事件的屏幕截图](https://img.learnblockchain.cn/attachments/migrate/1736159468295)\r\n\r\n在红框中，我们可以看到 `TransferSingle` 事件被发出，而在绿框中，零地址向我们的玩家地址发送了 `1000` 单位的游戏内货币（代币 ID `0`）。\r\n\r\n### 铸造更多 $IGC\r\n\r\n随着我们的玩家完成任务，我们希望用更多的 $IGC 奖励他们。我们可以在游戏合约中调用 `mintInGameCurrency` 函数，这将调用 OpenZeppelin 的 `_mint` 函数，指定我们玩家的地址（`0x5B38Da6a701c568545dCfcB03FcB875f56beddC4`）、作为奖励的代币金额（`500`）以及任何要发送到接收者回调的字节数据（在这种情况下没有数据）。使用这些值调用 `mintInGameCurrency` 将铸造 500 个 IGC 代币。\r\n\r\n当我们通过 `balanceOf` 检查玩家的 $IGC 余额时：\r\n\r\n![玩家 $IGC 余额的屏幕截图](https://img.learnblockchain.cn/attachments/migrate/1736159468530)\r\n\r\n我们看到我们的玩家现在的余额为 `1500` $IGC（初始 + 奖励）。\r\n\r\n### ERC-1155 代币 1：铸造非可替代资产（汽车）\r\n\r\n![汽车 nft](https://img.learnblockchain.cn/attachments/migrate/1736159468856)\r\n\r\n现在，假设我们希望允许玩家通过拥有最低的 $IGC 余额来铸造汽车。请记住，汽车集合是非可替代的。\r\n\r\n首先，我们将为每辆汽车 NFT 定义独特的元数据，其中包含汽车的特征。\r\n\r\n例如，我们集合中第一辆汽车的 URI 将是：\r\n\r\n`https://token-cdn-domain/0000000000000000000000000000000100000000000000000000000000000000.json`\r\n\r\n其中 id 是：以十进制表示或以十六进制表示\r\n\r\n橙色部分表示汽车集合 ID（`1`），而绿色部分表示第一辆汽车的代币 ID（`0`）。它们共同形成一个指向元数据的唯一 ID，可以是：\r\n```\r\n    {\r\n        \"name\": \"超级快速汽车\",\r\n        \"description\": \"这辆超级快速的汽车与其他汽车不同，它超级快速。\",\r\n        \"image\": \"https://images.com/{id}.png\",\r\n        \"properties\": {\r\n            \"features\": {\r\n                \"speed\": \"100\",\r\n                \"color\": \"blue\",\r\n                \"model\": \"SuperFast x1000\",\r\n                \"rims\": \"aluminum\"\r\n            }\r\n        }\r\n    }\r\n```\r\n\r\n现在，我们在合约上调用 `mintCar` 函数来铸造他们的汽车 NFT：\r\n```\r\n    function mintCar(address player, bytes memory data) public returns (uint256 carId) {\r\n        // 确保玩家的代币 ID `TOKEN_ID_IN_GAME_CURRENCY` 的余额\r\n        // 大于或等于 `MINIMUM_AMOUNT`\r\n        require(balanceOf(player, TOKEN_ID_IN_GAME_CURRENCY) >= MINIMUM_AMOUNT, \"\");\r\n\r\n        // 非可替代的魔法\r\n        carId = (TOKEN_ID_BASE_CAR_COLLECTION << 128) + nextTokenIndex++;\r\n\r\n        // 铸造汽车\r\n        _mint(player, carId, 1, data);\r\n    }\r\n```\r\n\r\n`carId` 变量是非可替代的魔法发生的地方。它通过组合汽车集合 ID 和下一个可用的代币索引（从零开始）来计算每辆汽车 NFT 的唯一代币 ID。\r\n\r\n在调用 `mintCar` 函数后：\r\n\r\n![调用 mintCar 函数后的日志屏幕截图](https://img.learnblockchain.cn/attachments/migrate/1736159468947)\r\n\r\n正如预期的那样，向玩家的地址铸造了一个汽车 NFT（黄色框）来自 `地址零`。\r\n\r\n**注意：** NFT 的 ID（红框）为 `340282366920938463463374607431768211456`，这是 `(1 << 128) + 0` 的结果，其中 `1` 是汽车集合的基础代币 ID，而 `0` 是集合内 NFT 的项目 ID。\r\n\r\n除了在单个合约中管理可替代代币和不可替代代币外，解决 ERC-1155 合约中的安全漏洞也很重要。一个常见的漏洞是重入攻击，它可以利用铸造或转移过程。\r\n\r\n## ERC-1155 中铸造和转移的重入攻击\r\n\r\n由于在 `safeTransferFrom` 和 `safeBatchTransferFrom` 操作中执行的回调函数，使用 ERC-1155 的合约容易受到重入攻击。ERC-1155 本身是安全的，但如果添加像不安全的铸造这样的代码，可能会引入重入问题。\r\n\r\n考虑 [这个](https://github.com/RareSkills/solidity-riddles/blob/main/contracts/Overmint1-ERC1155.sol) 来自 [RareSkills 的 Solidity Riddles](https://github.com/RareSkills/solidity-riddles/tree/main) CTF 挑战的合约：\r\n```solidity\r\n\r\n    // SPDX-License-Identifier: GPL-3.0\r\n    pragma solidity 0.8.15;\r\n    import \"@openzeppelin/contracts/utils/Address.sol\";\r\n    import \"@openzeppelin/contracts/token/ERC1155/ERC1155.sol\";\r\n    import \"@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol\";\r\n    \r\n    contract Overmint1_ERC1155 is ERC1155 {\r\n        using Address for address;\r\n        mapping(address => mapping(uint256 => uint256)) public amountMinted;\r\n        mapping(uint256 => uint256) public totalSupply;\r\n    \r\n        constructor() ERC1155(\"Overmint1_ERC1155\") {}\r\n    \r\n        function mint(uint256 id, bytes calldata data) external {\r\n            require(amountMinted[msg.sender][id] <= 3, \"max 3 NFTs\");\r\n            totalSupply[id]++;\r\n            _mint(msg.sender, id, 1, data);\r\n            amountMinted[msg.sender][id]++;\r\n        }\r\n    \r\n        function success(address _attacker, uint256 id) external view returns (bool) {\r\n            return balanceOf(_attacker, id) == 5;\r\n        }\r\n    }\r\n```\r\n注意到 `mint` 函数试图防止 `msg.sender` 铸造超过 `3` 个 NFT。然而，它没有包含重入锁，也没有遵循检查-效果-交互模式，因为它在铸造后检查 `msg.sender` 已铸造的数量并执行回调。因此，攻击者可以通过在其恶意合约的 `onERC1155Received` 回调函数中调用 `mint` 函数来利用这个合约，如下所示的攻击合约所示：\r\n```solidity\r\n    contract AttackOvermint1_ERC1155 {\r\n        Overmint1_ERC1155 overmint1_ERC1155;\r\n    \r\n        constructor(Overmint1_ERC1155 _overmint1_ERC1155) {\r\n            overmint1_ERC1155 = _overmint1_ERC1155;\r\n        }\r\n    \r\n        function attackMint(uint256 id) external {\r\n            overmint1_ERC1155.mint(id, \"\");\r\n        }\r\n    \r\n        function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _amount, bytes calldata _data) public returns (bytes4) {\r\n            uint256 balance = overmint1_ERC1155.balanceOf(address(this), _id);\r\n    \r\n            if (balance < 5) {\r\n                overmint1_ERC1155.mint(1, \"\");\r\n            }\r\n    \r\n            return bytes4(keccak256(\"onERC1155Received(address,address,uint256,uint256,bytes)\"));\r\n        }\r\n    }\r\n```\r\n\r\n攻击者首先会在其恶意合约中调用一个函数以启动铸造。这将导致 `msg.sender` 为攻击者的合约。当 NFT 被铸造时，`onERC1155Received` 将在攻击者的合约上被调用。该函数检查所需的数量是否已经铸造，如果没有，则重新进入 `mint` 函数。\r\n\r\n对于 ERC-1155 实现合约来说，重要的是通过严格遵循 [检查-效果-交互模式和/或实现重入锁](https://learnblockchain.cn/article/10525) 来减轻这种漏洞。\r\n\r\n## 结论\r\n\r\nERC-1155 为在单个合约中实现多种类型的代币标准化了接口。这允许像批量操作和一次性批准多个代币这样的节省 gas 的机制，以及在部署代币合约时的便利。\r\n\r\n该标准消除了在管理各种代币集时与多个合约交互的需要，提高了区块链游戏和其他使用多种代币的项目的 gas 效率和用户体验。\r\n\r\n> 我是 [AI 翻译官](https://learnblockchain.cn/people/19584)，为大家转译优秀英文文章，如有翻译不通的地方，在[这里](https://github.com/lbc-team/Pioneer/blob/master/translations/10524.md)修改，还请包涵～"},"author":{"user":"https://learnblockchain.cn/people/20722","address":null},"history":"bafkreibw6enen6bm46dlqnbbnhneyymsw7fkf6p5bi2labteaouirtaazu","timestamp":1736161346,"version":1}