OpenZeppeLin 学习:ERC721源码解析

ERC721.sol源码解读ERC721是ETH上的一种非同质化代币(NFT)标准,定义了一种唯一、不可分割、不可互换的代币类型。每一个ERC721代币都有唯一的tokenId和所有者,可以代表数字资产或现实世界中的物品,比如:数字艺术游戏道具虚拟地产门票......

ERC721.sol 源码解读

ERC721 是 ETH上的一种非同质化代币(NFT)标准,定义了一种唯一、不可分割、不可互换的代币类型。每一个 ERC721 代币都有唯一的 tokenId 和所有者,可以代表数字资产或现实世界中的物品,比如:

  • 数字艺术
  • 游戏道具
  • 虚拟地产
  • 门票
  • ......

ERC721(基于v5 的新架构) 合约定义了如下核心函数:

function balanceOf(address owner) external view returns (uint256);
function ownerOf(uint256 tokenId) external view returns (address);
function safeTransferFrom(address from, address to, uint256 tokenId) external;
function transferFrom(address from, address to, uint256 tokenId) external;
function approve(address to, uint256 tokenId) external;
function getApproved(uint256 tokenId) external view returns (address);
function setApprovalForAll(address operator, bool _approved) external;
function isApprovedForAll(address owner, address operator) external view returns (bool);

以及两个关键事件:

event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

📜 SPDX & 编译版本声明

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC721/ERC721.sol)

pragma solidity ^0.8.20;
  • SPDX-License-Identifier: 说明该文件使用的是 MIT 开源协议;
  • pragma solidity ^0.8.20;: 表示该合约编译器版本兼容 0.8.20 及以上(但不包括 0.9.0)。

📦 依赖导入

import {IERC721} from "./IERC721.sol";
import {IERC721Metadata} from "./extensions/IERC721Metadata.sol";
import {ERC721Utils} from "./utils/ERC721Utils.sol";
import {Context} from "../../utils/Context.sol";
import {Strings} from "../../utils/Strings.sol";
import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol";
import {IERC721Errors} from "../../interfaces/draft-IERC6093.sol";

✅ 模块说明

  • IERC721: ERC721 标准接口定义(转账、授权、ownerOf 等);
  • IERC721Metadata: 扩展接口,提供 name()symbol()tokenURI()
  • ERC721Utils: 提供工具函数,比如检查 onERC721Received 等;
  • Context: 获取调用上下文,如 _msgSender(),支持 meta-transactions;
  • Strings: 提供 uint256 -> string 等辅助函数;
  • IERC165 / ERC165: 用于实现接口识别(interface detection);
  • IERC721Errors: 草案中的错误定义接口(draft-EIP-6093),用于更明确的 revert 错误类型。

🧱 主合约结构声明

abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Errors
  • abstract contract: 抽象合约,不能直接部署,需要继承后实现完整逻辑;

  • 继承链:

    • Context: 提供 _msgSender() 等;
    • ERC165: 实现接口查询机制;
    • IERC721: 实现核心 NFT 功能;
    • IERC721Metadata: 提供可读性更好的名称、符号和 URI;
    • IERC721Errors: 提供 revert 报错细节(EIP-6093);

🔐 状态变量定义

string private _name;
string private _symbol;

mapping(uint256 tokenId => address) private _owners;
mapping(address owner => uint256) private _balances;
mapping(uint256 tokenId => address) private _tokenApprovals;
mapping(address owner => mapping(address operator => bool)) private _operatorApprovals;

ERC721 的核心数据结构:

变量名 作用
_name, _symbol token 集合的名称与符号(如 “CryptoKitties”, “CK”)
_owners tokenId => owner,表示每个 token 的所有者
_balances owner => token 数量,便于快速查询持有量
_tokenApprovals tokenId => address,某个具体 token 的授权账户
_operatorApprovals owner => operator => bool,是否全权授权给 operator(可以是多个operator)

💡 注:这些变量基本遵循 EIP-721 的推荐数据结构设计


🔧 构造函数

constructor(string memory name_, string memory symbol_) {
    _name = name_;
    _symbol = symbol_;
}
  • 初始化 token 的集合名和代号;
  • 一般在子合约中通过 ERC721("MyNFT", "MNFT") 调用父构造函数

🧩 接口支持:ERC165

