WTF Solidity 极简教程:ERC721 非同质化代币标准与 NFT 实战发行(含 ERC165 接口检测全解)
2026/9/14 20:49:38 网站建设 项目流程

WTF Solidity 极简教程:ERC721 非同质化代币标准与 NFT 实战发行(含 ERC165 接口检测全解)

【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity

本文是 WTF-Solidity 极简入门教程第 34 讲的英文版精读与实战解析。BTCETH等属于同质化代币,第一枚被挖出的BTC与第 10000 枚BTC完全等价;但现实世界中的房产、古董、虚拟艺术品等物品是非同质化的,无法用同质化代币抽象。ERC721(源自 Ethereum EIP-721 为核心,结合仓库中 34_ERC721 目录下的完整源码(ERC721.solIERC721.solIERC165.solIERC721Receiver.solIERC721Metadata.solString.solWTFApe.sol),系统讲解 EIP/ERC 的关系、ERC165接口检测原理、ERC721接口与主合约实现,并手把手在 Remix 中发行一枚基于 BAYC 元数据的免费铸造WTF ApeNFT。读完本文,你将理解为什么学习ERC721要从ERC165开始,并掌握从接口定义到合约部署、铸造、查询的完整链路。

EIP 与 ERC:Ethereum 生态标准的两个层次

本节标题是ERC721,但又会提到EIP721,二者是什么关系?

  • EIP(Ethereum Improvement Proposals,以太坊改进提案):由以太坊开发者社区提出的改进建议,是一系列按编号编排的文档,类似互联网上的 IETF RFC。EIP可以是以太坊生态中的任何改进,包括新特性、ERC 标准、协议改进、编程工具等。
  • ERC(Ethereum Request For Comment,以太坊征求意见稿):用于记录以太坊上各类应用级开发标准与协议。典型的代币标准(ERC20ERC721)、域名注册(ERC26ERC13)、URI 范式(ERC67)、Library/Package 格式(EIP82)、钱包格式(EIP75EIP85)等都属于 ERC 范畴。

ERC协议标准是影响以太坊发展的重要因素,ERC20ERC223ERC721ERC777等都对以太坊生态产生了深远影响。

结论一句话:EIP包含ERC

另外一个贯穿全文的结论是:为什么从ERC165而不是ERC721讲起?因为ERC721依赖ERC165来做接口能力声明与探测——只有先理解了ERC165,才能真正看懂ERC721的安全转账校验逻辑。如果你急于看结论,可以直接跳到文末「ERC165 与 ERC721 深度解析」一节。

ERC165:智能合约的"能力声明"标准

通过 ERC165 标准,智能合约可以声明自己实现的接口,供其他合约检查。简单说,ERC165用于检查某个智能合约是否支持ERC721ERC1155等接口。

接口合约IERC165只声明了一个supportsInterface函数:传入一个interfaceId查询时,若合约实现了该接口 ID,则返回true

interface IERC165 { /** * @dev Returns true if contract implements the `interfaceId` for querying. * See https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] for the definition of what an interface is. */ function supportsInterface(bytes4 interfaceId) external view returns (bool); }

对应仓库中的实现见 IERC165.sol。在ERC721主合约中,supportsInterface()是这样实现的:

function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { return interfaceId == type(IERC721).interfaceId || interfaceId == type(IERC165).interfaceId || interfaceId == type(IERC721Metadata).interfaceId; }

当查询的接口 ID 是IERC721IERC165IERC721Metadata时返回true,否则返回false。这就是一个合约对外宣称"我是谁、我支持什么"的机制。

IERC721:ERC721 标准接口

IERC721ERC721标准的接口合约,规定了ERC721必须实现的基本功能。它用tokenId表示特定的非同质化代币,授权或转账必须显式指定tokenId;而ERC20只需要显式指定转账金额。

/** * @dev ERC721 standard interface. */ 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); }

对应仓库源码见 IERC721.sol,注意它is IERC165,即继承了IERC165接口。

IERC721 事件(Events)

IERC721定义了三个事件,其中TransferApprovalERC20中同样存在:

  • Transfer事件:在转账时释放,记录发送方from地址、接收方to地址和代币tokenid
  • Approval事件:在授权时释放,记录授权的所有者owner地址、被授权approved地址和tokenid
  • ApprovalForAll事件:在批量授权时释放,记录批量授权的发送者owner、被授权的operator地址,以及标识operator是否被批准的approved标志。

