Ethereum / ERC standards / Lesson 15.03

一份合约,怎样装下整个资产世界

金币、药水、门票和唯一的传奇武器,看起来是四种完全不同的资产。 ERC-1155 的关键不是把它们变成同一种东西,而是给同一份账本增加 token id 这一维,再让查询、转移、授权和事件都认识这两个坐标。

  • 正式目录:ERC-1155,在同一合约中管理多种资产
  • 约 45 分钟
  • 4 个互动模型
  • 6 题自检
01 / ID IS A TYPE

token id 标识资产类型,amount 表示这种类型的数量。身份不再只由合约地址决定。

02 / BATCH IS NATIVE

多个 id 可以在一次标准调用中一起查询或转移。节省来自共享合约与批处理,不是神奇的固定折扣。

03 / POLICY DEFINES MEANING

标准提供容器和动作,不替你定义稀缺性。铸造权限、供应上限、元数据与升级权仍要逐项审计。

01 / THE PROBLEM

先想象一个世界,不要先背接口

一款游戏需要货币、消耗品、季票与唯一装备;真正的问题是资产类型太多

如果一个应用只发行一种可互换货币,ERC-20 很自然;如果它只管理一组逐件唯一的收藏品, ERC-721 很自然。可现实系统经常同时需要许多种资产,而且同一种资产还可能有许多份。

ID 1 / FUNGIBLE 世界金币

每一单位用途相同,Alice 的第 1 枚和第 100 枚无需区分。

ID 2 / FUNGIBLE 治疗药水

可堆叠、可消耗;余额是“还有几瓶”,不是“拥有哪几瓶”。

ID 7 / LIFECYCLE 赛季通行证

发行时多份相同,进入赛季后可兑换或销毁,语义随生命周期变化。

ID 2048 / UNIQUE POLICY 晨星遗物

应用承诺该 id 永远只发行 1 份,因此这一类型被当作唯一资产。

用 ERC-20 处理前两种资产,通常要为每种代币部署或维护独立合约;用 ERC-721 处理最后一种资产,token id 标识具体物品,但没有“同一 id 有 100 份”的余额概念。 ERC-1155 的提案正是为“一份系统里有很多 token type”设计:一份合约可以管理任意数量的 token id,每个 id 有自己的余额、供应和元数据。[1]

完整资产类型 = chain id + contract address + token id

合约地址回答“哪一本总账”,token id 回答“总账里的哪一种资产”,amount 回答“这个账户有多少”。

02 / THE DATA MODEL

ERC-20 是一列余额,ERC-1155 是一张表

从 balance[account] 增加 token id 这一维,得到 balance[id][account]

理解 ERC-1155 最重要的一行,不在转移函数,而在余额模型: 同一个账户对不同 id 分别拥有一个无符号整数余额。

balanceOf(account, id) → balances[id][account]

这是一种帮助理解接口的逻辑模型,并非规范强制的 Solidity 存储布局;具体实现可以不同。

这张表解释了接口差异。ERC-20 的 balanceOf(account) 默认大家都在问同一种代币; ERC-1155 必须再传一个 id。ERC-721 的 ownerOf(tokenId) 直接问某件 唯一物品归谁;ERC-1155 则问“某账户拥有某类型多少份”。[2] [3]

Interactive model 01

同一个数据结构,为什么能表达三种资产?

切换资产,观察供应与账户分布。分类来自发行策略,不是 id 本身。

Fungible by policy

世界金币 · ID 1

总量很大;每一单位具有相同权利,账户只关心数量,不区分具体哪一枚。

Alice 12,500
Bob 2,100
Vault 640,000

“供应量为 1”为什么还不够证明它永远是 NFT?

当前某个 id 的总供应量是 1,只能证明“现在观察到一份”。如果管理员明天还能对同一 id 再次 mint,它就不再具有永久唯一性。OpenZeppelin 的 ERC1155Supply 也明确提醒:totalSupply(id) == 1 可能意味着 NFT,但并不保证未来不会继续铸造。 [8]

