WTF Solidity 极简教程:第 40 讲 ERC1155 多代币标准与 BAYC1155 实战
2026/9/14 23:26:43 网站建设 项目流程

WTF Solidity 极简教程:第 40 讲 ERC1155 多代币标准与 BAYC1155 实战

【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity

导读

本文基于 WTF-Solidity 仓库第 40 讲的内容,系统讲解以太坊EIP-1155提出的多代币标准ERC1155:一个合约如何同时容纳同质化与非同质化的多种代币,以及它的接口、事件、函数、接收合约与完整实现。文中还给出了仓库中的实战案例BAYC1155——一个包含 10,000 种代币、元数据与 BAYC 完全一致的免费铸造合约,并演示了在 Remix 中的部署与铸造全流程。读完本文,你将掌握ERC1155的核心设计、IERC1155/IERC1155Receiver接口的每一个函数与事件,并能基于仓库源码独立实现一个多代币合约。

EIP1155:为什么需要多代币标准

ERC20ERC721标准都有一个共同特点:一个合约只对应一种代币。设想在以太坊上做一款类似《魔兽世界》的大型游戏,每一件装备都需要部署一个合约,数千件装备就意味着数千个合约,部署与管理工作极其繁琐。为此,Ethereum EIP1155 提出了多代币标准ERC1155,允许一个合约包含多种同质化与非同质化代币ERC1155在 GameFi 领域被广泛使用,Decentraland、The Sandbox 等知名链游均采用该标准。

简单类比之前介绍的 ERC721:

  • ERC721中,每个tokenId是唯一标识,一个tokenId只对应一个代币;
  • ERC1155中,每个种类的代币用id作为唯一标识,一个id对应一种代币。

这样,多种代币可以在同一个合约中异构管理,且每种代币都有一个 URLuri用于存放元数据,类似于ERC721中的tokenURI。下面是ERC1155的元数据接口合约IERC1155MetadataURI(完整源码见 IERC1155MetadataURI.sol):

/** * @dev 可选的 ERC1155 接口,加入 uri() 函数查询元数据 */ interface IERC1155MetadataURI is IERC1155 { /** * @dev 返回第 `id` 种类代币的 URI */ function uri(uint256 id) external view returns (string memory); }

如何判断ERC1155中某一种代币是同质化还是非同质化?规则很简单:

  • 某个id对应代币的总量为 1,则为非同质化代币,类似ERC721
  • 某个id对应代币的总量大于 1,则为同质化代币,因为这些代币共享同一个id,类似ERC20

IERC1155接口合约

IERC1155接口合约抽象了EIP1155实现所需的功能,包含4 个事件6 个函数。与ERC721不同,由于ERC1155包含多种代币,它实现了批量转账批量余额查询,可同时对多种代币进行操作。完整源码见 IERC1155.sol(仓库中英文版为 Languages/en/40_ERC1155_en/IERC1155.sol)。

// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../34_ERC721_en/IERC165.sol"; /** * @dev ERC1155 标准接口合约,实现 EIP1155 的功能 * 详见:https://eips.ethereum.org/EIPS/eip-1155[EIP]. */ interface IERC1155 is IERC165 { /** * @dev 单类代币转账事件 * 当 `value` 个 `id` 种类代币被 `operator` 从 `from` 转账到 `to` 时释放 */ event TransferSingle( address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value ); /** * @dev 多类代币转账事件 * ids 和 values 为转账的代币种类和数量数组 */ event TransferBatch( address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values ); /** * @dev 批量授权事件 * 当 `account` 将所有代币授权给 `operator` 时释放 */ event ApprovalForAll( address indexed account, address indexed operator, bool approved ); /** * @dev 当 `id` 种类代币的 URI 发生变化时释放,`value` 为新的 URI */ event URI(string value, uint256 indexed id); /** * @dev 持仓查询,返回 `account` 拥有的 `id` 种类代币的持仓量 */ function balanceOf(address account, uint256 id) external view returns (uint256); /** * @dev 批量持仓查询,`accounts` 和 `ids` 数组长度需相等 */ function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory); /** * @dev 批量授权,将调用者的代币授权给 `operator` 地址,释放 {ApprovalForAll} 事件 */ function setApprovalForAll(address operator, bool approved) external; /** * @dev 批量授权查询,如果授权地址 `operator` 被 `account` 授权则返回 `true` */ function isApprovedForAll(address account, address operator) external view returns (bool); /** * @dev 安全转账,将 `amount` 单位 `id` 种类代币从 `from` 转账给 `to`,释放 {TransferSingle} 事件 * 要求: * - 如果调用者不是 `from` 地址而是授权地址,则需要得到 `from` 的授权 * - `from` 地址必须有足够持仓 * - 如果接收方是合约,需要实现 `IERC1155Receiver` 的 `onERC1155Received` 方法并返回相应值 */ function safeTransferFrom( address from, address to, uint256 id, uint256 amount, bytes calldata data ) external; /** * @dev 批量安全转账,释放 {TransferBatch} 事件 * 要求: * - `ids` 和 `amounts` 长度相等 * - 如果接收方是合约,需要实现 `IERC1155Receiver` 的 `onERC1155BatchReceived` 方法并返回相应值 */ function safeBatchTransferFrom( address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data ) external; }

