Ethereum / ERC standards / Lesson 15.02

余额不够时,怎样给每一件资产一个身份

ERC-20 只需回答“某地址有多少”。当每一件资产都要被单独识别时, 我们还必须回答:它是哪一件、现在归谁、谁能替 owner 移动, 以及目标合约会不会正确接收。ERC-721 就是这套公共语法。

  • 正式目录:ERC-721 非同质化代币标准
  • 约 55 分钟
  • 4 个互动模型
  • 6 题自检
01 / IDENTITY IS A TUPLE

tokenId 只在一份合约里唯一。真正可定位的身份至少需要 chain id、合约地址与 tokenId。

02 / OWNERSHIP IS STATE

“拥有”是合约状态,不是图片属性。ownerOf、授权记录和转移规则共同决定谁能做什么。

03 / SAFE IS A HANDSHAKE

safeTransferFrom 的“safe”是接收回调。它防止误投给不兼容合约,却不证明资产有价值或接收方可信。

01 / FROM QUANTITY TO IDENTITY

同样拥有 1 个,为什么还不够?

ERC-20 记录数量;ERC-721 必须指出“具体是哪一件”

假设 Alice 有 1 张演唱会门票,Bob 也有 1 张。若只看余额,两人完全一样; 但 Alice 的票可能是 A 区 1 排 1 座,Bob 的票是 C 区 20 排 18 座。 两件资产不能只用数量表达,因为它们的身份、属性和权利都可能不同。

“非同质化”不是说两件东西永远无法交换,也不是说它们一定昂贵,而是说系统在记账时 保留了每个 token 的独立身份。即使两张门票图片相同、价格相同, 只要 tokenId 不同,合约就能分别指定 owner、授权和元数据。ERC-721 提案把这类 独立 token 的查询、转移、授权与事件统一成标准接口。[1]

ERC-20:balance[address] → amount   |   ERC-721:owner[tokenId] → address

02 / THE IDENTITY TUPLE

#42 不是完整身份,只是局部编号

相同 tokenId 可以同时存在于无数合约,也可以存在于不同网络

tokenId 是一个 uint256。规范没有规定它必须从 1 连续递增, 也没有规定 #1 比 #42 更早或更稀有。它只需要在这份 ERC-721 合约的 有效 token 集合里标识一个独立 token。

在实际产品里还应明确网络/chain。只报“#42”或只看名称与图片,都不足以验证身份。

Interactive model 01

两个都叫 #42,它们是同一枚吗?

本地固定数据 · 不连接网络
RECORD A
chain
1
contract
0x721A…A11
tokenId
42
RECORD B
chain
1
contract
0x721A…A11
tokenId
42
同一坐标

三项全部相同,才指向同一份链上身份记录。

名字、symbol 和图片为什么都不能当身份?

合约名称与 symbol 属于可选元数据扩展,其他合约可以使用同样的文本;图片也能被复制或 指向同一文件。验证 provenance 时,应从可信入口取得网络与合约地址, 再核对 tokenId、铸造与转移历史。界面上的“蓝勾”、集合名或缩略图只是额外线索, 不是协议层身份。

NAME

可以重复

规范没有全局名称注册表;仿冒合约可以复制名称和 symbol。

IMAGE

可以复制

媒体文件不是 token 本身;多枚 token 也可以指向完全相同的图片。

ADDRESS

必须核对

可信合约地址加 chain id,再配合 tokenId,才形成可查询坐标。

03 / OWNERSHIP AS STATE

ownerOf 看具体一枚,balanceOf 数一个地址

两种查询从相反方向观察同一套所有权状态

ERC-721 同时提供两种观察方式:ownerOf(tokenId) 从 token 找 owner;balanceOf(owner) 从 owner 数有效 token 的数量。 二者必须描述同一个世界,不能各说各话。