03 / THREE STANDARDS

不要问谁“更高级”,要问你的资产如何被计数

ERC-20、ERC-721、ERC-1155 是不同的接口与数据模型,不是升级替代关系

选择标准不是评奖。稳定币通常需要整个 DeFi 生态熟悉的 ERC-20 接口; 逐件唯一、按单件授权和转移的收藏品常适合 ERC-721;大量异构资产共存、需要原生批处理时, ERC-1155 的二维模型更自然。

问题 ERC-20 ERC-721 ERC-1155
一份合约主要表达 一种同质化代币 一组逐件唯一的 token 任意多个 token type
核心余额问题 账户有多少 某件归谁;账户有几件 账户对某 id 有多少
身份坐标 合约地址 合约地址 + token id 合约地址 + token id
标准原生批量转移 没有 没有 safeBatchTransferFrom
第三方操作授权 常见按 spender + amount 单件授权或 operator for all 同一合约内 operator for all
合约接收保护 核心标准没有接收回调 safe 转移时回调 所有标准转移都是 safe 风格

04 / THE INTERFACE

核心接口只做四件事:查、转、授权、留证

读取余额,安全转移,设置 operator,发出足以重建余额的事件

ERC-1155 的力量来自一组很小但组合良好的接口。注意:标准定义的是外部可交互行为, 并不规定“谁能铸造”“最大供应是多少”或“能否暂停”。

Read / 查询

  • balanceOf(account, id)一个坐标
  • balanceOfBatch(accounts, ids)多组坐标
  • isApprovedForAll(owner, operator)授权状态
  • supportsInterface(interfaceId)能力检测

Write / 转移与授权

  • safeTransferFrom(...)一个 id
  • safeBatchTransferFrom(...)多个 id
  • setApprovalForAll(operator, bool)全量 operator

Logs / 状态留证

  • TransferSingle单类型变化
  • TransferBatch多类型变化
  • ApprovalForAll授权变化
  • URI元数据地址变化

Optional / 扩展

  • uri(id)metadata URI
  • totalSupply(id)常见库扩展
  • exists(id)常见库扩展
  • burn(...)应用自行暴露

两个 Batch 函数都不是“随便塞一堆东西”

balanceOfBatch(accounts, ids) 按位置配对:结果第 i 项是 balanceOf(accounts[i], ids[i]),不是每个账户与每个 id 的笛卡尔积。 safeBatchTransferFrom(from, to, ids, values, data) 则把 ids[i]values[i] 配对,并且整个批次只有同一个 from 与同一个 to;两组数组长度必须一致。 [1]

interface IERC1155 is IERC165 {
    function balanceOf(address account, uint256 id)
        external view returns (uint256);

    function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids)
        external view returns (uint256[] memory);

    function safeTransferFrom(
        address from, address to, uint256 id, uint256 value, bytes calldata data
    ) external;

    function safeBatchTransferFrom(
        address from, address to,
        uint256[] calldata ids, uint256[] calldata values, bytes calldata data
    ) external;

    function setApprovalForAll(address operator, bool approved) external;
    function isApprovedForAll(address account, address operator)
        external view returns (bool);
}
0xd9b67a26 ERC-1155 核心接口
0x0e89341c 可选 Metadata URI 扩展
0x4e2312e0 Receiver 接收接口

ERC-1155 要求通过 ERC-165 报告核心接口支持,即 supportsInterface(0xd9b67a26) == true。这让钱包或合约先询问 “你会不会说 ERC-1155”,再选择正确的交互方式。[4]

05 / NATIVE BATCHING

批量的价值,不是“少写几行”,而是一次原子动作

同一发送者、同一接收者、多个 id 与 amount 配对进入一笔调用

Alice 把金币、药水、季票和遗物一起交给一个市场托管合约。若资产分散在不同标准合约中, 通常需要多次 token-level 转移调用;在同一 ERC-1155 合约里,可以用一次 safeBatchTransferFrom 表达整个包。

Interactive model 02