注:仓库根目录下的 40_ERC1155/IERC1155.sol 使用^0.8.34编译指令,并直接import "../34_ERC721/IERC165.sol";英文教程版 Languages/en/40_ERC1155_en/IERC1155.sol 则从34_ERC721_en目录导入IERC165,二者接口定义完全一致。

IERC1155的 4 个事件

  • TransferSingle事件:单类代币转账时释放,记录operatorfromtoidvalue
  • TransferBatch事件:多类代币转账时释放,idsvalues为代币种类和数量数组。
  • ApprovalForAll事件:批量授权时释放,记录授权人account、被授权人operator与授权状态approved
  • URI事件:元数据地址uri变化时释放,value为新 URI,id为对应代币种类。

IERC1155的 6 个函数

  • balanceOf(account, id):查询accountid种类代币的持仓量。
  • balanceOfBatch(accounts, ids):批量查询持仓量,accountsids数组长度必须相等,返回持仓量数组。
  • setApprovalForAll(operator, approved):调用者将全部代币授权给operator,即"全量授权"。
  • isApprovedForAll(account, operator):查询operator是否被account授权。
  • safeTransferFrom(from, to, id, amount, data):单类代币安全转账。若to是合约,必须实现onERC1155Received()函数。
  • safeBatchTransferFrom(from, to, ids, amounts, data):多类代币安全转账,amountsids是长度相等的数组。若to是合约,必须实现onERC1155BatchReceived()函数。

ERC1155接收合约

ERC721类似,为了防止代币被转入"黑洞合约",ERC1155要求代币接收合约继承IERC1155Receiver并实现两个接收函数(完整源码见 IERC1155Receiver.sol):

  • onERC1155Received():单类代币转账的接收函数,必须返回选择器0xf23a6e61(即bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")))。
  • onERC1155BatchReceived():多类代币转账的接收函数,必须返回选择器0xbc197c81(即bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))),才能通过safeBatchTransferFrom接受ERC1155安全批量转账。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../34_ERC721_en/IERC165.sol"; /** * @dev ERC1155 接收合约,要接受 ERC1155 的安全转账需要实现这个合约 */ interface IERC1155Receiver is IERC165 { /** * @dev 接受 ERC1155 安全转账 `safeTransferFrom` * 需要返回 0xf23a6e61 或 `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` */ function onERC1155Received( address operator, address from, uint256 id, uint256 value, bytes calldata data ) external returns (bytes4); /** * @dev 接受 ERC1155 批量安全转账 `safeBatchTransferFrom` * 需要返回 0xbc197c81 或 `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); }

主合约ERC1155的实现

主合约 ERC1155.sol 实现了IERC1155接口合约规定的函数,并额外提供了单类/多类代币的铸造与销毁函数。

ERC1155的 4 个状态变量

  • name:代币名称。
  • symbol:代币代号。
  • _balances:代币持仓映射,记录地址accountid种类代币的持仓量balances,结构为mapping(uint256 => mapping(address => uint256))
  • _operatorApprovals:批量授权映射,记录持有人地址对另一地址的授权情况,结构为mapping(address => mapping(address => bool))

ERC1155的 16 个函数