function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
    return
        interfaceId == type(IERC721).interfaceId ||
        interfaceId == type(IERC721Metadata).interfaceId ||
        super.supportsInterface(interfaceId);
}
  • supportsInterface 是 ERC165 标准的一部分,用于检查合约是否实现某个接口。
  • 通过 type(IERC721).interfaceId 获取接口的 selector。
  • 这里判断是否支持:
    • IERC721
    • IERC721Metadata
    • 如果子合约扩展了其他接口,也可以通过 super.supportsInterface 来继续递归检查。

用途:前端或其他合约可调用此函数,判断一个地址是否符合某个标准。


📥 基本查询函数

balanceOf

function balanceOf(address owner) public view virtual returns (uint256) {
    if (owner == address(0)) {
        revert ERC721InvalidOwner(address(0));
    }
    return _balances[owner];
}
  • owner 不能是零地址;
  • _balances[owner] 返回其持有的 token 数量。

💡 _balances 是一个映射,记录了每个地址拥有的 NFT 数量。


ownerOf

  • ownerOf -> _requireOwned -> _ownerOf
function ownerOf(uint256 tokenId) public view virtual returns (address) {
     return _requireOwned(tokenId);
 }   

// _requireOwned(内部方法)
 function _requireOwned(uint256 tokenId) internal view returns (address) {
    address owner = _ownerOf(tokenId);
    if (owner == address(0)) {
        revert ERC721NonexistentToken(tokenId);
     }
    return owner;
}

// _ownerOf(内部方法)
function _ownerOf(uint256 tokenId) internal view virtual returns (address) {
    return _owners[tokenId];
}
  • 通过 _ownerOf(tokenId) 获取当前所有者;
  • 如果为零地址,说明 tokenId 不存在,revert。

☑️ 核心数据 _owners 提供 tokenId 对应的所有者。


namesymbol

function name() public view virtual returns (string memory) {
    return _name;
}

function symbol() public view virtual returns (string memory) {
    return _symbol;
}
  • 这两个方法实现了 IERC721Metadata 接口,通常用于 UI 显示 NFT 系列的名称和代号。

tokenURI

  • tokenURI -> _baseURI
function tokenURI(uint256 tokenId) public view virtual returns (string memory) {
    if (_ownerOf(tokenId) == address(0)) {
        revert ERC721NonexistentToken(tokenId);
    }
    return _baseURI();
}

// _baseURI 内部函数
function _baseURI() internal view virtual returns (string memory) {
    return "";
}
  • 检查 token 是否存在;
  • 返回 _baseURI()

🧠 实际应用中,一般会 override _baseURItokenURI 来返回形如"https://api.example.com/metadata/{id}" 的地址。

🎯 授权机制


🧾 approve

  • 授权单个token的操作人
  • 不能同时授权多个地址对一个 tokenId 使用 approve()
  • 只能有一个特定地址通过 approve() 被授权操作某个 tokenId
  • 每次调用 approve(to, tokenId) 都会覆盖之前的授权地址
  • 若调用 approve(address(0), tokenId),就是取消授权
  • 方法调用链:approve -> _approve -> _approve 重载 -> emit Approval
 function approve(address to, uint256 tokenId) public virtual {
    // to:授权账号;
    // _mesgSender(): 当前合约的调用账号
    _approve(to, tokenId, _msgSender());
 }

function _approve(address to, uint256 tokenId, address auth) internal {
        _approve(to, tokenId, auth, true);
}

function _approve(address to, uint256 tokenId, address auth, bool emitEvent) internal virtual {
    // 是否需要 执行权限检查或发事件
    //    如果 emitEvent == true:说明调用方希望发出 `Approval` 事件
   //     如果 `auth != address(0)`:表示这次调用涉及“授权者”身份,必须要做权限校验
    if (emitEvent || auth != address(0)) { 
       // 第一步安全检查:获取该 tokenId 的当前所有者,若不存在则 revert
         address owner = _requireOwned(tokenId);
           // 第二步安全检查:调用者(auth)是否合法
           //  1. auth != address(0):如果 auth 是零地址,就不需要校验(常出现在清除授权场景)。
           //  2. owner != auth:auth 不是该 token 的 owner。
           //  3. !isApprovedForAll(owner, auth):auth 也不是 owner 授权的全局 operator。
           //  三个条件同时满足,说明这个 `auth` 既不是 owner,也不是全局授权者,auth就不该拥有给别人授权的权力,直接 `revert ERC721InvalidApprover(auth)
           // 防止“无权的人”调用 approve。

            if (auth != address(0) && owner != auth && !isApprovedForAll(owner, auth)) {
                revert ERC721InvalidApprover(auth);
            }
            // 事件触发(可选):内部 silent 操作时可能不发(比如 `transferFrom` 里清空授权)
            if (emitEvent) {
                emit Approval(owner, to, tokenId);
            }
    }
    // 更新授权关系
    // `to` 可以是新的授权地址,也可以是 `address(0)` 表示清除授权
    _tokenApprovals[tokenId] = to;
}