组一个资产包,看标准原生调用如何变化

这是调用结构模型,不是 Gas 报价器;实际费用取决于实现、状态、接收方与执行环境。

Alice wallet FROM

金币 ×120 药水 ×3 季票 ×1 遗物 ×1

Market escrow TO

ID 1 / 120 ID 2 / 3 ID 7 / 1 ID 2048 / 1
选中 token type 4 数组中 id/value 的配对数量
标准原生转移调用 1 safeBatchTransferFrom 一次
原子结果 ALL / REVERT 直接批量调用中,任一失败会回滚整笔交易

为什么批量通常更高效,但不能写成“永远省 X%”?

多个 token type 共用一份合约,减少重复部署的字节码;批量转移还共享一次交易基础开销、 一次外部入口,并在一组循环中更新余额;OpenZeppelin 等常见实现通常共享一次批量回调。EIP-1155 因此说: 与为多个 token type 发送多笔单独交易相比,批量转移可以降低交易成本。 [1]

但费用不是标准常数。单一 id 转移时,safeTransferFrom 可能比构造长度为 1 的 batch 更合适;自定义存储、重复 id、接收方逻辑、冷/热存储访问、L1 或 L2 的费用结构都会改变结果。 正确说法是“ERC-1155 提供原生聚合机会”,不是“ERC-1155 每次都最便宜”。

06 / SAFE RECEIPT

“安全转移”不是检查人,而是询问合约:你会接吗?

接收者若是合约,必须返回规定的 magic value;否则标准转移回滚

把 token 发给一个不会处理它们的合约,资产可能永久卡住。ERC-1155 因此只有 safe 风格的 标准转移:若接收地址当前有代码,token 合约要调用接收钩子并验证明确的返回值。

单类型接收函数是 onERC1155Received,接受值为 0xf23a6e61;批量接收函数是 onERC1155BatchReceived, 接受值为 0xbc197c81。接收方也可以主动 revert 表示拒绝。 规范允许一次批量钩子,也允许以多次单项钩子覆盖所有余额变化;这里的互动模型展示前一种常见实现。 [1]

Interactive model 03

改变接收方回答,看整笔交易的结局

余额更新与事件先发生,但只要后续回调拒绝,EVM 会把前面的状态和日志一起回滚。

function onERC1155BatchReceived(...)
external returns (bytes4) {
  return 0xbc197c81;
}
转移完成

接收合约明确接受;余额更新与 TransferBatch 日志成为交易结果的一部分。

为什么“先更新、再回调”仍然要警惕重入?

回调是一次外部合约调用。接收合约在回调期间可能再次调用 token 合约或调用你系统里的其他函数。 标准要求相关余额与事件在回调前更新,符合 checks-effects-interactions 的基本方向;但你的自定义逻辑 如果在回调之后才维护关键状态,就可能暴露重入窗口。OpenZeppelin 5.x 文档因此不建议覆写 带 acceptance check 的内部函数,而建议把自定义状态逻辑放在 _update[7]

07 / OPERATOR APPROVAL

一次 ApprovalForAll,授权的不是一件,而是一整个柜子

operator 获得同一 ERC-1155 合约内 owner 所有 token id 的操作权

ERC-1155 核心没有 ERC-20 那种“给 spender 100 单位额度”,也没有 ERC-721 的单 token id 授权。标准授权入口是 setApprovalForAll(operator, approved)

这种设计让一个市场只需获得一次授权,就能处理用户在该集合/世界中的多种资产; 也意味着恶意或被攻破的 operator 可能移动该合约内的全部 token type。授权事件 ApprovalForAll(owner, operator, approved) 必须记录开启或关闭。 [1]

SCOPE / CONTRACT

范围不是“整个钱包”

授权键包含 owner、operator 和当前 token 合约地址;其他合约不受影响。

SCOPE / ALL IDS

也不是“只卖这一件”

核心标准不提供按 id、数量、截止时间的细粒度 allowance。

VERIFY / OPERATOR