IERC721 函数(Functions)

  • balanceOf:返回某地址持有的 NFT 数量balance
  • ownerOf:返回某tokenId的所有者owner
  • transferFrom:普通转账,参数为发送方from、接收方totokenId
  • safeTransferFrom:安全转账,若目标地址是合约,要求其实现ERC721Receiver接口。参数为发送方from、接收方totokenId
  • approve:授权另一个地址使用你的 NFT。参数为被授权的to地址和tokenId
  • getApproved:返回tokenId被授权到的地址。
  • setApprovalForAll:批量授权operator地址持有发送方拥有的全部 NFT。
  • isApprovedForAll:返回某地址的 NFT 是否被批量授权给另一个operator地址持有。
  • safeTransferFrom:安全转账的重载函数,参数中多了data

IERC721Receiver:防止 NFT 落入"黑洞"

如果某个合约没有实现ERC721的相关函数,转入的 NFT 就会被卡住无法转出,造成代币损失。为防止意外转账,ERC721实现了safeTransferFrom(),目标合约必须实现IERC721Receiver接口才能接收ERC721代币,否则交易会revertIERC721Receiver接口只包含一个onERC721Received()函数:

// ERC721 receiver interface: Contracts must implement this interface to receive ERC721 tokens via safe transfers. interface IERC721Receiver { function onERC721Received( address operator, address from, uint tokenId, bytes calldata data ) external returns (bytes4); }

对应仓库源码见 IERC721Receiver.sol。

_checkOnERC721Received:安全转账的守护者

ERC721主合约通过_checkOnERC721Received确保目标合约实现了onERC721Received()函数(返回值须为onERC721Received的 selector):

function _checkOnERC721Received( address from, address to, uint tokenId, bytes memory _data ) private returns (bool) { if (to.isContract()) { return IERC721Receiver(to).onERC721Received( msg.sender, from, tokenId, _data ) == IERC721Receiver.onERC721Received.selector; } else { return true; } }

仓库中的最新版(34_ERC721/ERC721.sol)对该函数做了更健壮的演进:用try/catch包裹调用,引入error ERC721InvalidReceiver(address receiver),当返回的retval不是onERC721Received.selector时直接revert ERC721InvalidReceiver(to);若外部调用本身失败且无错误原因(reason.length == 0)也回滚该错误,有错误原因则通过assembly原样向上传播。相比"返回 bool 让上层 require"的旧写法,新版对接收方契约异常的处理更精确、gas 更友好。

IERC721Metadata:ERC721 的元数据扩展

IERC721MetadataERC721的扩展接口,实现了 3 个常用的查询metadata的函数:

  • name():返回代币的名称。
  • symbol():返回代币的代号(Symbol)。
  • tokenURI():通过tokenId查询并返回metadata的 URL,这是ERC721独有的功能。
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); }

对应仓库源码见 IERC721Metadata.sol。tokenURI是 NFT 图片在网站上展示的关键:前端页面展示的 NFT 图片 URL 正是由它返回的。

ERC721 主合约:从接口到完整实现

ERC721主合约实现了IERC721IERC165IERC721Metadata定义的全部功能。它包含namesymbol两个公开变量,以及四个核心映射(_owners_balances_tokenApprovals_operatorApprovals)作为状态存储。完整实现见仓库 34_ERC721/ERC721.sol,核心要点如下:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.21; import "./IERC165.sol"; import "./IERC721.sol"; import "./IERC721Receiver.sol"; import "./IERC721Metadata.sol"; import "./Address.sol"; import "./String.sol"; contract ERC721 is IERC721, IERC721Metadata{ using Address for address; // 使用Address库的isContract检查地址是否为合约 using Strings for uint256; // 使用String库 // Token名称 string public override name; // Token代号 string public override symbol; // tokenId 到 owner address 的持有人映射 mapping(uint => address) private _owners; // address 到 持仓数量 的持仓量映射 mapping(address => uint) private _balances; // tokenID 到 授权地址 的授权映射 mapping(uint => address) private _tokenApprovals; // owner地址 到 operator地址 的批量授权映射 mapping(address => mapping(address => bool)) private _operatorApprovals; /** * 构造函数,初始化`name` 和`symbol` . */ constructor(string memory name_, string memory symbol_) { name = name_; symbol = symbol_; } // ...(supportsInterface / balanceOf / ownerOf / 授权 / 转账 / 铸造 / 销毁等函数) }

状态变量与数据结构

状态变量类型作用
namestring代币名称,如WTF
symbolstring代币代号,如WTF
_ownersmapping(uint => address)记录每个tokenId的持有人
_balancesmapping(address => uint)记录每个地址持有的 NFT 数量
_tokenApprovalsmapping(uint => address)记录每个tokenId被授权的地址
_operatorApprovalsmapping(address => mapping(address => bool))记录 owner 对 operator 的批量授权关系

核心内部函数与调用链

主合约将"权限校验"与"状态变更"拆分为内部函数,供外部函数复用:

  • _approve(owner, to, tokenId):私有函数,更新_tokenApprovals[tokenId] = to并释放Approval事件。外部approve先校验msg.sender == owner || _operatorApprovals[owner][msg.sender](即调用者是 owner 或已被批量授权),再调用它。
  • _isApprovedOrOwner(owner, spender, tokenId):判断spender是否为 owner、单枚授权地址或批量授权 operator,三者满足其一即通过。
  • _transfer(owner, from, to, tokenId):核心转账逻辑。校验from == ownerto != address(0),清空该 token 的授权(_approve(owner, address(0), tokenId)),更新_balancesfrom减 1、to加 1)与_owners[tokenId] = to,释放Transfer事件。transferFrom在调用它之前先做_isApprovedOrOwner校验。
  • _safeTransfer:先_transfer,再require(_checkOnERC721Received(...), "not ERC721Receiver"),保证 NFT 不会永久锁死在合约里。
  • _mint(to, tokenId):内部虚函数,校验to != address(0)_owners[tokenId] == address(0)(token 尚未铸造),更新余额与归属并释放Transfer(address(0), to, tokenId)任何人都能直接调用它,实际使用时开发者必须重写并加上权限、数量等条件。
  • _burn(tokenId):销毁函数,要求msg.sender == owner,清空授权、更新余额、删除_owners[tokenId],释放Transfer(owner, address(0), tokenId)
  • tokenURI/_baseURItokenURI_baseURI()tokenId.toString()(来自 String.sol 库)拼接返回;_baseURI默认返回空字符串,需开发者重写。