函数作用
构造函数初始化状态变量namesymbol
supportsInterface()实现ERC165标准,声明合约支持的接口,供其他合约查询
balanceOf()实现IERC1155balanceOf(),与ERC721不同,需要同时传入查询地址account和代币id
balanceOfBatch()实现balanceOfBatch(),批量查询代币持仓
setApprovalForAll()实现setApprovalForAll(),批量授权并释放ApprovalForAll事件
isApprovedForAll()实现isApprovedForAll(),批量查询授权信息
safeTransferFrom()实现safeTransferFrom(),单类代币安全转账并释放TransferSingle事件;与ERC721不同,除fromtoid外还需要转账数量amount
safeBatchTransferFrom()实现safeBatchTransferFrom(),多类代币安全转账并释放TransferBatch事件
_mint()单类代币铸造函数
_mintBatch()多类代币铸造函数
_burn()单类代币销毁函数
_burnBatch()多类代币销毁函数
_doSafeTransferAcceptanceCheck()单类转账安全检查,由safeTransferFrom()调用,确保接收方为合约时实现了onERC1155Received()
_doSafeBatchTransferAcceptanceCheck()多类转账安全检查,由safeBatchTransferFrom()调用,确保接收方为合约时实现了onERC1155BatchReceived()
uri()返回id种类代币的元数据 URL,类似ERC721tokenURI
baseURI()返回baseURIuribaseURI拼接id,可由开发者重写

核心实现要点解析

1. 批量余额查询balanceOfBatch():先校验accountsids数组长度相等,否则revert("ERC1155: accounts and ids length mismatch");随后用 for 循环逐对调用balanceOf()填充返回数组。

2. 安全转账safeTransferFrom():核心校验链为"调用者身份 → 零地址 → 余额充足"三连判:

  • 调用者必须是持有人或被授权人:from == operator || isApprovedForAll(from, operator)
  • to不能是零地址;
  • _balances[id][from] >= amount余额必须充足。

余额扣减使用unchecked块防止无意义溢出检查,最后释放TransferSingle事件并执行安全接收检查。safeBatchTransferFrom()则是同样的逻辑在 for 循环中处理多组id/amount,最后释放TransferBatch事件。

3. 铸造与销毁_mint()amount记入to地址的_balances[id][to],以from = address(0)释放TransferSingle事件;_burn()反向操作,扣减余额并以to = address(0)释放事件。批量版本_mintBatch()/_burnBatch()通过 for 循环批量更新并释放TransferBatch事件。全部四个函数均为internal,对外暴露铸造/销毁入口由子合约自行封装。

4. 安全接收检查:这是ERC1155防"黑洞合约"的关键。_doSafeTransferAcceptanceCheck()先通过to.code.length > 0判断接收方是否为合约,若是则以try/catch调用onERC1155Received(),并要求返回值等于函数选择器IERC1155Receiver.onERC1155Received.selector(即0xf23a6e61),否则revert ERC1155InvalidReceiver(to)(仓库源码 40_ERC1155/ERC1155.sol 定义了错误类型error ERC1155InvalidReceiver(address receiver)并给出空 reason 的兜底处理)。批量检查_doSafeBatchTransferAcceptanceCheck()逻辑相同,只是改为校验onERC1155BatchReceived的选择器0xbc197c81

5. 元数据uri()uri(id)返回_baseURI()拼接id.toString(),其中toString来自 34_ERC721/String.sol 中的Strings库(英文版从34_ERC721_en导入)。默认_baseURI()返回空字符串,需要子合约重写。

6. 接口声明supportsInterface()ERC1155同时继承IERC165IERC1155IERC1155MetadataURIsupportsInterface()逐一比对三个接口的type(...).interfaceId,其他合约可通过 34_ERC721/IERC165.sol 中的supportsInterface()查询其能力。

实战:把 BAYC 改造成ERC1155——BAYC1155

仓库在 40_ERC1155/BAYC1155.sol(英文版 Languages/en/40_ERC1155_en/BAYC1155.sol)中提供了一个实战合约BAYC1155:对无聊猿 BAYC 稍作修改,改为遵循ERC1155标准并支持免费铸造。其关键设计是重写_baseURI(),使BAYC1155uri与 BAYC 的tokenURI相同,从而让BAYC1155的元数据与无聊猿完全一致。

