欢迎光临
我们一直在努力

【技术详解】 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) {
return
interfaceId == 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 个代币ID
if (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[]数组,包含每个地址对应每个代币的余额,顺序与输入数组一致。

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

​​实现细节​​:

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

赞(0)
未经允许不得转载:171主机测评 » 【技术详解】 OpenZeppelin ERC1155:Solidity 多代币标准实现原理(附完整 Solidity 源码)​
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址