因而 ownerOf(42) = AliceownerOf(77) = BobbalanceOf(Alice) = 2。balance 仍然有用,但它不再足以定位资产。

  1. 每个当前存在的 tokenId 恰有一个非零 owner。正常状态下同一个 ERC-721 token 不能同时属于两人。
  2. 一个地址可以拥有零枚、多枚或很多枚。非同质化不等于“每人只能有一枚”。
  3. balance 等于该地址当前拥有的有效 token 数量。转入加一,转出减一;铸造与销毁也改变 balance。
  4. 无效 tokenId 的 ownerOf 必须失败。返回零地址会混淆“从未铸造”“已经销毁”和“当前 owner”。
  5. 零地址不能作为正常 owner。balanceOf(address(0)) 也必须失败。

04 / LIFECYCLE AND EVENTS

铸造、授权、转移、销毁:一枚 token 的四幕

mint 和 burn 不是核心函数名,却能通过 Transfer 事件表达状态边界

ERC-721 规定的是可观察结果,不规定你的外部铸造函数必须叫 mint, 也不要求一定提供 burn。常规铸造应发出从零地址来的 Transfer,销毁时发出转往零地址的 Transfer; 中间的普通转移则从旧 owner 指向新 owner。EIP-721 另有一个历史性例外: 合约创建期间分配 token 时,可以不逐枚发出 Transfer。[1]

Interactive model 02

逐步改写 token #42 的状态

每一步显示状态与对应事件
CURRENT STATE / TOKEN #42

Owner · Alice

Approved · none

Alice balance · +1

OBSERVABLE LOG Transfer(0x0000…, Alice, 42)

从零地址转入表示创建;规范不要求公开函数必须叫 mint。

事件不是第二份权威状态

TransferApprovalApprovalForAll 写入交易日志,便于钱包和索引器重建历史;合约执行时真正读取的是存储中的当前 owner 与授权。 同一笔交易若最终回滚,日志也会一起消失。索引器还要知道:token 转移会使单枚 approval 失效,规范并不要求再额外发一条“清零 Approval”事件。[1]

05 / THE STANDARD SURFACE

九个核心函数、三个事件,外加 ERC-165

核心接口负责查、转、授权与留证;元数据、枚举、铸造都不在这一层

钱包不需要知道合约内部用了怎样的 mapping,只需知道它是否支持 0x80ac58cd,再按统一函数签名查询和调用。ERC-721 核心接口 继承 ERC-165,因此能力检测也是标准的一部分。[2]

IERC721 · 精简接口
interface IERC721 is IERC165 {
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    function balanceOf(address owner) external view returns (uint256);
    function ownerOf(uint256 tokenId) external view returns (address);
    function getApproved(uint256 tokenId) external view returns (address);
    function isApprovedForAll(address owner, address operator) external view returns (bool);

    function approve(address to, uint256 tokenId) external;
    function setApprovalForAll(address operator, bool approved) external;
    function transferFrom(address from, address to, uint256 tokenId) external;
    function safeTransferFrom(address from, address to, uint256 tokenId) external;
    function safeTransferFrom(
        address from, address to, uint256 tokenId, bytes calldata data
    ) external;
}

哪些常见函数其实是可选或项目自定义?

name · symbol · tokenURI

Metadata 扩展

接口 ID 0x5b5e139f。基础 OpenZeppelin ERC721 实现通常包含它,但协议层仍是可选扩展。

totalSupply · tokenByIndex · tokenOfOwnerByIndex

Enumerable 扩展

接口 ID 0x780e9d63。链上枚举有额外存储与 Gas 成本,不是 ERC-721 核心要求。

mint · safeMint · burn

发行与销毁策略

外部函数名、调用权限、供应上限和价格由实现决定;标准只定义相关状态与事件语义。

royaltyInfo · pause · upgrade

其他能力

版税、暂停和升级来自其他标准或项目逻辑,不能因为合约是 ERC-721 就自动假定存在。

interface ID 是怎样来的?