先核对 operator 地址

前端域名与签名文案不是权限本身;真正获得权力的是链上 operator 地址。

REVOKE / BOOL

结束后可设为 false

撤销只阻止未来操作,不能追回已被转走的资产或取消已经执行的交易。

08 / METADATA URI

一个 URI 模板,怎样展开成成千上万份元数据?

{id} 被客户端替换为 64 位、小写、无 0x 前缀的十六进制 token id

多资产合约若为每个 id 重复保存完整 URL,会增加链上存储。ERC-1155 的可选 Metadata URI 扩展允许所有 token type 共享一个包含 {id} 的模板。

Metadata transformer

输入十进制 id,亲手完成标准替换

默认示例使用 EIP-1155 中的 314592,即十六进制 0x4cce0。

允许 0 到 2²⁵⁶−1。这里只做本地格式转换,不读取链上数据。

64-character hex 000000000000000000000000000000000000000000000000000000000004cce0
Resolved URI https://assets.example/000000000000000000000000000000000000000000000000000000000004cce0.json

客户端遇到 URI 或元数据 JSON 值中的 {id} 时,必须用 64 个十六进制字符替换: 只用 0-9a-f,不带 0x,左侧补零。标准元数据 JSON 可以包含 namedescriptionimage,也有可选 decimals 字段用于显示数量的小数位。[1]

链上的 token,为什么画面仍可能被改?

合约通常只返回 URI;名称、描述和图片可能位于普通 HTTPS 服务器或可变的内容入口。 若管理员能改 base URI、服务器能替换 JSON,用户界面看到的内容就可能改变。 选择 IPFS 等内容寻址存储、冻结 URI、把 JSON 作为 data URI 放到链上,能改变信任边界, 但成本和可更新性也不同。OpenZeppelin 官方指南特别提醒:元数据不在链上时,开发者可以改变底层信息。 [6]

NO NAME() / CORE

核心没有集合级 name 与 symbol

ERC-1155 把人类可读名称交给每个 id 的元数据,避免把金融 ticker 假设强加给所有资产。

URI ≠ EXISTENCE

有 URI 不证明 token 存在

实现可以对尚未铸造的任意 id 返回同一个模板;标准明确禁止用 uri(id) 判断存在性。

EVENT / URI

可表达的 URI 变化要留事件

若某 id 的 URI 更新能由事件表达,应发出 URI(value, id);动态算法 URI 可能无法逐项发出。

DECIMALS / OPTIONAL

显示小数是元数据语义

核心没有 decimals() 函数;可选 JSON decimals 告诉客户端如何显示整数余额。

09 / IMPLEMENTATION

标准不会替你守住稀缺性,代码策略必须补上

OpenZeppelin 提供经过广泛使用的基础实现;应用仍要定义角色、供应与唯一性

下面是一份教学骨架:金币与季票可以按策略增发;唯一资产使用独立 id,并用永久 uniqueIdUsed 记录防止销毁后重铸。它展示边界,不是未经审计即可上线的产品。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC1155} from
    "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import {ERC1155Supply} from
    "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol";
import {Ownable} from
    "@openzeppelin/contracts/access/Ownable.sol";

contract WorldItems is ERC1155Supply, Ownable {
    uint256 public constant GOLD = 1;
    uint256 public constant SEASON_PASS = 7;
    mapping(uint256 id => bool) private uniqueIdUsed;

    constructor()
        ERC1155("ipfs://bafy.../{id}.json")
        Ownable(msg.sender)
    {
        _mint(msg.sender, GOLD, 1_000_000, "");
        _mint(msg.sender, SEASON_PASS, 500, "");
    }

    function mintFungible(
        address to,
        uint256 id,
        uint256 amount
    ) external onlyOwner {
        require(id == GOLD || id == SEASON_PASS, "restricted id");
        _mint(to, id, amount, "");
    }

    function mintUnique(address to, uint256 id) external onlyOwner {
        require(id >= 1_000_000, "unique id range");
        require(!uniqueIdUsed[id], "id permanently used");
        uniqueIdUsed[id] = true;
        _mint(to, id, 1, "");
    }

    function burn(uint256 id, uint256 amount) external {
        _burn(msg.sender, id, amount);
    }
}

