ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

WTF-Solidity 实战笔记:BanaCat NFT 合约源码分析(一)——ERC721 全部读函数拆解

2026/9/15 21:59:55 拓冰建站 浏览量
WTF-Solidity 实战笔记:BanaCat NFT 合约源码分析(一)——ERC721 全部读函数拆解 WTF-Solidity 实战笔记BanaCat NFT 合约源码分析一——ERC721 全部读函数拆解【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity本文基于 WTF-Solidity 仓库 Topics/StudyNotes 中的学习笔记对一个部署在 Polygon 链上的真实 NFT 项目BanaCat香蕉猫的合约源码进行逐函数拆解聚焦合约中所有读函数view类查询函数。读完本文你将掌握balanceOf、ownerOf、getApproved、isApprovedForAll、walletOfOwner、tokenURI、supportsInterface等 ERC721 核心读函数的底层实现思路理解它们背后的映射表数据结构并能把 WTF-Solidity 仓库中的 ERC721 教学实现 与真实商业项目代码一一对应起来。关于 BanaCat 项目BanaCat 一期是一个部署在 Polygon 区块链上的头像类数字艺术品PFPProfile Picture项目定位为半公益性质供交流与学习使用因此铸造成本被定得很低。项目配套开发了表情包周边其中香蕉猫看戏篇已上架微信表情包商城。合约的读函数是理解整个合约的入口它们只能读取合约状态、不会改变合约状态因此既是最容易被审计的部分也最能反映合约的数据结构设计。本文分析的合约本质上是一个标准 ERC721 合约并叠加了 ERC721Enumerable、Ownable、Pausable 等扩展能力其读函数大体可以分为四类分类函数作用资产查询balanceOf()查询某地址持有的 NFT 数量资产查询ownerOf()查询某tokenID的归属地址资产查询totalSupply()查询已被 mint 且可追踪的 NFT 总量资产查询walletOfOwner()返回某地址名下的全部tokenID列表资产查询tokenOfOwnerByIndex()返回某地址在指定索引处的tokenID资产查询tokenByIndex()返回全局索引对应的tokenID授权查询getApproved()查询某个 NFT 被授权给了哪个地址授权查询isApprovedForAll()查询 owner 是否将全部 NFT 批量授权给 operator铸造配置cost单个 NFT 的 mint 价格铸造配置maxMintAmount()单次最大铸造数量铸造配置maxSupply()当前还可以 mint 的数量铸造配置isPaused()mint 活动的开关状态元数据name()/symbol()项目名称与代号元数据baseExtension()Metadata 文件格式后缀元数据tokenURI()某tokenID对应的元数据地址权限与能力owner()合约当前拥有者权限与能力supportsInterface()ERC165 接口能力声明BanaCat 项目中编号 1623 的雷达猫像素头像用于说明 tokenURI 元数据存储资产查询类读函数balanceOf()查询地址持有的 NFT 数量balanceOf()输入一个地址参数返回该地址在本合约中持有的 NFT 数量。其底层数据结构是一个address uint256的映射表持仓量映射函数在检查地址合法性非零地址的前提下直接从映射表中返回该地址的 NFT 数量。这一点在 WTF-Solidity 的教学实现中可以精确印证ERC721.sol 中balanceOf的实现与 BanaCat 合约如出一辙function balanceOf(address owner) external view override returns (uint) { require(owner ! address(0), owner zero address); return _balances[owner]; }ownerOf()查询 NFT 的归属地址ownerOf()输入tokenID返回该 NFT 当前的持有者地址。底层数据结构是uint256 address的_owners映射函数在判断返回地址合法非零即该 token 确实存在之后返回对应地址。对应实现见 ERC721.solfunction ownerOf(uint tokenId) public view override returns (address owner) { owner _owners[tokenId]; require(owner ! address(0), token doesnt exist); }totalSupply()已铸造总量totalSupply()显示目前已被 mint 且可被追踪到的 NFT 数量。它是 ERC721Enumerable 扩展库提供的接口解决的是链上流通总量可枚举的需求。tokenByIndex() 与 tokenOfOwnerByIndex()可枚举的关键标准的 ERC721 合约只提供了ownerOf()由 tokenID 查地址和balanceOf()由地址查数量但无法回答某个地址下面都有哪些 tokenID这个问题。ERC721Enumerable 扩展库的提出就是为了解决这一需求它在标准合约基础上维护了两套额外的索引结构_allTokens数组把所有被 mint 的 tokenID 按顺序记录tokenByIndex()返回索引值在_allTokens数组中对应的tokenID便于全局遍历_ownedTokens二级映射记录(地址 (NFT索引 NFT tokenID))tokenOfOwnerByIndex(owner, index)返回地址owner在索引index处对应的tokenID。仓库中的 OpenZeppelin 参考实现位于 ERC721Enumerable.sol其接口定义见 IERC721Enumerable.sol。walletOfOwner()一次拿回地址下所有 tokenIDwalletOfOwner()返回输入地址名下的全部 NFT tokenID 列表是 NFT 项目前端展示用户资产时最常用的查询函数。它的执行过程分四步读取(地址 (NFT索引 NFT tokenID))二级映射_ownedTokens把某个地址下的所有 NFT 编号记录下来先调用balanceOf(_owner)查询当前地址拥有多少个 NFT以此为长度建立一个临时数组tokenIds循环调用tokenOfOwnerByIndex(_owner, i)取回地址在索引i位置对应的 NFT 编号依次写入临时数组tokenIds返回整个tokenIds数组。作者在笔记中还记录了一个 gas 优化的思考理论上也可以建立一个地址 address[]的映射实现同样功能但后者会随着 NFT 交易而频繁操作数组空间从而增加 gas 消耗因此 ERC721Enumerable 采用索引映射而非动态数组的设计更省 gas。例如实际调用walletOfOwner()查询某个地址返回结果可能是[12, 11, 1199, 10, 521, 1081]这样的 tokenID 列表。由于数组索引从 0 开始0 号位置查出来的就是 tokenID 为 12 的 NFT授权查询类读函数getApproved()查询单枚 NFT 的授权对象getApproved()是 ERC721 标准的核心函数之一用于查询某个 NFT 被授权给了哪个地址。它通过tokenID address的授权映射表_tokenApprovals建立 NFT 编号到授权地址之间的映射在检查tokenID合法性该 token 存在之后返回授权地址。实现见 ERC721.solfunction getApproved(uint tokenId) external view override returns (address) { require(_owners[tokenId] ! address(0), token doesnt exist); return _tokenApprovals[tokenId]; }getApproved()需要配合approve()单枚授权和setApprovalForAll()批量授权使用三者共同构成 NFT 的授权体系。一个典型应用是NFT 持有者先调用approve()把某枚 NFT 授权给代理地址再由代理地址调用safeTransferFrom()完成转移从而在持有者不直接签名转移交易的情况下完成 NFT 流转。isApprovedForAll()查询是否批量授权isApprovedForAll()检查 NFT 的 owner 是否把自己当前地址下的所有NFT 都授权给了operator地址。底层数据结构是一个二级映射owner address (operator address bool)。当 bool 为true时表示 owner 将自己的全部 NFT 授权给了 operator为false时表示没有授权或已取消授权。对应实现见 ERC721.solfunction isApprovedForAll(address owner, address operator) external view override returns (bool) { return _operatorApprovals[owner][operator]; }铸造配置类读函数costmint 单价cost是 mint 单个 NFT 的价格。BanaCat 一期的单价是0.5 ether由于合约部署在 Polygon 网络mint 一个 NFT 的实际支付是 0.5 MATIC。以太坊生态货币的计量单位是 WEI1 ETH 10^18 WEI因此0.5 ether在合约内部表示为5 * 10^17wei。作者在笔记中特别说明并非所有 NFT 项目价格都是固定的很多项目方会把定价设成变量并辅以修改变量的函数根据项目后期的公售情况动态调整价格——这也是cost被设计为可写状态而不是constant的原因。maxMintAmount()单次最大铸造数量maxMintAmount()控制单次交易中最大可铸造的 NFT 数量默认值是 5。它是一个变量而非常量后续可以通过setMaxMintAmountOneTime()之类的函数修改用于应对发售策略调整例如从白名单限购切换到公售放量。maxSupply()剩余可铸造量maxSupply()返回当前项目还可以 mint 的 NFT 数量。BanaCat 一期的 NFT 总量直接写死在合约中项目方将总量上限写进了合约状态通过maxSupply对外暴露配合totalSupply()即可随时计算发售进度。笔记作者以幽默的口吻提示总量数字与作者本人有些关系实际发行量以链上合约读出的数值为准。isPaused()mint 活动开关isPaused()控制 mint 活动的进行或暂停。它是一个 bool 状态变量在mint()函数内部作为是否开始 mint的开关只有isPaused false时用户才能正常铸造。这种设计对应 OpenZeppelin 的 Pausable 模式参考 ERC721Pausable.sol项目方可以在发售前把合约置于暂停状态到点后再开闸也可以在发现异常时紧急暂停。元数据类读函数name() 与 symbol()项目名称与代号name()返回项目名称如 BanaCatsymbol()返回项目代号二者原理类似都来自 ERC721Metadata 拓展接口中声明的变量并在合约的构造函数中被初始化。对应的接口定义见 IERC721Metadata.sol初始化逻辑见 ERC721.sol 构造函数constructor(string memory name_, string memory symbol_) { name name_; symbol symbol_; }baseExtension()Metadata 文件格式baseExtension()返回 Metadata 文件的格式说明。BanaCat 的元数据采用 JSON 格式因此返回.json。baseExtension同样可以在后期通过函数进行修改用于元数据存储方案迁移时例如从 json 换成其他格式保持向后兼容。tokenURI()NFT 元数据的寻址方式tokenURI()返回某个输入tokenID对应的元数据地址是理解 NFT 存储方案的关键。要理解它需要先弄清楚 NFT 元文件Metadata的存储方式。为什么元数据不能直接上链区块链的共识机制会在每个全节点上备份全网的区块数据1M 的文件在 100 个节点上备份就会产生 100M 的数据量其中 99M 都是冗余数据。这不仅造成存储空间浪费还会极大增加 NFT 的发行成本。因此当前主流的 NFT Metadata 存储方案是把 NFT 源文件存放在 IPFS 网络中链上只保存寻址所需的最少信息。Metadata 的 JSON 结构每一个 NFT 都会有一个与之对应的 JSON 文件所有文件放在一个文件夹传到 IPFS 网络后会生成一个文件夹的 CID 地址这个地址就是合约中的baseURI项目中每个 NFT 对应的文件地址都是由这个基地址拼接起来的。以下面 1623 号雷达猫的 Metadata 文件为例{ name: BanaCat #1623, // NFT 的名字 description: Pixel kitty with different styles, if youre tired of those bored Apes, come and take a look at these cute cats~ Maybe they are the most suitable NFT for avatar and your honey may love them too. Lets explore what the next kitty will wear on his/her head!, image: ipfs://QmYr9NUaom7uijzTdsjvQ57hRNV4gttnhXF7Sgvt36cUUh/1623.png, // 图像在 IPFS 网络的存储位置类似 HTTP 协议中的 URL dna: b39631c09c646593738fa44a1d8665cdb74faf08, // 以 attributes 为输入经过 hash 计算出的数据摘要保证不会有重复的图片 edition: 1623, // 相当于 ID 号 date: 1643206138987, // 生成日期 attributes: [ // 图片的属性 { trait_type: background, value: Orange }, // 背景橙色 { trait_type: head, value: Gray }, // 灰色头 { trait_type: blush, value: Pink }, // 粉色腮红 { trait_type: nose, value: Brown }, // 棕色鼻子 { trait_type: mouse, value: Smile }, // 微笑嘴巴 { trait_type: eyes, value: Blingbling }, // 闪亮眼睛 { trait_type: hat, value: Radar } // 雷达帽子致敬西电雷达通信工程 ], Author: shuxun }可见attributes里的每一项trait_type/value都对应着 NFT 图像的一个视觉特征背景、头、腮红、鼻子、嘴巴、眼睛、帽子dna则是这些属性输入 hash 算法后得到的数据摘要用于保证生成过程中不会出现重复图片。tokenURI 的拼接逻辑tokenURI()的本质就是把baseURI tokenID baseExtension拼接起来拿到tokenID对应的 NFT 在 IPFS 中的存储地址。WTF-Solidity 教学实现中的对应逻辑见 ERC721.solfunction tokenURI(uint256 tokenId) public view virtual override returns (string memory) { require(_owners[tokenId] ! address(0), Token Not Exist); string memory baseURI _baseURI(); return bytes(baseURI).length 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : ; } function _baseURI() internal view virtual returns (string memory) { return ; }注意_baseURI()是virtual的实际项目必须重写它来填入自己的 IPFS 目录 CID。仓库里的 WTFApe.sol 就是一个直观示例——它把_baseURI()覆盖为 BAYC 的 IPFS 地址从而直接复用无聊猿的元数据function _baseURI() internal pure override returns (string memory) { return ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/; }权限与能力声明类读函数owner()合约拥有者owner()返回当前合约的拥有者默认是最初发布合约的地址后期也可以通过函数将合约的拥有权移交给其他人。这是 OpenZeppelinOwnable模块的标准能力参考仓库内 Ownable.sol通常配合onlyOwner修饰符保护铸币配置等管理函数。关于归属权转移作者记录了一个非常实用的 gas 优化场景如果项目方 mint 自己的 NFT 是免费的同时又想向自己的另一个地址批量发送大量 NFT一种节约成本的做法是——先把合约的归属权owner转移给目标地址由目标地址批量 mint 一定数量 NFT之后再通过transferOwnership()把归属权转移回来。这也是项目所有权移交给社区、由 DAO 共同打理这一去中心化治理模式在合约层的雏形。supportsInterface()ERC165 接口能力声明supportsInterface()是 ERC165 标准的核心理功能用于检测合约是否实现了特定接口。以 ERC721 的 InterfaceID0x80ac58cd为例外部合约可以通过传入该 ID 询问你是不是 ERC721。WTF-Solidity 第 34 讲34_ERC721/readme.md对 ERC165 的原理做了深入讲解supportsInterface()的返回值通过比对interfaceId实现接口 ID 由接口内所有函数选择器异或XOR计算得出。关键 ID 包括InterfaceID对应接口含义0x01ffc9a7ERC165接口能力自检0x80ac58cdERC721基础 NFT 接口0x5b5e139fERC721Metadata元数据拓展接口0x780e9d63ERC721Enumerable可枚举拓展接口ERC721.sol 中的实现如下function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { return interfaceId type(IERC721).interfaceId || interfaceId type(IERC165).interfaceId || interfaceId type(IERC721Metadata).interfaceId; }由于该函数通常声明为virtual合约使用者可以继承后继续追加ERC721Enumerable等拓展接口的 ID这种接口自检 按需扩展的设计正是 ERC165 让 NFT 生态可以安全组合不同能力模块的底层支撑。读函数背后的数据结构总览把这 17 个读函数串起来看整个合约的状态设计就非常清晰了。核心的映射表与数组包括_balancesaddress uint256地址持仓量_ownersuint256 addresstokenID 归属_tokenApprovalsuint256 address单枚 NFT 授权_operatorApprovalsaddress (address bool)批量授权_ownedTokensEnumerableaddress (uint256 uint256)地址下的 tokenID 索引_allTokensEnumerableuint256[]全局 tokenID 有序数组铸造配置状态cost、maxSupply、maxMintAmount、isPaused等元数据状态name、symbol、baseURI、baseExtension。读函数不改变任何状态因此都是view类型调用不消耗 gas由节点本地执行。理解这些读函数及其背后的映射结构是进一步分析合约写函数mint、approve、transferFrom、safeTransferFrom等的基础——写函数本质上就是在安全校验后对这些映射表进行增删改并同步更新数组索引。如果你想在自己的项目中对照学习这份真实合约的设计可以参考仓库中的教学实现 ERC721.sol、接口定义 IERC721.sol 与 IERC165.sol以及 OpenZeppelin 的 ERC721Enumerable.sol 和 Ownable.sol 参考实现逐一比对即可获得完整认知。【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考