一般情况下,函数签名先经过 Keccak-256,取前 4 字节成为 selector;接口里各函数 selector 再按位 XOR,得到 ERC-165 interface ID。只 XOR 该接口自身定义的函数,不包含继承函数:因此 ERC-721 的 0x80ac58cd 不包含 supportsInterface,Metadata 与 Enumerable 的 ID 也不包含核心九函数。事件、返回类型、参数名以及 view/payable 等修饰不参与计算。ERC-4906 只新增事件, 所以 0x49064906 是规范直接指定的固定值,不是事件 selector 的 XOR。 supportsInterface 只声明“我声称支持什么”,并不证明实现正确、安全或真实。

06 / TWO LEVELS OF PERMISSION

approve 给一枚钥匙,operator 拿一串钥匙

所有权没有转移,但被授权者已经获得调用转移函数的能力

市场合约若要在成交时交付 token,不一定先成为 owner。Alice 可以只批准它移动 #42,也可以让它成为自己在这一份 ERC-721 合约内的全局 operator。 两种授权范围差异巨大,界面却常只写一个模糊的“Approve”。

canTransfer = caller == owner | caller == getApproved(id) | isApprovedForAll(owner, caller)

Interactive model 03

谁能移动 Alice 的 token #42?

当前:Carol 单枚授权 · Market 全量授权
msg.sender Alice

current owner

AUTHORIZATION CHECK 允许转移

caller 与当前 owner 相同,第一条条件成立。

五个容易被忽略的授权细节

  1. 单枚授权不等于所有权。Carol 能调用转移,但 ownerOf(42) 在转移前仍返回 Alice。
  2. approved 不能凭单枚权限继续批给别人。调用 approve 的权限属于 owner 或 owner 的全局 operator。
  3. 任何转移都会清除该 token 的单枚 approval。即使 Alice 把 #42 转给自己,也应按转移语义清除。
  4. operator 关系绑定 owner 与合约。Alice 给 Market 的权限不涵盖其他 ERC-721 合约,更不涵盖整个钱包的全部资产。
  5. 旧 owner 的 operator 关系不会因一枚 token 转出而自动删除。它对 Alice 留下或以后获得的该合约 token 仍有效,直到撤销。

07 / TRANSFER AS A STATE TRANSITION

一次转移,至少改写三类状态

先验证 token、from、to 与 caller,再清授权、改余额、换 owner、发事件

从 Alice 把 #42 转给 Bob,不是“把文件复制过去”,而是一次原子的合约状态转换。 任何前置条件失败,整笔调用回滚;全部成立时,所有权、双方余额、单枚授权和事件日志 才共同形成新结果。

transferFrom 与 safeTransferFrom 共同保证什么?

两者都必须检查 token、from、to 和 caller,并在成功时完成同样的核心所有权转换。 差别只出现在目标地址是合约时:safeTransferFrom 还要求接收者完成一次 标准回调握手;transferFrom 不问,因而可能把 token 送进不会处理或 无法转出的合约。下一节把这条分叉单独展开。

08 / THE RECEIVER HANDSHAKE

“安全”不是鉴定好坏,而是确认合约会接

EOA 直接接收;合约必须返回 0x150b7a02,否则整个 safe transfer 回滚

地址可能属于人控制的 EOA,也可能是一段合约代码。EOA 没有回调函数; 对合约地址,safeTransferFrom 会在更新 ERC-721 状态后调用 onERC721Received。正确返回 selector 才表示接收。[8]

IERC721Receiver · 合约接收者接口
interface IERC721Receiver {
    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) external returns (bytes4);
}

// 接受时必须返回:
return IERC721Receiver.onERC721Received.selector; // 0x150b7a02

Interactive model 04

同一枚 #42,换一种目标与方法会怎样?

观察“成功”“回滚”与“可能锁住”的区别
CALL PATH safeTransferFrom(Alice, Vault, 42) → Vault.onERC721Received(...) → return 0x150b7a02
TRANSACTION RESULT 成功提交

Vault 明确表示会接收,ownerOf(42) 最终变为 Vault。

回调里的 operator 到底是谁?

回调参数 operator本次 safe transfer 的发起者, 可能是 owner、单枚 approved,也可能是全局 operator。它不是 setApprovalForAll 意义下角色的保证。回调中的 from 是转移前 owner,安全铸造时通常为零地址;data 的格式由调用方与接收者约定。