🔍 解读:

  1. 授权人(auth)必须是 owner 或 operator(被 owner 授权过的地址);
  2. 调用内部 _approve 函数完成授权;
  3. _tokenApprovals 是一个 mapping(uint256 => address),记录每个 tokenId 被授权的地址;
  4. 触发 Approval 事件(可选),前端或链上服务可监听。

📌 _msgSender()Context 合约里的,表示当前调用合约的外部账户或合约地址

?疑问:auth != address(0):如果 auth 是零地址,就不需要校验

在 OpenZeppelin v5 架构中,很多底层函数像 _approve()_update() 都增加了 auth 参数,不直接使用 msg.sender,而是显式传入这个“调用发起者”,主要目的是:

  • 支持内部调用/代理调用/meta-tx 场景:可以灵活传入想要验证权限的地址(不一定是 msg.sender)。
  • 方便测试与组合调用:比如内部逻辑复用 _approve(),可以不用每次都重新计算权限,直接把调用者传进来做统一校验。

auth == address(0) 表示“跳过权限验证”,假设调用是受信任的(比如由合约自身发起)

_approve(address(0), tokenId, address(0), false); // 清除授权,无需验证

这个例子常出现在 transfer、burn 等逻辑里。当 NFT 被转移或销毁时,需要清除授权关系,但这个动作是由合约自动发起的,不应该限制它只能由 owner/operator 才能做。

所以:

  • auth == address(0):说明这是合约内部自己发起的授权清除逻辑,不校验权限。
  • 🚫 如果你手动写代码时设置 auth == address(0),你应该非常确定当前代码逻辑已经是“安全场景”。

🧾 setApprovalForAll

  • 给多个地址授权操作你账户下的所有 token
  • 相当于多个“操作人”都能代理你,不限 tokenId
  • 是独立存在的,不影响 approve() 的单 token 授权
function setApprovalForAll(address operator, bool approved) public virtual {
    if (operator == _msgSender()) {  //  不能授权给自己:调用者 == 授权者
        revert ERC721InvalidOperator(_msgSender());
    }
   // 更新映射:调用者拥有的所有token是否授权给operator
    _operatorApprovals[_msgSender()][operator] = approved;
    // 触发ApprovalForAll 事件
    emit ApprovalForAll(_msgSender(), operator, approved); 
}

// 结构是:`owner => (operator => approved)`
mapping(address => mapping(address => bool)) private _operatorApprovals;

🔍 解读:

  1. 不能授权给自己;
  2. 为某个地址(operator)设置是否拥有当前调用者的所有 token 的操作权;
  3. 状态记录在 _operatorApprovals 里;
  4. 触发 ApprovalForAll 事件

🧾 isApprovedForAll

function isApprovedForAll(address owner, address operator) public view virtual returns (bool) {
    return _operatorApprovals[owner][operator];
}
  • 查看 operator 是否被 owner 授权对其所有 token 进行操作

🧾 getApproved

  • getApproved -> _getApproved
// 查看某个 token 是否已经单独授权给某个地址
 function getApproved(uint256 tokenId) public view virtual returns (address) {
     _requireOwned(tokenId); // 作用:检验tokenId的所有者是否是非零地址,没有使用函数的返回值
     return _getApproved(tokenId);
}

 function _getApproved(uint256 tokenId) internal view virtual returns (address) {
     return _tokenApprovals[tokenId];
 }

 // 等价效果
 function getApproved(uint256 tokenId) public view virtual returns (address) {
    if (_ownerOf(tokenId) == address(0)) {
        revert ERC721NonexistentToken(tokenId);
    }
    return _tokenApprovals[tokenId];
}
  • 查看某个 token 是否已经单独授权给某个地址

被授权的地址 vs token 所有者 的区别

