Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. 管理员调用 mint(Alice, 1),检查 ownerOf(1) == Alice。
  2. Alice 调用 approve(Operator, 1),检查 getApproved(1) == Operator。
  3. Operator 调用 safeTransferFrom(Alice, Bob, 1)。
  4. 检查 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 或业务协议的审计。

参考资料