09 / METADATA IS A DESCRIPTION LAYER

token 在链上,图片通常不在 token 里

tokenURI 返回的是元数据 URI;JSON 再告诉界面名称、描述与媒体地址

ERC-721 的核心所有权接口即使没有图片也能工作。可选 Metadata 扩展增加 name()symbol()tokenURI(id)。 tokenURI 返回一条 URI;钱包随后读取 JSON,JSON 才可能通过 image 指向媒体。[1]

HTTP、IPFS、data URI 分别保证什么?

HTTPS

位置寻址

部署简单,但域名、服务器、路径或权限变化会影响访问;同一 URL 的内容也可能被替换。

IPFS

内容寻址

CID 可校验字节是否相同,却不等于永久有人保存或提供内容;pin 与可用性仍需考察。

DATA / ON-CHAIN

链上生成或存储

减少外部服务依赖,通常更昂贵;仍要检查合约可升级性与生成逻辑是否可变。

元数据变化,钱包怎样知道要刷新?

ERC-721 核心没有定义更新通知。可选 ERC-4906 增加 MetadataUpdate(tokenId)BatchMetadataUpdate(fromTokenId, toTokenId) 事件, 并指定接口 ID 0x49064906。合约一旦声明支持 ERC-4906, 单枚 token 或连续 ID 范围的 JSON metadata 改变时,必须发出对应更新事件; 规范建议 mint、burn,以及 tokenURI 改变但 JSON metadata 未改变时不发 MetadataUpdate。它只改善变化的可发现性,不授予谁可以修改元数据; 权限仍由实现决定。[9]

10 / IMPLEMENTATION AND EXTENSIONS

最小合约很短,真正的风险都在策略里

ERC-721 解决互操作;供应、权限、升级、URI 与业务规则仍要自行设计

生产实现通常复用经过广泛审查的库,而不是从头手写 owner 与 approval mapping。 下面的 OpenZeppelin 5.x 骨架只做三件事:初始化名称、限制铸造者、用 _safeMint 创建连续编号。它可以符合 ERC-721,却没有 totalSupply(),因为 Enumerable 本来就是可选扩展。

Solidity · OpenZeppelin Contracts 5.x 教学骨架
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract LorePass is ERC721, Ownable {
    uint256 private _nextTokenId = 1;

    constructor(address initialOwner)
        ERC721("Lore Pass", "LORE")
        Ownable(initialOwner)
    {}

    function safeMint(address to)
        external
        onlyOwner
        returns (uint256 tokenId)
    {
        tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
    }

    function _baseURI()
        internal
        pure
        override
        returns (string memory)
    {
        return "ipfs://REPLACE_WITH_METADATA_CID/";
    }
}

逐行看,它做了什么,也没做什么

ERC721("Lore Pass", "LORE")

复用核心与 Metadata

库提供 owner、balance、approval、转移、接收检查、ERC-165 与基础 tokenURI 逻辑。

onlyOwner

显式限制铸造

ERC-721 不规定谁能 mint。本例把权力给合约 owner;真实项目还要评估单点权限与移交流程。

_nextTokenId++

选择一种编号策略

连续编号只是实现选择。规范允许散列、随机分配或业务编号,只要有效 token 身份不冲突。

_safeMint(to, tokenId)

创建并做接收检查

若 to 是合约,会执行 receiver 回调。外部回调也意味着自定义后续逻辑要考虑重入。

_baseURI()

拼接元数据路径

基础实现通常以 base URI 加 tokenId 的十进制字符串形成 tokenURI;资源需返回 JSON。

no Enumerable · no cap · no burn

没有自动附赠的承诺

供应上限、链上遍历、销毁、暂停、升级和版税都需要另外实现并说明权限。

读实现时要特别检查的风险面

01 / MINT AUTHORITY

谁能增发

检查公开 mint 条件、角色、签名验证、上限与 tokenId 是否可能重用。

02 / ADMIN POWER

