当前位置: 首页 > news >正文

【技术详解】 OpenZeppelin ERC1155:Solidity 多代币标准实现原理(附完整 Solidity 源码)​

一、引言:什么是 ERC1155?

在区块链世界中,代币标准定义了数字资产如何在智能合约中被创建、管理和交互。ERC1155 是 Ethereum Request for Comments 1155 的缩写,是由 Enjin 团队提出并已成为以太坊社区标准的代币规范。它是一种​​多代币标准​​,允许在一个智能合约中管理​​无限数量的代币类型​​,既可以是同质化代币(Fungible Tokens,如游戏金币),也可以是非同质化代币(Non-Fungible Tokens,如独特的游戏道具),甚至是两者的混合。

与 ERC20(仅支持同质化代币)和 ERC721(仅支持非同质化代币)相比,ERC1155 的最大优势在于​​高效性和灵活性​​。它特别适合需要管理大量相似但又可能不同的代币类型的应用场景,比如:

  • ​游戏行业​​:一个游戏中可能有数百或数千种道具,比如武器、防具、药水等,每种道具有不同的ID和数量,但同一种道具之间是可互换的(同质化),而不同ID的道具则代表不同的物品(非同质化)。

  • ​数字收藏品平台​​:可以同时管理限量版艺术品和批量生产的数字商品。

  • ​供应链与票据系统​​:管理不同类型的凭证、许可或资产。

标准

用途

特点

​ERC20​

同质化代币,比如 USDT、ETH 等

所有代币都一模一样,不可区分,比如 1 USDT = 1 USDT

​ERC721​

非同质化代币(NFT),比如 CryptoPunks、BAYC

每个代币都是独一无二的,有独立 ID 和元数据

​ERC1155​

​多代币混合标准,同时支持同质化和非同质化​

一个合约可以管理 ​​成千上万个代币ID​​,每个 ID 可以有任意数量,​​效率高、批量操作强​​,广泛用于游戏道具、数字资产集合等


二、合约概览与基础信息

1. 开源许可证声明

// SPDX-License-Identifier: MIT
// 声明该代码使用的开源许可证是 MIT,允许自由使用、修改和分发

这一行声明了该代码使用的开源许可证是 ​​MIT​​。MIT 许可证非常宽松,允许开发者​​自由使用、修改和分发​​代码,甚至用于商业用途,只需保留原始版权声明即可。这对于开源项目来说极为常见,也是企业友好型许可证。

2. Solidity 版本指定

pragma solidity ^0.8.20;
// 指定该智能合约使用 Solidity 0.8.20 或更高但兼容的版本编译

此语句指定了该智能合约必须使用 ​​Solidity 0.8.20 或更高但兼容的版本​​进行编译。Solidity 是以太坊智能合约的开发语言,每个版本都会引入新特性、优化和安全性改进。指定版本有助于确保合约在不同环境下的一致性,避免因编译器版本差异导致意外行为。

3. 导入依赖

// 导入各种接口和工具库
import {IERC1155} from "./IERC1155.sol";                         // ERC1155 标准接口,定义基础功能
import {IERC1155MetadataURI} from "./extensions/IERC1155MetadataURI.sol"; // 扩展接口,支持通过URI查询元数据
import {ERC1155Utils} from "./utils/ERC1155Utils.sol";         // 工具库,用于处理接收者校验等逻辑
import {Context} from "../../utils/Context.sol";              // 提供 msg.sender 等上下文信息
import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol"; // ERC165 接口检测标准
import {Arrays} from "../../utils/Arrays.sol";                // 数组操作的工具库
import {IERC1155Errors} from "../../interfaces/draft-IERC6093.sol"; // 定义错误类型

这一系列 import语句导入了 ERC1155 实现所需的各种​​接口和工具库​​,包括:

  • ​IERC1155​​:ERC1155 标准的基础接口,定义了核心功能如转账、余额查询等。

  • ​IERC1155MetadataURI​​:扩展接口,支持通过 URI 获取每个代币的元数据(如图片、描述等)。

  • ​ERC1155Utils​​:工具库,用于处理接收者校验等逻辑,确保代币不会被发送到无法处理的合约。

  • ​Context​​:提供如 msg.sender等上下文信息的基础合约。

  • ​IERC165 和 ERC165​​:接口检测标准,允许外部工具识别合约实现了哪些功能。

  • ​Arrays​​:数组操作的工具库,简化数组处理。

  • ​IERC1155Errors​​:定义了 ERC1155 相关的错误类型,用于更精确的错误处理。