类型 地址 权限 能转让 NFT? 能取消授权?
👑 Owner 实际拥有者 _ownerOf(tokenId) 完全控制权
🛂 Approved approve(to, tokenId) 设置的地址 可转指定 NFT ✅(仅该 token) ❌(Owner 才能取消)
🧰 Operator setApprovalForAll(operator, true) 设置的地址 可转 owner 所有 NFT ✅(任意 token) ❌(Owner 才能取消)

总结对比图

┌────────────────────┐
│      Owner         │
│                    │
│ - 完全控制         │
│ - 可授权别人       │
└────────┬───────────┘
         │
         ▼
  ┌──────────────┐          ┌────────────────────┐
  │ approve()    │  单 token 授权 → 一次一个      │
  │ tokenId 1 → Bob ─────────> Bob 能操作 tokenId 1
  └──────────────┘

  ┌──────────────┐          ┌────────────────────┐
  │ setApproval  │ 所有 token 授权 → 多个 operator │
  │ operator = Eve ────────> Eve 可操作 Owner 所有 NFT
  └──────────────┘

疑问&思考:拥有者可以授权别人帮他操作 NFT

问:授权后的 NFT 真的安全吗?会不会被授权者随便转走? 答:一旦你授权了某个地址(单个 token 或全部 token),这个地址就真的可以把你的 NFT 转走

🧱 如何防止 NFT 被意外转走?

场景 安全建议
✅ 没有要转移的需求 不要随便调用 approve()setApprovalForAll()
✅ 和 DApp 互动时 每次授权后,及时用 approve(address(0), tokenId) 取消授权
✅ 对整账户授权 在不需要时,及时用 setApprovalForAll(operator, false) 撤销
✅ 想知道 NFT 是否授权 查看 Etherscan 上的 token 授权记录(例如 OpenSea、Blur 等)
✅ 自动化 使用 Wallet Guard 等工具拦截潜在风险授权

🚨真实案例:

  • 很多 NFT 盗窃事件,并不是黑客破坏了合约,而是用户自己 approve 给了钓鱼地址。
  • 钓鱼合约伪装成“空投/白名单验证/游戏网站”,诱导你签署授权交易。
  • 用户一旦授权,该地址立刻调用 transferFrom() 转走你的 NFT——而这是“合法操作”,你无法追回

授权即赋权,慎之又慎!\ 只要你 approve()setApprovalForAll() 给了别人,他们就能随时合法转走你的 NFT,而这并不是 bug,而是设计如此。


🚚 转账核心函数


🧾 transferFrom

  • transferFrom() -> _update()
 function transferFrom(address from, address to, uint256 tokenId) public virtual {
        if (to == address(0)) {
            revert ERC721InvalidReceiver(address(0));
        }

        address previousOwner = _update(to, tokenId, _msgSender());
        if (previousOwner != from) {
            revert ERC721IncorrectOwner(from, tokenId, previousOwner);
        }
    }

// to: 目标地址(可以是 mint 的收件人、transfer 的接收方、或 burn 时是 address(0))
// tokenId: NFT 的编号
// auth: 发起请求者(如 _msgSender(),用于权限校验。为 `address(0)` 时跳过授权检查
// 返回值是 from,即之前的拥有者地址

function _update(address to, uint256 tokenId, address auth) internal virtual returns (address) {
        // 获取当前 token 的持有者:读取 _owners[tokenId]
        address from = _ownerOf(tokenId);

        // 如果传入了授权者(auth)地址,检查是否被授权操作这个 token
        if (auth != address(0)) {
            _checkAuthorized(from, auth, tokenId);
        }
        //  如果这个 token 当前已经存在(即非 mint 情况):
        //  1. 清除授权:调用 `_approve()` 清除 `tokenId` 的批准地址
        //  2. 未触发 Approval 事件(传了 `emit=false`),更节省 gas
        //  3. 余额减少(unchecked 避免溢出检查)
        if (from != address(0)) {
            _approve(address(0), tokenId, address(0), false); // 清除授权

            unchecked {
                _balances[from] -= 1;
            }
        }
        // 如果不是 burn 行为,即:目标地址不是零地址
        // 目标地址加上余额
        if (to != address(0)) {
            unchecked {
                _balances[to] += 1;
            }
        }
        // 更新 `_owners` 映射,把 tokenId 的所有权转到 `to`
        // burn 时 `to == address(0)`,也就等于清除所有权
        _owners[tokenId] = to;

        // 统一生命周期事件,不管是 mint、transfer、burn,都发 `Transfer` 事件
        emit Transfer(from, to, tokenId);
       // 返回之前的 owner,用于上层函数继续做一致性检查
        return from;
}