谁能改变规则

代理升级、暂停、强制转移、URI 更新与角色管理员可能改变最初假设。

03 / CALLBACK

哪里会外部调用

safe mint/transfer 的 receiver 回调可以重入;检查关键状态与业务动作的顺序。

04 / APPROVAL

权限是否过宽

operator for all 是常见高权限入口;检查前端提示、撤销路径与签名域。

05 / METADATA

描述能否变化

base URI、逐 token URI、升级权、服务器与内容固定方式决定展示是否可变。

06 / EXTENSIONS

扩展是否真的实现

用 ERC-165 核对声明,再测试行为;支持 ID 不是正确性与安全性的证明。

11 / CHOOSE BY THE LEDGER

不要按营销名字选标准,要按资产怎样被计数

一维余额、单件所有权、二维余额,分别对应 ERC-20、721、1155

三种标准没有高低之分,它们回答不同的数据问题。先画出你的账本: 账户只需要一个数量、每件都需要独立 owner,还是同一合约要管理许多类型与份数? 数据模型一旦清楚,接口选择往往自然出现。

维度 ERC-20 ERC-721 ERC-1155
账本核心 balance[account] owner[tokenId] balance[id][account]
资产身份 合约地址 合约地址 + tokenId 合约地址 + token type id
转移参数 数值 value 具体一个 tokenId id + amount,可批量
授权模型 数值 allowance 单枚 approved + owner-wide operator owner-wide operator
标准批量转移 有 safeBatchTransferFrom
典型问题 一个地址有多少单位? 这一个编号当前归谁? 这个地址拥有这个类型多少份?

ERC-721 没有标准 decimals,也不把一个 tokenId 拆成份额。外部包装协议可以表达份额, 但那是另一层合约关系,不是 ERC-721 自身能力。

面对任何 ERC-721,用十步把宣传还原成可验证问题

  1. 确认 chain 与合约地址从可信入口取得地址,不用名称、symbol 或图片代替身份核验。
  2. 核对 tokenId 与当前 owner直接读取 ownerOf;无效 token 的查询应失败。
  3. 检查 ERC-165 声明与实际行为核心、Metadata、Enumerable、ERC-4906 分别核对,不能从其中一个推断全部。
  4. 画出单枚与全量授权读取 getApproved 与 isApprovedForAll,确认 operator 的范围与撤销方式。
  5. 追踪 mint、transfer 与 burn 权限谁能创建、销毁、强制移动或重用 tokenId?
  6. 检查供应承诺来自哪里核心标准没有 totalSupply、cap 或“永不增发”;承诺必须由代码、权限和治理共同支撑。
  7. 沿 tokenURI 检查元数据记录 URI、JSON、媒体位置、内容哈希、可用性与管理员修改能力。
  8. 识别升级与管理员边界代理、角色、多签、暂停和 URI 更新可能改变当前结论。
  9. 审查 receiver 与重入路径safe transfer 会执行外部代码;接收成功也不等于以后能取回。
  10. 把链上所有权与现实权利分开版权、门票、会员与兑付效力要看许可证、合同和适用规则。

12 / THE DURABLE MODEL

把 ERC-721 压缩成一条状态因果链

身份定位状态,授权决定能力,转移改写账本,事件通知外部,元数据解释展示

真正理解 ERC-721,不是背出“NFT 标准”四个字,而是能在任何合约面前分清: 哪些是核心协议保证,哪些是可选扩展,哪些只是项目自己的经济与治理策略。

六题校准

1. 两个合约里都存在 tokenId #42,最准确的说法是?

选择一个答案。

无 JavaScript 时查看答案

答案 B。tokenId 只是合约内部编号;跨链还必须加入 chain id。

2. 哪一个函数属于 ERC-721 核心必需接口?

选择一个答案。

无 JavaScript 时查看答案

答案 C。totalSupply 属于 Enumerable,tokenURI 属于 Metadata;两者都是可选扩展。

3. Alice 把 #42 转给 Bob 后,Carol 原有的单枚 approval 怎样?