两种 transferFrom 的区别

  • transferFrom:普通转账,不检查接收方能力,可能把 NFT 转入"黑洞"合约,官方建议谨慎使用。
  • safeTransferFrom:安全转账,转账后调用_checkOnERC721Received,目标为合约时必须实现IERC721Receiver,否则回滚。

实战:写一个免费铸造的 WTF Ape

现在用ERC721写一个总量10000的免费铸造WTF APE。我们只需要重写mint()baseURI()两个函数。baseURI()直接沿用BAYC的元数据地址,让 NFT 的元数据直接使用那些"无聊猿"的信息(类似 RRBAYC 的做法)。完整代码见 34_ERC721/WTFApe.sol:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.21; import "./ERC721.sol"; contract WTFApe is ERC721{ uint public MAX_APES = 10000; // 总量 // 构造函数 constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_){ } // BAYC的baseURI为ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ function _baseURI() internal pure override returns (string memory) { return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/"; } // 铸造函数 function mint(address to, uint tokenId) external { require(tokenId >= 0 && tokenId < MAX_APES, "tokenId out of range"); _mint(to, tokenId); } }

要点说明:

  • MAX_APES = 10000限定了 NFT 总量;mint中的require(tokenId >= 0 && tokenId < MAX_APES, "tokenId out of range")确保铸造的tokenId不越界。
  • _baseURI重写为 BAYC 的ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/,则tokenURI(0)会返回ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/0,前端据此渲染图片。
  • 真实项目里mint通常要加上白名单、公售开关、价格、单地址限额等条件;这里的mint仅用于教学演示。

在 Remix 中发行自己的 ERC721 NFT

有了ERC721标准,在 ETH 链上发行 NFT 变得非常简单。下面在 Remix 中实际操作一遍:

  1. 按顺序编译ERC721合约与WTFApe合约(WTFApe依赖ERC721及其接口文件)。
  2. 进入部署栏,在构造函数参数区输入name_symbol_(均设为WTF),点击transact按钮部署。

  1. 部署成功后,在Deployed Contracts面板中找到WTFApe合约。调用mint()给自己铸造 NFT:在mint函数面板输入账户地址和tokenId(例如0),点击mint按钮,即可给自己铸造0WTFNFT。

铸造后点击右侧 Debug 按钮可以查看日志,其中包含四类关键信息:

  • 事件Transfer
  • 铸造来源地址0x0000000000000000000000000000000000000000(零地址表示铸造)
  • 接收地址(如0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
  • Token id0
  1. balanceOf()查询账户余额:输入当前账户地址,返回值显示已成功持有 1 个 NFT。

  1. ownerOf()查询归属:输入tokenId(如0),返回该 NFT 的所有者地址,与铸造地址一致。

ERC165 与 ERC721 深度解析:从接口 ID 到能力探测

回到开头的问题:为什么ERC721依赖ERC165?前面提到,为了防止 NFT 被转到无法处理它的合约,目标地址必须正确实现ERC721TokenReceiver接口:

interface ERC721TokenReceiver { function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4); }

借用编程语言的概念:无论是 Java 的 interface 还是 Rust 的 Trait(在 Solidity 中更接近"函数选择器集合"而非 Trait),接口都意味着"一组行为的集合"。某类型实现了某接口,就说明该类型具备某项能力。因此,只要某合约类型实现了上述ERC721TokenReceiver接口(具体说是实现了onERC721Received函数),就对外表明它有管理 NFT 的能力(操作 NFT 的逻辑则实现在合约的其他函数中)。

执行ERC721标准中的safeTransferFrom时,合约会检查目标合约是否实现了onERC721Received函数——这正是基于ERC165思想的操作。

那么 ERC165 究竟是什么?

ERC165是一种对外表明"合约实现了哪些接口"的技术标准。如前述,实现某接口意味着合约拥有某项特殊能力;当合约之间交互时,一方期望另一方具备某些能力,就可以通过ERC165标准互相查询。以一个合约为例,如何检查它是否实现ERC721?按照 EIP-165 的检测步骤:先检查合约是否实现ERC165,再检查它实现的具体接口(此时即IERC721)。为什么称IERC721为"基础接口"?因为还有ERC721MetadataERC721Enumerable等扩展接口。

接口 ID 的魔法数字:0x80ac58cd、0x01ffc9a7、0x5b5e139f、0x780e9d63

ERC721的接口 ID0x80ac58cd由 EIP-165 规定的异或计算方法得到:

bytes4(keccak256(ERC721.Transfer.selector) ^ keccak256(ERC721.Approval.selector) ^ ··· ^ keccak256(ERC721.isApprovedForAll.selector))
/// Please note this **0x80ac58cd** /// **⚠⚠⚠ Note: the ERC-165 identifier for this interface is 0x80ac58cd. ⚠⚠⚠** interface ERC721 /* is ERC165 */ { 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 safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable; function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; function transferFrom(address _from, address _to, uint256 _tokenId) external payable; function approve(address _approved, uint256 _tokenId) external payable; function setApprovalForAll(address _operator, bool _approved) external; function getApproved(uint256 _tokenId) external view returns (address); function isApprovedForAll(address _owner, address _operator) external view returns (bool); }

同理可以计算ERC165自身的接口 ID(仅含function supportsInterface(bytes4 interfaceID) external view returns (bool);一个函数),即bytes4(keccak256(supportsInterface.selector)),结果为0x01ffc9a7

ERC721还定义了扩展接口ERC721Metadata,其接口 ID 为0x5b5e139f

/// Note: the ERC-165 identifier for this interface is 0x5b5e139f. interface ERC721Metadata /* is ERC721 */ { function name() external view returns (string _name); function symbol() external view returns (string _symbol); function tokenURI(uint256 _tokenId) external view returns (string); // 非常重要:网站上 NFT 图片的 URL 就是由该函数返回的 }

0x5b5e139f的计算方式为:

IERC721Metadata.name.selector ^ IERC721Metadata.symbol.selector ^ IERC721Metadata.tokenURI.selector

Solmate 式实现:4 行代码满足 ERC165

以 Solmate 的ERC721.sol为例,supportsInterface的实现简洁至极:

function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { return interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata }

外部按照 EIP-165 的检测步骤查询时:

  • 检查是否实现ERC165:输入参数0x01ffc9a7supportsInterface必须返回true,输入0xffffffff时必须返回false——上述实现完全符合要求;
  • 检查是否ERC721:输入参数0x80ac58cd,返回true即可;
  • 检查是否实现ERC721Metadata扩展:输入参数0x5b5e139f,返回true即可。

由于该函数是virtual,合约使用者可以继承后继续实现ERC721Enumerable接口(如totalSupply等函数),并重写supportsInterface追加对应接口 ID:

function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { return interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 interfaceId == 0x5b5e139f || // ERC165 Interface ID for ERC721Metadata interfaceId == 0x780e9d63; // ERC165 Interface ID for ERC721Enumerable }

优雅、简洁、可扩展性拉满——这正是ERC165+ERC721设计的精妙之处。

总结

本讲介绍了ERC721的标准、接口与实现:从EIPERC的关系出发,依次讲解了ERC165能力声明机制、IERC721的三类事件与九个函数、IERC721Receiver安全接收接口、IERC721Metadata元数据扩展,以及ERC721主合约基于四个映射的状态管理(铸造、授权、转账、销毁、查询的完整调用链);随后用ERC721创建了免费铸造的WTF APENFT(元数据直接调用 BAYC 的 IPFS 地址),并在 Remix 中完成了部署、铸造、余额与归属查询的完整实战。

ERC721标准仍在演进,目前流行的版本包括ERC721Enumerable(提升 NFT 的可枚举/可查询性)与ERC721A(节省铸造gas)。本仓库中对应英文版源码位于 Languages/en/34_ERC721_en,中文版源码与教程见 34_ERC721,可以对照阅读,加深对接口分层与状态管理设计的理解。

【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询