// _checkAuthorized() 会确保:
//    auth == from(本人)
//   或 auth == getApproved(tokenId)(已被授权地址)
//   或 isApprovedForAll(from, auth)(operator)
//   ⬅️ 这是所有授权流程的统一验证口。
function _checkAuthorized(address owner, address spender, uint256 tokenId) internal view virtual {
        if (!_isAuthorized(owner, spender, tokenId)) {   // 核心验证逻辑 
            if (owner == address(0)) {
                revert ERC721NonexistentToken(tokenId);
            } else {
                revert ERC721InsufficientApproval(spender, tokenId);
            }
        }
}

function _isAuthorized(address owner, address spender, uint256 tokenId) internal view virtual returns (bool) {
       //  spender 是所有者  或 全局授权者 或 单个token授权者
        return  spender != address(0) &&
            (owner == spender || isApprovedForAll(owner, spender) || _getApproved(tokenId) == spender);
    }

🔍 解读:

  1. 接收者地址不能为 0 地址:防止 NFT 被转入 “黑洞地址”
  2. 调用 _update() 更新 owner:【_update() 是 v5 的新内部逻辑方法】
    • tokenId 是否存在
    • _isAuthorized 验证:是否为 owner、被授权者或 operator
    • 更新 _owners[tokenId] 映射
    • 清除授权
    • 触发 Transfer 事件
      1. 安全性检查
    • _update() 虽完成了转账逻辑,但为了防止误调用或攻击行为(比如参数伪造),再进行一次检查:
    • from 参数必须等于真正的所有者。
    • 如果不一致,说明外部传了错误的 from 参数(可能是攻击行为)

🧠 补充理解 _update(): OpenZeppelin v5 将许多“共用的 NFT 转移逻辑”抽象成 _update() 方法,在 mint/burn/transfer 时都可以复用。其作用包括:

  • 检查 token 是否存在;
  • 验证调用者是否被授权;
  • 更新 token 的 owner;
  • 清除旧授权;
  • 发出 Transfer 事件;
  • 调用前后转移钩子(可重写 _beforeTokenTransfer / _afterTokenTransfer);

🧾 _isAuthorized

function _isAuthorized(address owner, address spender, uint256 tokenId) internal view virtual returns (bool) {
    return (spender == owner ||
            isApprovedForAll(owner, spender) ||
            getApproved(tokenId) == spender);
}
  • 判断调用者(spender)是否为:
    • token 所有者;
    • 所有权代理 operator;
    • 或对该 token 授权过。

🧾 safeTransferFrom

相较于普通的 transferFrom,它多了一个重要的功能 —— 确保接收方(to)是一个可以处理 ERC721 的合约,防止 NFT 被转到不能处理它的合约地址后“卡住”

  • 提供了2个重载版本
function safeTransferFrom(address from, address to, uint256 tokenId) public virtual {
    safeTransferFrom(from, to, tokenId, "");
}

function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual {
   transferFrom(from, to, tokenId);
   ERC721Utils.checkOnERC721Received(_msgSender(), from, to, tokenId, data);
}

// utils/ERC721Utils.sol
function checkOnERC721Received(
        address operator,
        address from,
        address to,
        uint256 tokenId,
        bytes memory data
    ) internal {
        // 判断 `to` 是否是合约地址:`EOA` 的 `code.length` 恒为 0;合约地址则大于 0
        if (to.code.length > 0) { // 是合约地址
            // `try/catch` 用于安全调用目标合约(to)的 onERC721Received() 方法
            // 若合约未实现该接口,将进入 `catch` 处理。
            // 返回值 `retval` 应该等于 `IERC721Receiver.onERC721Received.selector` 【函数选择器】
            try IERC721Receiver(to).onERC721Received(operator, from, tokenId, data) returns (bytes4 retval) {
                // 合约(to)实现了接口但返回了错误的 selector,也会视为非法接收者
                if (retval != IERC721Receiver.onERC721Received.selector) {
                    // Token rejected
                    revert IERC721Errors.ERC721InvalidReceiver(to);
                }
            } catch (bytes memory reason) {
              // reason 为空,则说明目标合约根本没实现该接口,直接 revert
                if (reason.length == 0) {
                    revert IERC721Errors.ERC721InvalidReceiver(to);
                } else {
                    // 合约有自定义错误或 revert 消息),则原样抛出错误信息,方便开发者调试
                    assembly ("memory-safe") {
                        revert(add(32, reason), mload(reason))
                    }
                }
            }
        }
    }

