ERC-721:从所有权、授权到安全转移
ERC-721 用 (合约地址, tokenId) 标识非同质化代币。balanceOf 返回持有的 NFT 数量,ownerOf(tokenId) 返回某个 NFT 的持有地址。标准接口不自动证明链外物品的所有权,也不规定发行权限。规范
本节沿着“发行一个 NFT → 授权 → 转移 → 接收回调”学习。示例固定为 Solidity 0.8.24、OpenZeppelin Contracts 5.2.0;完整工程与测试在仓库的 examples/ethereum-signatures。
1. 两种授权的范围
| 操作 | 含义 | 转移后的变化 |
|---|---|---|
approve(operator, tokenId) | 授权操作某一个 NFT | 该 NFT 的单独授权被清除 |
setApprovalForAll(operator, true) | 授权操作该所有者在此合约中的全部 NFT | 不因转移其中一个 NFT 而撤销 |
approve(address(0), tokenId) | 撤销单独授权 | 不改变所有权 |
setApprovalForAll(operator, false) | 撤销全局操作员授权 | 不改变所有权 |
所有者、该 NFT 的被授权地址、或所有者的全局操作员可以发起转移。NFT 转给新持有者后,旧持有者的全局授权仍属于旧持有者;它不会自动赋予操作员对新持有者资产的权限。
Approval 和 ApprovalForAll 记录授权设置,Transfer 记录所有权变化。索引器不能只保留历史 Approval,还必须处理转移清除单独授权的规则。
2. 一个最小发行合约
保存为示例工程中的 src/LearningNFT.sol:
// 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 LearningNFT is ERC721, Ownable {
constructor() ERC721("Learning NFT", "LNFT") Ownable(msg.sender) {}
function mint(address recipient, uint256 tokenId) external onlyOwner {
_safeMint(recipient, tokenId);
}
}
mint 只允许合约管理员调用;_safeMint 向合约地址发行时会检查接收回调。发行权限是本例的业务规则,不属于 ERC-721 的必需接口。
ERC-721 的 metadata 和 enumerable 是可选扩展。tokenURI 属于 metadata;本例没有设置图片或 JSON URI,也没有实现全部 token 的枚举。支持某个接口应通过 ERC-165 查询,不能只从名字猜测。
3. 授权与转移的完整路径
假设 Alice 持有 tokenId = 1,Operator 要把它转给 Bob:
- 管理员调用
mint(Alice, 1),检查ownerOf(1) == Alice。 - Alice 调用
approve(Operator, 1),检查getApproved(1) == Operator。 - Operator 调用
safeTransferFrom(Alice, Bob, 1)。 - 检查
ownerOf(1) == Bob、getApproved(1) == address(0),并检查双方 NFT 数量。
from 必须匹配当前所有者,不能通过随意填写 from 绕过权限。第一次转移后,Operator 不能继续凭旧的单独授权转走 Bob 的 NFT。
这条路径对应 testNFTApprovedTransferClearsApproval。全局操作员转移多个 NFT 与未授权转移失败也有独立测试。
4. safeTransferFrom 的 safe 指什么?
目标地址有代码时,安全转移会调用 onERC721Received,要求返回 0x150b7a02。回调拒绝、返回错误值或缺失必要接口,会使整次转移回滚。它用于检查接收协议,不保证接收者可信,也不是防盗或防重入机制。
在本例使用的 OpenZeppelin 5.2.0 实现中,先更新所有权并清除单独授权,再调用接收者。因此回调能读到新的所有权,也能再次调用 NFT 或其他业务合约。固定版本源码
sequenceDiagram
participant O as 已授权操作员
participant N as NFT 合约
participant R as 接收合约
O->>N: safeTransferFrom(from, R, tokenId)
N->>N: 检查权限、更新所有权、清除单独授权
N->>R: onERC721Received(...)
R-->>N: 返回接收魔数或拒绝
Note over N,R: 拒绝时本次调用的状态变化回滚
测试中的 ForwardingReceiver 在回调中把 NFT 再转给 Bob,整笔交易仍成功。这说明安全转移结束后,最终所有者未必就是最初的接收合约。
市场、质押或借贷协议若在转移 NFT 后才更新订单、抵押或领取状态,可能受到回调重入影响。应按业务不变量安排检查和状态更新,并在需要的入口使用重入保护;不能把 _safeMint 或 safeTransferFrom 当作锁。
transferFrom 不做接收回调检查,可能把 NFT 转进无法操作它的合约。向尚未部署代码的预测地址转移也不会验证未来代码的接收能力。
5. 运行与观察
在示例工程中执行:
npm ci --ignore-scripts
forge test --match-test testNFT -vv
测试覆盖授权清除、全局授权、未授权失败、回调读取新状态、拒绝回调时的所有权及授权回滚,以及回调中的再次转移。它验证这份教学实现,不代表对任意 NFT 或业务协议的审计。