逐行看真正的安全假设

  1. ERC1155Supply 是扩展,不是核心保证 它跟踪每个 id 的 totalSupply 和 exists;不能从所有 ERC-1155 合约假设这些函数存在。
  2. onlyOwner 决定谁能增加供应 部署者私钥、代理升级管理员或角色合约若被攻破,供应规则也会被改写。
  3. 常量只固定 id,不固定供应 GOLD = 1 只是编号;真正的供应来自每次 _mint 与 _burn 的累计结果。
  4. 唯一 id 需要永久使用记录 只检查 totalSupply(id) == 0 会允许销毁后再次铸造;永久映射把“曾经使用”也纳入约束。
  5. 接收钩子仍会执行 _mint 给合约地址时也要通过 receiver acceptance check;接收方可以拒绝或触发回调逻辑。

OpenZeppelin 的基础实现使用 mapping(uint256 id => mapping(address account => uint256)) 存余额,并把 operator 授权存为 owner 与 operator 的二维映射。它提供内部 _mint_mintBatch_burn_burnBatch, 由应用决定如何安全暴露。[5]

10 / EVENTS & AUDIT

链上看见一份 ERC-1155,先从事件重建它的历史

标准保证余额变化留痕,但 token id 的发现与丰富查询通常需要索引器

ERC-1155 为减轻合约存储,不要求提供“列出所有 id”或“列出某账户全部资产”的核心函数。 区块浏览器、市场与钱包通常从部署区块开始监听事件,在链下数据库中维护 token id、供应和 URI。

标准要求所有余额变化——包括零值转移、创建与销毁——发出 TransferSingleTransferBatch;事件必须足以让观察者重建当前余额。 若要宣布一个尚无初始余额的新 id,规范建议从零地址向零地址发出 value 为 0 的 TransferSingle[1]

面对任何 ERC-1155 合约,用这八问完成第一轮审计

  1. 这是什么链、哪个合约、哪个 id? 名称和图片可能重名;完整身份至少需要 chain id、contract address 与 token id。
  2. 该 id 的供应不变量是什么? 查当前总量、上限、是否可增发、是否可销毁、销毁后能否重铸,以及约束是否真的在代码里。
  3. 谁持有 mint、pause、URI 与 upgrade 权限? 标准没有规定这些角色;owner、多签、Timelock、代理管理员都可能改变资产含义。
  4. ApprovalForAll 会暴露哪些 id? 枚举 owner 在这份合约中的所有资产,不要只看当前准备交易的那一件。
  5. 接收钩子是否产生外部调用风险? 检查回调前后状态、重入保护、转发逻辑,以及合约是否有安全的出库路径。
  6. 批量数组的语义是否被正确验证? ids 与 values 必须同长且按位置配对;自定义逻辑不要错误假设 id 一定唯一或已排序。
  7. 元数据在哪里,谁能改变? 追踪 base URI、内容哈希、服务器、IPFS pin、链上 data URI 与管理员更新路径。
  8. 索引器的视图能否与链上余额对账? 用 balanceOf/Batch 抽样复核事件数据库,处理重组、部署起点和代理升级后的事件连续性。

11 / RECAP

把整课压缩成五个坐标

地址定位账本,id 定位类型,amount 定位数量,policy 定义稀缺,events 让外部世界看见变化

ERC-1155 = one contract × many ids × balance per account × native batch × safe receipt

六题自检

1. 在 ERC-1155 中,什么最准确地定位一个资产类型?

选择答案后查看解析。

答案:C。同一条链上,ERC-1155 的资产类型至少由合约地址与 token id 共同定位;跨链比较还要加入 chain id。

2. totalSupply(2048) 当前等于 1,是否足以证明 ID 2048 永远是唯一 NFT?

选择答案后查看解析。