🔍 解读:

  1. transferFrom(from, to, tokenId);

    • 标准的转移操作
    • 调用 _update() 更新内部状态(_balances、_owners、清除授权等)
    • 做授权检查(是否是 owner 或已被授权地址)。
  2. ERC721Utils.checkOnERC721Received(...):关键的安全性步骤

    • 如果 to 是一个合约地址,它必须实现 IERC721Receiver.onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) 接口。
    • 如果不实现,转账会 revert,防止 NFT 被锁死在不支持 ERC721 的合约里。

_mint 函数:创建 NFT

将一个新 NFT mint 给指定地址。必须保证:

  1. Token 之前不存在。
  2. to 地址合法。
  3. 正确触发 Transfer(address(0), to, tokenId) 事件
function _mint(address to, uint256 tokenId) internal {
        if (to == address(0)) {
            revert ERC721InvalidReceiver(address(0));
        }
        address previousOwner = _update(to, tokenId, address(0)); 
        if (previousOwner != address(0)) {
            revert ERC721InvalidSender(address(0));
        }
 }

🔍 解读:

  1. 目标地址不能为 0:表示不能给“黑洞地址”发 NFT。
  2. _update(address to, uint256 tokenId, address auth)
    • tokenId 的所有权设为 to
    • 更新 _balances[to] += 1
    • _owners[tokenId] = to
    • 触发 Transfer(from, to, tokenId) 事件,此时 from == address(0)
    • 返回 previousOwner —— 如果该 tokenId 已经存在,则返回已有 owner 其中 auth = address(0),表示无授权检查(因为 mint 是创建,不涉及授权)
  3. 检查 token 是否已存在(防止重复 mint) *previousOwner != address(0),说明该 tokenId 已经存在
    • 抛出 ERC721InvalidSender(address(0))意思是不能从零地址向一个已经存在的 token 发起 mint

🔥 _burn 函数:销毁 NFT

  • 销毁一个已存在的 token(即将其 owner 设置为 address(0));
  • 触发 Transfer(from, address(0), tokenId) 事件;
  • 如果该 token 不存在,抛出异常;
  • 不进行任何权限检查(授权逻辑留给上层控制)
function _burn(uint256 tokenId) internal {
        address previousOwner = _update(address(0), tokenId, address(0));
        if (previousOwner == address(0)) {
            revert ERC721NonexistentToken(tokenId);
        }
}

🔍 解读:

  1. _update(address to, uint256 tokenId, address auth)
    • to = address(0),意味着要销毁 token。
    • auth = address(0),代表不校验授权(因为这个函数是内部调用,外层一般已做检查
    • _update 会自动做这些事: a. 获取 token 的当前所有者 b. 清除该 token 的授权 c. 扣减当前所有者余额 d. 清除 _owners[tokenId] 映射; e. 触发 Transfer(previousOwner, address(0), tokenId) 事件

🚨 如果该 token 从未存在,那么 _ownerOf(tokenId) 返回 0,说明当前 from == address(0),也就是说,尝试销毁一个不存在的 token

  1. 判断 token 是否存在
    • _update() 会返回 token 原本的 owner(即 from)
    • 如果是 address(0),说明这个 token 从未存在,抛出错误:ERC721NonexistentToken(tokenId)

附录

IERC721.sol 源码

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC721/IERC721.sol)

pragma solidity ^0.8.20;

import {IERC165} from "../../utils/introspection/IERC165.sol";

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 balance);

    function ownerOf(uint256 tokenId) external view returns (address owner);

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

    function safeTransferFrom(address from, address to, uint256 tokenId) external;

    function transferFrom(address from, address to, uint256 tokenId) external;

    function approve(address to, uint256 tokenId) external;

    function setApprovalForAll(address operator, bool approved) external;

    function getApproved(uint256 tokenId) external view returns (address operator);

    function isApprovedForAll(address owner, address operator) external view returns (bool);
}

IERC721Metadata.sol 源码

pragma solidity ^0.8.20;

import {IERC721} from "../IERC721.sol";

interface IERC721Metadata is IERC721 {
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function tokenURI(uint256 tokenId) external view returns (string memory);
}
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Henry Wei
Henry Wei
Web3 探索者