这些依赖共同构成了 ERC1155 实现的基础,确保了合约的功能完整性、安全性和可扩展性。


三、合约定义与核心概念

1. 合约声明

/*** @dev 这是一个 ERC1155 标准的通用实现,支持同时管理多种代币(同质化和非同质化的混合)。* 比如:一个游戏里有多种道具,每种道具有不同ID和数量,就可以用 ERC1155 来管理。* 官方文档:https://eips.ethereum.org/EIPS/eip-1155* 原始实现参考:Enjin 团队*/
abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IERC1155Errors {// 使用工具库提供的数组便捷方法using Arrays for uint256[];using Arrays for address[];

ERC1155被定义为一个 ​​抽象合约(abstract contract)​​,这意味着它​​不能直接部署​​,而是需要被其他合约​​继承并实现其抽象部分​​。它继承了多个基础合约和接口:

  • ​Context​​:提供如 msg.sender等上下文信息。

  • ​ERC165​​:支持接口检测标准,允许外部工具识别合约实现了哪些功能。

  • ​IERC1155​​ 和 ​​IERC1155MetadataURI​​:定义了 ERC1155 标准的核心功能和元数据相关功能。

  • ​IERC1155Errors​​:定义了该标准相关的错误类型,用于更精确的错误处理。

通过继承这些基础合约和接口,ERC1155实现了标准化、模块化和可扩展的设计,确保了与其他工具和合约的互操作性。

2. 核心数据结构

a. 代币余额映射
    // 核心数据结构1:记录每个代币ID下,每个地址有多少余额// 结构:_balances[代币ID][用户地址] = 余额数量mapping(uint256 id => mapping(address account => uint256)) private _balances;

这是 ERC1155 的​​核心数据结构之一​​,用于记录每个代币 ID 下,每个地址的余额。

  • ​结构​​:_balances[代币ID][用户地址] = 余额数量

  • ​作用​​:通过二维映射,快速查询和更新任意代币 ID 下任意用户的持有数量。

  • ​示例​​:_balances[1]["0x123..."] = 100表示地址 "0x123..."持有代币 ID 为 1 的 100 个单位。

这种设计使得合约能够高效管理大量不同类型的代币,同时为每个用户维护独立的余额记录。

b. 操作者授权映射
    // 核心数据结构2:记录每个用户授权了哪些地址可以操作他的代币// 结构:_operatorApprovals[用户地址][被授权地址] = 是否授权(true/false)mapping(address account => mapping(address operator => bool)) private _operatorApprovals;

这是另一个核心数据结构,用于记录每个用户授权了哪些地址可以操作他的代币。

  • ​结构​​:_operatorApprovals[用户地址][被授权地址] = 是否授权(true/false)

  • ​作用​​:允许用户(owner)授权其他地址(operator)代表其管理所有代币,无需每次交易都单独授权。

  • ​示例​​:_operatorApprovals["0x123..."]["0x456..."] = true表示地址 "0x123..."授权了地址 "0x456..."可以管理其所有代币。

这种批量授权机制极大地提升了交互效率,特别是在需要频繁进行代币管理的场景中,如游戏道具交易、资产管理平台等。

c. 通用元数据 URI
    // 通用元数据URI,可通过字符串替换机制为不同代币ID生成具体URI// 比如:https://example.com/assets/{id}.json,客户端会自动把 {id} 替换成真实代币ID//// 说明:// - 这是一个字符串变量,用于存储 ERC1155 代币的“通用元数据链接模板”// - 该模板通常包含一个占位符 {id},代表代币的唯一编号// - 当用户或前端(比如钱包、市场)想查看某个代币的元数据时(比如图片、名称、属性等),//   它们会根据这个模板,把 {id} 替换为具体的代币ID,然后请求对应的元数据文件// - 例如:代币ID=123,模板是 "https://example.com/assets/{id}.json",//   那么实际请求的元数据链接就是 "https://example.com/assets/123.json"// - 这种设计非常常用,可以极大简化元数据的管理和访问//// 注意:这个变量 _uri 是 private(私有的),只能在本合约内部访问,外部无法直接读取//       但通常会有一个 public 的 uri(uint256 id) 函数来返回它string private _uri;

这是一个​​私有状态变量​​,用于存储 ERC1155 代币的​​通用元数据 URI 模板​​。

  • ​作用​​:提供一个统一的 URI 模板,通常包含一个占位符 {id},客户端(如钱包、市场、前端 DApp)在获取某个代币的元数据时,会将 {id}替换为具体的代币 ID,从而获取该代币的元数据文件(如图片、名称、属性等)。

  • ​示例​​:_uri = "https://example.com/assets/{id}.json",当查询代币 ID 为 123 的元数据时,实际请求的链接将是 "https://example.com/assets/123.json"

​注意​​:这个变量是私有的,只能在本合约内部访问,但通常会通过一个公共的 uri(uint256 id)函数来返回它,允许外部查询。


四、合约功能详解

1. 构造函数:初始化元数据 URI

    /*** @dev 构造函数,在部署合约时设置统一的元数据URI模板* * 作用:这是在部署 ERC1155 合约时**自动调用的构造函数**,它的作用是:* - 在合约一部署的时候,就初始化好一个通用的元数据 URI 模板,比如:*   "https://example.com/assets/{id}.json"* - 这个模板后续会被用于告诉客户端(如钱包、市场、前端)如何获取每个代币的元数据* * 为什么需要这个?* - ERC1155 标准支持“一个合约管理多个代币”,比如 1000 种道具,每种都有独立的元数据* - 但如果为每个代币都单独存一个 URI,会很麻烦* - 所以通常采用“一个模板 + 动态替换 {id}”的方式,极大简化管理和访问* * @param uri_ 元数据的基础URI模板,比如:*             "https://example.com/assets/{id}.json"*             或者 "ipfs://QmXXX/{id}/metadata.json"*             它通常包含一个 {id} 占位符,客户端会将其替换为真实的代币ID*/constructor(string memory uri_) {// 调用内部函数 _setURI(uri_),将传入的 uri_ 字符串保存到本合约的私有状态变量 _uri 中_setURI(uri_);}
  • ​作用​​:在合约部署时调用,用于​​初始化通用的元数据 URI 模板​​。

  • ​参数​​:uri_,例如 "https://example.com/assets/{id}.json",通常包含 {id}占位符,用于动态替换为具体代币 ID。

  • ​实现​​:调用内部函数 _setURI(uri_),将传入的 URI 模板保存到私有状态变量 _uri中。

​为什么需要这个?​

在 ERC1155 标准中,一个合约可以管理​​成百上千种不同的代币​​。如果为每个代币单独存储一个 URI,不仅繁琐而且低效。通过采用​​一个统一的 URI 模板 + 动态替换 {id}​​ 的方式,可以极大简化元数据的管理和访问,提升系统的可扩展性和维护性。


2. ERC165 接口检测:支持哪些标准?

    // ========== ERC165 接口检测 ==========/*** @dev 检查本合约是否支持某个接口,ERC1155 和 ERC1155MetadataURI 是必须支持的* * 作用:这是一个标准的 ERC165 接口检测函数,用于告诉外部调用者(比如钱包、DApp、市场等):* “本合约支持哪些接口?”* * 在以太坊和智能合约生态中,**接口检测是非常重要的机制**,它让外部工具(如 OpenSea、MetaMask、钱包等)* 能够识别这个合约实现了哪些标准功能(比如 ERC1155、ERC721、ERC20 等),从而决定如何与它交互。* * 举个例子:OpenSea 会先调用这个函数,检测你是不是一个 ERC1155 合约,如果是,才会按 NFT 集合的方式展示你的代币。* * @param interfaceId 要检测的接口的唯一标识符(一个 4 字节的 bytes4 值,每个标准接口都有固定的ID)* @return bool 是否支持该接口(true = 支持,false = 不支持)*/function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {returninterfaceId == type(IERC1155).interfaceId ||                     // ✅ 检查是否支持 ERC1155 标准主接口interfaceId == type(IERC1155MetadataURI).interfaceId ||          // ✅ 检查是否支持 ERC1155MetadataURI 扩展接口(提供元数据URI)super.supportsInterface(interfaceId);                            // 🔁 其它接口检测交给父类(比如 ERC165)处理}
  • ​作用​​:这是一个标准的 ERC165 接口检测函数,用于告诉外部调用者(如钱包、DApp、市场等)本合约支持哪些接口。

  • ​返回值​​:true表示支持该接口,false表示不支持。

  • ​检测的接口​​:

    • ​IERC1155​​:ERC1155 标准的主接口,定义了核心功能。

    • ​IERC1155MetadataURI​​:扩展接口,支持通过 URI 获取代币元数据。

    • ​其他接口​​:通过 super.supportsInterface(interfaceId)将其他接口检测交给父类(如 ERC165)处理。

​为什么重要?​

在以太坊生态中,​​接口检测是外部工具识别合约功能的关键机制​​。例如,OpenSea 等市场会先调用这个函数,检测你的合约是否实现了 ERC1155 标准,如果是,才会按照 NFT 集合的方式展示你的代币。这使得你的合约能够与各种工具和平台无缝集成,提升互操作性和用户体验。


3. 元数据相关:获取代币的 URI

// ========== 元数据相关 ==========/*** @dev 返回某个代币ID的元数据URI,这里返回的是通用模板,实际使用时客户端要替换 {id}* * 作用:这是一个标准的 ERC1155 可选函数,用于返回某个代币ID(比如 1、2、3...)对应的元数据文件的访问链接(URI)。* 在这个实现中,我们采取了一种常见的、简化的设计:* - 不为每个代币单独设置一个 URI,而是返回一个**统一的 URI 模板字符串**,比如:*   "https://example.com/assets/{id}.json"* - 这个模板中包含一个占位符 `{id}`,它代表代币的唯一编号* - 客户端(比如钱包、OpenSea、前端 DApp)在拿到这个 URI 后,会**自动把 {id} 替换成真实的代币ID**,*   从而得到每个代币真正对应的元数据链接,比如:*     - 代币ID=1 → "https://example.com/assets/1.json"*     - 代币ID=2 → "https://example.com/assets/2.json"* * ⚠️ 注意:这个函数的参数 `id` 虽然传进来了,但在本实现中我们并没有使用它!*      因为我们是返回一个通用模板,而不是为每个 id 单独返回不同的 URI。*      如果你想要为每个代币定制不同的 URI,你需要在这里根据 id 返回不同的字符串。* * @param id 代币的唯一标识符(比如 1、2、3...)。在这个实现中不会用到,所以用注释标记忽略了它* @return 返回一个字符串,表示该代币的元数据 URI 模板(比如包含 {id} 的链接地址)*/function uri(uint256 /* id */) public view virtual returns (string memory) {return _uri; // 直接返回合约中存储的通用元数据 URI 模板,通常是在构造函数中通过 _setURI(...) 设置的}
  • ​作用​​:返回某个代币 ID 的元数据 URI。在这个实现中,返回的是​​通用模板​​,而不是为每个代币单独设置的 URI。

  • ​参数​​:id,代币的唯一标识符。​​注意​​:在这个实现中,id参数并未实际使用,因为返回的是统一模板。

  • ​返回值​​:存储在私有变量 _uri中的通用元数据 URI 模板,如 "https://example.com/assets/{id}.json"

​如何实现个性化 URI?​

虽然这个实现返回的是统一模板,但如果需要为每个代币定制不同的 URI,可以在该函数中根据 id参数返回不同的字符串。例如:

function uri(uint256 id) public view virtual override returns (string memory) {return string(abi.encodePacked("https://example.com/assets/", Strings.toString(id), ".json"));
}

这样,每个代币 ID 都会有一个独特的 URI,指向其专属的元数据文件。


4. 余额查询:查看代币持有量

a. 单个余额查询
    // ========== 余额查询 ==========/*** @dev 查询某个地址拥有某个代币ID的数量* @param account 用户地址* @param id 代币ID* @return 该用户拥有该代币的数量*/function balanceOf(address account, uint256 id) public view virtual returns (uint256) {return _balances[id][account]; // 直接从映射中读取余额}
  • ​作用​​:查询某个地址 (account) 拥有某个代币 ID (id) 的数量。

  • ​参数​​:

    • account:用户地址。

    • id:代币 ID。

  • ​返回值​​:该用户在该代币上的余额数量。

​实现原理​​:直接从二维映射 _balances[id][account]中读取余额值,简单高效。

b. 批量余额查询
    /*** @dev 批量查询多个地址的对应代币ID的余额* * 作用:一次性查询多个用户各自拥有的对应代币的余额,避免多次调用 balanceOf,提升效率,尤其在游戏、批量展示用户资产等场景非常有用。* * @param accounts 地址数组,比如 [Alice, Bob, Carol],表示你想查这几个用户的余额* @param ids 代币ID数组,比如 [1, 2, 3],表示你想查每个用户对应代币ID的余额* @return batchBalances 每个地址对应每个代币的余额数组,长度与输入数组一致* * 要求:accounts 和 ids 两个数组的长度必须相等,比如你提供 3 个用户,就得同时提供 3 个代币ID,否则会报错!* * 举个简单例子:* 假设:* - accounts = [addr1, addr2,addr3] (两个用户)* - ids = [101, 101,102]         (两种代币ID)* * 那么函数会返回一个长度为 3 的数组 batchBalances ,其中:* - 第 1 个元素是 addr1 拥有的代币 101 的余额* - 第 2 个元素是 addr2 拥有的代币 101 的余额* - 第 3 个元素是 addr3 拥有的代币 102 的余额* 可以理解为:返回数组的第 i 个元素,代表第 i 个用户(accounts[i])拥有的第 i 个代币(ids[i])的余额。* */function balanceOfBatch(address[] memory accounts,uint256[] memory ids) public view virtual returns (uint256[] memory) {// 先检查输入的两个数组长度是否一致,比如你不能传 3 个地址但只传 2 个代币IDif (accounts.length != ids.length) {// 如果不一致,抛出自定义错误,提示调用者两个数组长度不同revert ERC1155InvalidArrayLength(ids.length, accounts.length); // 长度不一致则报错}// 创建一个用于返回的数组,长度和输入一样,比如传了 3 个地址就返回 3 个余额结果uint256[] memory batchBalances = new uint256[](accounts.length);// 循环遍历每一个地址和对应的代币ID,逐个查询余额for (uint256 i = 0; i < accounts.length; ++i) {// 从输入的数组中按索引取出第 i 个地址 和 第 i 个代币ID// 注意:这里用了 unsafeMemoryAccess,是一种高效但不那么安全的取值方式(在数组访问安全可控时使用)// 一般可以理解为:accounts[i] 和 ids[i]// 然后调用基础的 balanceOf 方法,查询这个地址 拥有 这个代币ID 的余额是多少batchBalances[i] = balanceOf(accounts.unsafeMemoryAccess(i), ids.unsafeMemoryAccess(i));}// 最终返回一个数组,里面按顺序放着每个地址-代币ID 对应的余额return batchBalances;}
  • ​作用​​:​​一次性查询多个地址各自拥有的对应代币的余额​​,提升效率,特别适用于游戏、批量展示用户资产等场景。

  • ​参数​​:

    • accounts:地址数组,如 ["0x123...", "0x456..."]

    • ids:代币 ID 数组,如 [1, 2]

  • ​返回值​​:一个 uint256[]数组,包含每个地址对应每个代币的余额,顺序与输入数组一致。

  • ​要求​​:accountsids两个数组的长度必须相等,否则会抛出 ERC1155InvalidArrayLength错误。

​实现细节​​:

  • 首先检查两个输入数组的长度是否一致,若不一致则回滚交易并抛出错误。<

http://www.dtcms.com/a/364109.html

相关文章:

  • 网络通信IP细节
  • 【Vue】前端 vue2项目搭建入门级(二)
  • 嵌入式概述 与 51 单片机
  • 【单片机day01】
  • 第二章:技术基石:写出“活”的代码(1)
  • 什么时候需要使用虚继承,什么是菱形继承
  • HI3519DRFCV500/HI3519DV500海思核心板IPC算力2.5T图像ISP超高清智能视觉应用提供SDK软件开发包
  • 平衡车-ADC采集电池电压
  • 从 Arm Compiler 5 迁移到 Arm Compiler 6
  • HandyControl 解决不全局引入控件部分内容不显示问题
  • 论文学习30:LViT: Language Meets Vision Transformerin Medical Image Segmentation
  • 给大模型开卷考试的机会——写给开发者的 RAG 技术入门
  • 2025年女性最实用的IT行业证书推荐:赋能职业发展的8大选择
  • Shell编程从入门到实践:基础语法与正则表达式文本处理指南
  • RPM 构建错误: /var/tmp/rpm-tmp.gAmM5N (%prep) 退出状态不好,怎么办
  • HBuilder X 4.76 开发微信小程序集成 uview-plus
  • 关于IDE的相关知识之一【使用技巧】
  • GFSK信号生成算法原理详解
  • 避免侵权!这6个可免费下载字体网站能放心商用
  • 「数据获取」《安徽建设统计年鉴》(2002-2007)(2004、2006缺失)(获取方式看绑定的资源)
  • 【世纪龙科技】汽车专业数字化课程资源包-虚拟仿真实训资源建设
  • MYSQL配置复制拓扑知识点
  • 告别集成烦恼!H-ZERO iframe 支持第三方系统 / AI 助手轻松接入
  • 【机器学习入门】5.3 线性回归原理——从模型定义到参数求解,手把手带练
  • 模型常见训练超参数介绍(1)
  • Vue.js 中深度选择器的区别与应用指南
  • Corrosion: 1靶场渗透
  • 新手也能轻松选!秒出PPT和豆包AI PPT优缺点解析
  • 自学嵌入式第三十三天:网络编程-UDP
  • SpringMVC的RequestMapping注解与请求参数绑定