// SPDX-License-Identifier: MIT // by 0xAA pragma solidity ^0.8.21; import "./ERC1155.sol"; contract BAYC1155 is ERC1155 { uint256 constant MAX_ID = 10000; // 构造函数 constructor() ERC1155("BAYC1155", "BAYC1155") {} // BAYC 的 baseURI 为 ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ function _baseURI() internal pure override returns (string memory) { return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/"; } // 铸造函数 function mint(address to, uint256 id, uint256 amount) external { // id 不能超过 10,000 require(id < MAX_ID, "id overflow"); _mint(to, id, amount, ""); } // 批量铸造函数 function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts) external { // id 不能超过 10,000 for (uint256 i = 0; i < ids.length; i++) { require(ids[i] < MAX_ID, "id overflow"); } _mintBatch(to, ids, amounts, ""); } }

BAYC1155的要点:

  • 常量MAX_ID = 10000,限定了代币种类id的范围(09999,共 10,000 种),mintmintBatch均校验id < MAX_ID,超限即revert("id overflow")
  • _baseURI()返回 BAYC 官方 IPFS 地址,配合基类uri()baseURI + id.toString()的拼接逻辑,uri(0)uri(1)……即对应 BAYC 编号 0、1…… 的元数据;
  • 铸造入口对外公开,任何人可免费铸造任意种类的任意数量——这正演示了ERC1155中"同一种类数量为 1 则是 NFT、数量大于 1 则是 FT"的灵活性。

注意:仓库根目录源码 40_ERC1155/BAYC1155.sol 使用pragma solidity ^0.8.34,教程正文中的示例为^0.8.21,二者逻辑一致,编译时以仓库实际文件为准。

Remix 实战演示

以下操作均在 Remix IDE 中进行(若需本地体验,可通过git clone拉取本仓库后使用 Foundry/Remix 打开 40_ERC1155/BAYC1155.sol)。

1. 部署BAYC1155合约

在 Remix 编译BAYC1155.sol后,于 "DEPLOY & RUN TRANSACTIONS" 面板选择合约并点击Deploy即可完成部署,合约继承的mintmintBatch等函数会一并出现在交互面板中。

2. 查看元数据URI

部署后调用uri(id)函数,输入某个id(如0),即可查看到由 BAYC 的 IPFS baseURI 与id拼接而成的元数据地址,验证与 BAYC 元数据一致。

3.mint单类铸造并查看持仓变化

mint区域输入账户地址、id与数量后点击mint按钮铸造。如果数量为1,该id即非同质化代币;如果数量大于1,则该id是同质化代币

balanceOf区域输入账户地址与id,即可查看对应持仓数量,验证铸造结果。

4. 批量mint并查看持仓变化

mintBatch区域输入ids数组与对应铸造数量数组,两个数组长度必须相同(否则触发ERC1155: ids and amounts length mismatch)。输入多个id与其对应数量(例如[1,2,3,4][99,9,1,1])后执行批量铸造。

随后在balanceOfBatch中输入账户地址数组与ids数组,即可一次性查看多个种类的持仓变化。

5. 转账与接收合约校验

safeBatchTransferFrom(或safeTransferFrom)区域,从已持有代币的地址向新地址转账,输入ids与对应amounts数组(长度必须相等)。目标地址可以是普通地址,也可以是合约地址:

  • 若目标为普通地址,转账直接成功;
  • 若目标为合约地址,合约会调用_doSafeBatchTransferAcceptanceCheck(),验证其是否实现了onERC1155Received()/onERC1155BatchReceived()并返回正确选择器,未实现则revert,从而防止代币被打入黑洞合约。

转账后可在balanceOf/balanceOfBatch中查看接收地址持仓变化。

小结

本讲学习了以太坊EIP1155提出的ERC1155多代币标准:它允许一个合约包含多种同质化或非同质化代币,通过"id对应一种代币、数量为 1 即 NFT、大于 1 即 FT"的统一抽象,显著降低了多资产场景(尤其是 GameFi)的合约部署与管理成本。同时,我们基于仓库源码创建了 BAYC 的ERC1155改造版BAYC1155——包含 10,000 种、元数据与 BAYC 完全一致、支持免费铸造的代币合约。目前ERC1155主要应用于 GameFi,随着元宇宙技术的发展,这一标准有望获得更广泛的应用。

【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询