选择一个答案。

无 JavaScript 时查看答案

答案 A。任何转移都会清除该 token 的单枚 approval;不要求额外发清零 Approval 事件。

4. safeTransferFrom 把 #42 发给不兼容 receiver 合约,会发生什么?

选择一个答案。

无 JavaScript 时查看答案

答案 B。缺失、抛错或错误返回都会让 safe transfer 整笔回滚。

5. tokenURI 返回 IPFS URI,协议层最稳妥的结论是?

选择一个答案。

无 JavaScript 时查看答案

答案 C。内容寻址帮助验证字节,不自动保证永久存储、版权或可用性。

6. supportsInterface(0x80ac58cd) 返回 true,能证明什么?

选择一个答案。

无 JavaScript 时查看答案

答案 A。ERC-165 是能力声明,不是正确性、安全性或资产真实性证明。

13 / TERMS AND PRIMARY SOURCES

术语与一手资料

把“核心标准”“可选扩展”“官方实现”分开阅读

tokenId
ERC-721 合约内部用于标识一个独立 token 的 uint256;不是全网唯一编号。
owner
当前由 ownerOf(tokenId) 返回的非零地址,是协议层所有权状态。
approved
获准移动一个指定 tokenId 的单个地址;任意转移时失效。
operator
获准管理某 owner 在同一 ERC-721 合约内全部 token 的地址。
safe transfer
向合约转移时要求其完成 IERC721Receiver 回调握手的路径。
interface ID
通过 ERC-165 查询合约声明支持哪些标准接口的 bytes4 标识。
tokenURI
可选 Metadata 扩展中返回 token 元数据位置的函数与结果。
mint / burn
创建与销毁 token 的常用实现术语;不是核心接口规定的公开函数名。

一手资料与标准原文

  1. 01
    ERC-721 · Non-Fungible Token Standard

    核心函数、事件、状态要求、安全转移、Metadata 与 Enumerable 扩展的规范原文。

  2. 02
    ERC-165 · Standard Interface Detection

    supportsInterface、interface ID 计算与检测流程的规范。

  3. 03
    ethereum.org · ERC-721

    以太坊开发者文档对非同质化 token、接口与典型用途的官方说明。

  4. 04
    OpenZeppelin Contracts 5.x · ERC-721 Guide

    当前官方库的创建、URI、存储与扩展教学。

  5. 05
    OpenZeppelin Contracts 5.x · ERC-721 API

    核心实现、扩展、errors、回调与重入注意事项的 API 参考。

  6. 06
    OpenZeppelin v5.6.1 · IERC721.sol

    现代 Solidity 形式的九个核心函数与三个事件接口。

  7. 07
    OpenZeppelin v5.6.1 · ERC721.sol

    owner、balance、approval、_update、转移与 Metadata 的官方实现。

  8. 08
    OpenZeppelin v5.6.1 · IERC721Receiver.sol

    onERC721Received 的参数、返回 selector 与回调语义。

  9. 09
    ERC-4906 · EIP-721 Metadata Update Extension

    MetadataUpdate、BatchMetadataUpdate 与 0x49064906 能力标识。

  10. 10
    ERC-6093 · Custom Errors for Common Token Standards

    ERC-721 等常见 token 标准的统一 custom error 词汇。

  11. 11
    ERC-2981 · NFT Royalty Standard

    版税信息是独立可选标准;不属于 ERC-721 核心,也不强制付款。

  12. 12
    OpenZeppelin v5.6.1 · ERC721URIStorage.sol

    逐 token URI 存储、ERC-4906 支持与 MetadataUpdate 事件的官方实现。

The durable model

ERC-721 记录的不是图片,而是一件资产的身份、所有权与行动权限。

当你再次看到一枚 NFT,先写下 chain、contract 与 tokenId;再读取 owner、 单枚 approval 和 operator;然后检查转移与 receiver 回调;最后才沿 tokenURI 进入 JSON、媒体和现实权利。按这个顺序,你看到的是可验证系统,而不是一张缩略图。