答案:B。核心标准没有 FT/NFT 类型标志;当前供应为 1 也不能证明未来不会再铸造。要审计永久供应规则。

3. balanceOfBatch([Alice, Bob], [1, 7]) 返回哪两项?

选择答案后查看解析。

答案:A。balanceOfBatch 按位置读取 (accounts[i], ids[i]);safeBatchTransferFrom 也按位置配对 ids[i] 与 values[i],且只有一组 from/to。

4. Alice 调用 setApprovalForAll(Market, true) 后,Market 的标准权限范围是什么?

选择答案后查看解析。

答案:C。operator for all 的范围是指定 owner 在这一个 ERC-1155 合约内的全部 token id;不是全钱包,也不是单一 id。

5. 接收合约没有实现 ERC1155Receiver,safeBatchTransferFrom 会怎样?

选择答案后查看解析。

答案:B。标准转移给合约时必须通过 receiver hook;错误返回值、缺少接口或 revert 都会使标准转移回滚。

6. uri(999) 返回一个有效 JSON 地址,能否证明 ID 999 已经存在?

选择答案后查看解析。

答案:A。实现可能对任意 id 返回共享 URI 模板,即使该 id 从未铸造。存在性应看供应扩展、事件历史与具体实现。

12 / TERMS & PRIMARY SOURCES

把术语落回规范原文

本课以 ERC/EIP 与 OpenZeppelin 官方文档、官方实现为事实基线

token type
ERC-1155 中由一个 uint256 id 标识的资产类别;同一 id 可有任意数量余额。
operator
经 owner 授权、可以代表其转移同一 ERC-1155 合约内 token 的地址。
batch
在一个标准调用里按位置处理多组 id/value 或 account/id 配对。
magic value
receiver hook 必须返回的 bytes4 常量,用明确响应证明接收合约理解并接受 token。
mint / burn
供应增加/减少;通过 Transfer 事件中零地址作为 from/to 表达,但公共入口由实现决定。
indexer
扫描链上事件并建立可搜索数据库的系统;帮助发现 token id,但不替代合约状态验证。
  1. 01
    ERC-1155 · Multi Token Standard

    核心规范:函数、事件、safe transfer、receiver、URI 替换、审批、批量能力与事件可追溯性。

  2. 02
    ERC-20 · Token Standard

    同质化代币的 balance、transfer、allowance 与事件基线,用于比较一维余额模型。

  3. 03
    ERC-721 · Non-Fungible Token Standard

    逐件唯一资产、ownerOf、单件授权与 safe receiver 的规范基线。

  4. 04
    ERC-165 · Standard Interface Detection

    supportsInterface 与 interface id 的定义;ERC-1155 依赖它发布接口能力。

  5. 05
    OpenZeppelin Contracts v5.6.1 · ERC1155.sol

    锁定发布版本的官方实现,可核对二维余额、operator approval、URI、更新与接收检查的实际代码结构。

  6. 06
    OpenZeppelin Contracts 5.x · ERC-1155 Guide

    多代币模型、创建、URI、发送给合约与 ERC1155Holder 的官方教学说明。

  7. 07
    OpenZeppelin Contracts 5.x · ERC1155 API

    当前 5.x 函数、扩展、错误、acceptance check 与重入注意事项的 API 参考。

  8. 08
    OpenZeppelin Contracts 5.x · ERC1155Supply

    每 id 供应跟踪、exists,以及“供应为 1 不保证未来不会继续铸造”的明确边界。

  9. 09
    ethereum.org · Token Standards

    以太坊开发者文档对 ERC-20、ERC-721、ERC-1155 及其组合与批量价值的概览。

The durable model

标准统一动作,策略决定资产。

当你再次看到“ERC-1155 NFT”或“多代币合约”,不要只记住“一个合约里放很多东西”。 先写下合约地址与 id,再读余额、供应与 mint 权限;再看 operator、receiver hook、URI 与事件。只有把接口能力和应用策略分开,你才真正理解这份资产是什么。