erc-721
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseERC-721 NFT Integration for Scaffold-ETH 2
为Scaffold-ETH 2集成ERC-721 NFT
Prerequisites
前置条件
Check if exists directly in the current working directory (do not search subdirectories). If it doesn't exist, this is not a Scaffold-ETH 2 project. Follow the instructions at https://docs.scaffoldeth.io/SKILL.md to scaffold it first. If it exists, continue directly with building.
./packages/nextjs/scaffold.config.ts检查当前工作目录下是否直接存在文件(不要搜索子目录)。如果不存在,则这不是一个Scaffold-ETH 2项目,请先按照https://docs.scaffoldeth.io/SKILL.md中的说明搭建项目。如果文件存在,则直接开始构建。
./packages/nextjs/scaffold.config.tsOverview
概述
ERC-721 is the standard interface for non-fungible tokens (NFTs) on Ethereum. This skill covers adding an ERC-721 contract to a Scaffold-ETH 2 project using OpenZeppelin's ERC-721 implementation, along with deployment scripts and a frontend for minting, listing, and transferring NFTs.
For anything not covered here, refer to the OpenZeppelin ERC-721 docs or search the web. This skill focuses on what's hard to discover: SE-2 integration specifics, common pitfalls, and ERC-721 gotchas.
ERC-721是以太坊上非同质化代币(NFT)的标准接口。本技能介绍如何使用OpenZeppelin的ERC-721实现,在Scaffold-ETH 2项目中添加ERC-721合约,同时包含部署脚本以及用于铸造、展示和转账NFT的前端页面。
对于本文未覆盖的内容,请参考OpenZeppelin ERC-721文档或在网络上搜索。本技能重点介绍难以发现的细节:SE-2集成的具体事项、常见陷阱以及ERC-721的注意事项。
Dependencies
依赖项
OpenZeppelin contracts are already included in SE-2's Hardhat and Foundry setups, so no additional dependency installation is needed. If for some reason they're missing:
- Hardhat: in
@openzeppelin/contractspackages/hardhat/package.json - Foundry: installed via , with remapping
forge install OpenZeppelin/openzeppelin-contracts@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
No new frontend dependencies are required.
SE-2的Hardhat和Foundry环境中已包含OpenZeppelin合约,因此无需安装额外依赖。若因某些原因缺失:
- Hardhat:在中添加
packages/hardhat/package.json@openzeppelin/contracts - Foundry:通过安装,并添加重映射
forge install OpenZeppelin/openzeppelin-contracts@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
无需新增前端依赖项。
Smart Contract
智能合约
The token contract extends OpenZeppelin's base. Import path: . The constructor takes a token name and symbol.
ERC721@openzeppelin/contracts/token/ERC721/ERC721.solSyntax reference for a basic NFT with open minting and IPFS metadata:
solidity
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
contract MyNFT is ERC721Enumerable {
uint256 public tokenIdCounter;
constructor() ERC721("MyNFT", "MNFT") {}
function mintItem(address to) public returns (uint256) {
tokenIdCounter++;
_safeMint(to, tokenIdCounter);
return tokenIdCounter;
}
function tokenURI(uint256 tokenId) public view override returns (string memory) {
_requireOwned(tokenId);
return string.concat(_baseURI(), Strings.toString(tokenId));
}
function _baseURI() internal pure override returns (string memory) {
return "ipfs://YourCID/";
}
}Adapt the contract based on the user's requirements. Available extensions (all under ):
@openzeppelin/contracts/token/ERC721/extensions/- : on-chain enumeration of all tokens and per-owner tokens. Enables
ERC721Enumerable,totalSupply(),tokenByIndex(). Convenient but expensive (see gas section below).tokenOfOwnerByIndex() - : per-token URI storage via
ERC721URIStorage. Emits ERC-4906_setTokenURI()events in v5.MetadataUpdate - : lets token owners destroy their NFTs
ERC721Burnable - : admin can freeze all transfers
ERC721Pausable - : governance checkpoints, each NFT = 1 vote
ERC721Votes - : ERC-2981 royalty info (see royalties section below)
ERC721Royalty - : batch minting during construction (ERC-2309)
ERC721Consecutive
See OpenZeppelin's ERC-721 extensions for the full list.
代币合约继承自OpenZeppelin的基类。导入路径:。构造函数接收代币名称和符号。
ERC721@openzeppelin/contracts/token/ERC721/ERC721.sol以下是带有开放铸造功能和IPFS元数据的基础NFT合约语法示例:
solidity
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
contract MyNFT is ERC721Enumerable {
uint256 public tokenIdCounter;
constructor() ERC721("MyNFT", "MNFT") {}
function mintItem(address to) public returns (uint256) {
tokenIdCounter++;
_safeMint(to, tokenIdCounter);
return tokenIdCounter;
}
function tokenURI(uint256 tokenId) public view override returns (string memory) {
_requireOwned(tokenId);
return string.concat(_baseURI(), Strings.toString(tokenId));
}
function _baseURI() internal pure override returns (string memory) {
return "ipfs://YourCID/";
}
}可根据用户需求调整合约。可用扩展(均位于路径下):
@openzeppelin/contracts/token/ERC721/extensions/- :支持链上枚举所有代币以及每个所有者的代币。提供
ERC721Enumerable、totalSupply()、tokenByIndex()方法。使用便捷但gas成本较高(见下文gas部分)。tokenOfOwnerByIndex() - :通过
ERC721URIStorage为每个代币存储独立URI。在v5版本中会触发ERC-4906的_setTokenURI()事件。MetadataUpdate - :允许代币持有者销毁自己的NFT
ERC721Burnable - :管理员可冻结所有转账操作
ERC721Pausable - :治理检查点,每个NFT等同于1票
ERC721Votes - :ERC-2981版税信息(见下文版税部分)
ERC721Royalty - :合约构建期间批量铸造(ERC-2309)
ERC721Consecutive
完整扩展列表请查看OpenZeppelin的ERC-721扩展文档。
OpenZeppelin v5 changes to be aware of
需要注意的OpenZeppelin v5变更
If referencing older tutorials or code, note these breaking changes in OpenZeppelin v5:
- and
_beforeTokenTransferhooks are gone. Replaced by a single_afterTokenTransferoverride point that handles mint, transfer, and burn._update(address to, uint256 tokenId, address auth) - Custom errors replaced revert strings (e.g. ,
ERC721NonexistentToken)ERC721InsufficientApproval - requires explicit owner:
Ownableinstead ofOwnable(msg.sender)Ownable() - now emits ERC-4906
ERC721URIStorageevents whenMetadataUpdateis called_setTokenURI
若参考旧教程或代码,请注意OpenZeppelin v5中的以下破坏性变更:
- 和
_beforeTokenTransfer钩子已移除,替换为单个_afterTokenTransfer重写点,处理铸造、转账和销毁操作。_update(address to, uint256 tokenId, address auth) - 自定义错误替代了回退字符串(例如、
ERC721NonexistentToken)ERC721InsufficientApproval - 需要显式指定所有者:使用
Ownable而非Ownable(msg.sender)Ownable() - ****现在在调用
ERC721URIStorage时会触发ERC-4906的_setTokenURI事件MetadataUpdate
Metadata: The Part Most People Get Wrong
元数据:多数人容易出错的部分
The metadata JSON schema
元数据JSON schema
ERC-721 metadata follows a standard JSON structure returned by :
tokenURI()json
{
"name": "My NFT #1",
"description": "Description of the NFT",
"image": "ipfs://QmImageCID",
"attributes": [
{ "trait_type": "Color", "value": "Blue" },
{ "trait_type": "Rarity", "value": "Rare" }
]
}The array is not in the EIP but is the de facto standard used by OpenSea, Blur, and every other marketplace. Without it, traits won't display.
attributesERC-721元数据遵循返回的标准JSON结构:
tokenURI()json
{
"name": "My NFT #1",
"description": "Description of the NFT",
"image": "ipfs://QmImageCID",
"attributes": [
{ "trait_type": "Color", "value": "Blue" },
{ "trait_type": "Rarity", "value": "Rare" }
]
}attributesOn-chain vs off-chain metadata
链上元数据 vs 链下元数据
| Factor | On-chain (base64/SVG) | Off-chain (IPFS/Arweave) |
|---|---|---|
| Permanence | Permanent as long as Ethereum exists | Depends on pinning/persistence |
| Gas cost | Very expensive (~128KB payload ceiling) | Cheap (just store a URI string) |
| Mutability | Immutable once deployed | Can disappear if unpinned |
| Best for | Small collections, generative art | Large collections, rich media |
| 因素 | 链上(base64/SVG) | 链下(IPFS/Arweave) |
|---|---|---|
| 持久性 | 只要以太坊存在就永久保存 | 取决于固定/持久化方式 |
| Gas成本 | 非常高(约128KB payload上限) | 低廉(仅存储URI字符串) |
| 可变性 | 部署后不可变 | 若未固定则可能消失 |
| 适用场景 | 小型集合、生成艺术 | 大型集合、富媒体内容 |
IPFS gotchas
IPFS的注意事项
About 20% of sampled NFTs have broken or expired metadata links. Common causes:
- Unpinned data gets garbage collected. IPFS nodes drop data nobody is actively pinning. If the original pinner stops, the data vanishes.
- Gateway URLs vs protocol URIs. Use (content-addressed, portable) not
ipfs://QmCID(depends on one gateway staying up).https://gateway.pinata.cloud/ipfs/QmCID - Base URI must end with . OpenZeppelin's
/concatenatestokenURI(). If the base URI is_baseURI() + tokenId.toString()without a trailing slash, token 42 becomesipfs://QmCIDinstead ofipfs://QmCID42.ipfs://QmCID/42 - File naming. If using base URI + token ID, metadata files must be named ,
0,1etc. (no2extension) unless you override.jsonto append it.tokenURI()
For permanent storage, consider Arweave or a paid IPFS pinning service (Pinata, Filebase).
约20%的抽样NFT存在元数据链接损坏或过期的问题,常见原因包括:
- 未固定的数据会被垃圾回收:IPFS节点会删除无人主动固定的数据。若原始固定者停止操作,数据将消失。
- 网关URL vs 协议URI:使用(内容寻址,可移植)而非
ipfs://QmCID(依赖单个网关的可用性)。https://gateway.pinata.cloud/ipfs/QmCID - Base URI必须以结尾:OpenZeppelin的
/会将tokenURI()拼接。若Base URI为_baseURI() + tokenId.toString()且无尾部斜杠,代币42的URI会变成ipfs://QmCID而非ipfs://QmCID42。ipfs://QmCID/42 - 文件命名:若使用Base URI + 代币ID的方式,元数据文件必须命名为、
0、1等(无2后缀),除非重写.json方法来添加后缀。tokenURI()
如需永久存储,可考虑Arweave或付费IPFS固定服务(如Pinata、Filebase)。
ERC721Enumerable: Convenient but Expensive
ERC721Enumerable:便捷但成本高昂
ERC721Enumerable maintains four additional data structures that get updated on every mint and transfer. Concrete gas comparison:
- Minting 5 tokens with ERC721Enumerable: ~566,000 gas
- Minting 5 tokens with ERC721A: ~104,000 gas (5.5x cheaper)
When to use it: Small collections, learning/demos, when you need on-chain enumeration without an indexer.
When to skip it: Large collections (1k+ tokens), gas-sensitive mints. Use a simple counter for and index token ownership off-chain using events (via a subgraph or Ponder, both available as SE-2 skills).
totalSupply()TransferERC721Enumerable维护四个额外的数据结构,每次铸造和转账时都会更新。具体gas成本对比:
- 使用ERC721Enumerable铸造5个代币:约566,000 gas
- 使用ERC721A铸造5个代币:约104,000 gas(便宜5.5倍)
适用场景:小型集合、学习/演示、无需索引器即可实现链上枚举的场景。
不适用场景:大型集合(1000+代币)、对gas成本敏感的铸造。可使用简单计数器实现,并通过事件在链下索引代币所有权(可使用SE-2技能中的subgraph或Ponder)。
totalSupply()TransferERC721A as an alternative
替代方案:ERC721A
ERC721A by Azuki makes batch minting cost nearly the same as minting a single token. A 10-token mint costs ~110,000 gas vs ~1,100,000+ with ERC721Enumerable. It works by lazily initializing ownership: only the first token in a batch gets an ownership record, and later tokens infer ownership by scanning backwards.
Trade-offs:
- Requires sequential token IDs (no random IDs)
- First transfer after a batch mint is more expensive (must initialize ownership)
- Not an OpenZeppelin extension; separate dependency from npm package
erc721a
Azuki开发的ERC721A使批量铸造的成本几乎与铸造单个代币相同。铸造10个代币约需110,000 gas,而使用ERC721Enumerable则需1,100,000+ gas。它通过延迟初始化所有权实现优化:批量铸造中仅第一个代币会创建所有权记录,后续代币通过反向扫描推断所有权。
权衡点:
- 要求代币ID连续(无随机ID)
- 批量铸造后的首次转账成本更高(需初始化所有权)
- 并非OpenZeppelin扩展,需从npm包单独安装依赖
erc721a
Security: The Reentrancy You Didn't Expect
安全性:意想不到的重入漏洞
_safeMint
and safeTransferFrom
call external code
_safeMintsafeTransferFrom_safeMint
和safeTransferFrom
会调用外部代码
_safeMintsafeTransferFromBoth and invoke on the recipient if it's a contract. This is an external call that happens after the token has been minted/transferred, creating a reentrancy vector.
_safeMintsafeTransferFromonERC721Received()Real exploit (HypeBears, Feb 2022): The contract tracked per-address minting limits but updated state after . An attacker's callback called again before the limit was recorded, bypassing the per-address cap entirely.
_safeMintonERC721ReceivedmintNFTsolidity
// VULNERABLE: state update after _safeMint
function mintNFT() public {
require(!addressMinted[msg.sender], "Already minted");
_safeMint(msg.sender, tokenId); // calls onERC721Received on attacker
addressMinted[msg.sender] = true; // too late, attacker already re-entered
}
// SAFE: state update before _safeMint
function mintNFT() public {
require(!addressMinted[msg.sender], "Already minted");
addressMinted[msg.sender] = true; // update state first
_safeMint(msg.sender, tokenId);
}Mitigations: Update state before / (checks-effects-interactions pattern), or use OpenZeppelin's ( modifier).
_safeMintsafeTransferFromReentrancyGuardnonReentrant_safeMintsafeTransferFromonERC721Received()真实攻击案例(HypeBears,2022年2月):合约跟踪每个地址的铸造上限,但在之后才更新状态。攻击者的回调在状态更新前再次调用,完全绕过了地址铸造上限。
_safeMintonERC721ReceivedmintNFTsolidity
// 存在漏洞:状态更新在_safeMint之后
function mintNFT() public {
require(!addressMinted[msg.sender], "Already minted");
_safeMint(msg.sender, tokenId); // 调用攻击者的onERC721Received
addressMinted[msg.sender] = true; // 为时已晚,攻击者已重入
}
// 安全写法:先更新状态
function mintNFT() public {
require(!addressMinted[msg.sender], "Already minted");
addressMinted[msg.sender] = true; // 先更新状态
_safeMint(msg.sender, tokenId);
}缓解措施:在/之前更新状态(检查-效果-交互模式),或使用OpenZeppelin的(修饰器)。
_safeMintsafeTransferFromReentrancyGuardnonReentrantsetApprovalForAll
is a dangerous permission
setApprovalForAllsetApprovalForAll
是危险权限
setApprovalForAllsetApprovalForAll(operator, true)setApprovalForAllsetApprovalForAll(operator, true)setApprovalForAllFlash loan governance attacks
闪电贷治理攻击
NFTs used for governance (each NFT = 1 vote) can be manipulated via flash loans: borrow NFTs, vote, return them. Use with checkpoints and voting delays rather than raw for governance.
ERC721VotesbalanceOf()用于治理的NFT(每个NFT等同于1票)可能被闪电贷操纵:借入NFT、投票、归还。治理应使用带检查点和投票延迟的,而非直接使用。
ERC721VotesbalanceOf()Royalties (ERC-2981)
版税(ERC-2981)
ERC-2981 defines a standard function that returns the royalty receiver and amount. OpenZeppelin provides to implement this.
royaltyInfo(tokenId, salePrice)ERC721RoyaltyThe critical thing to know: ERC-2981 is advisory, not enforceable. The standard provides an interface for querying royalty info, but nothing forces marketplaces to honor it. Anyone can transfer an NFT via without paying royalties.
transferFromCurrent marketplace stance:
- OpenSea: ended mandatory enforcement Aug 2023. Added ERC-721C support Apr 2024 for opt-in on-chain enforcement.
- Blur: enforces only a 0.5% minimum on most collections.
ERC-721C by Limit Break attempted to solve this by restricting transfers to whitelisted operator contracts. Adoption is growing but not universal.
ERC-2981定义了标准的方法,返回版税接收方和金额。OpenZeppelin提供来实现此标准。
royaltyInfo(tokenId, salePrice)ERC721Royalty关键须知:ERC-2981为建议性标准,不具备强制性。该标准仅提供查询版税信息的接口,并无机制强制市场遵守。任何人都可通过转账NFT而无需支付版税。
transferFrom当前市场立场:
- OpenSea:2023年8月结束强制版税 enforcement,2024年4月添加ERC-721C支持以实现链上可选强制版税。
- Blur:仅对大多数集合强制收取0.5%的最低版税。
Limit Break开发的ERC-721C试图通过限制转账至白名单操作员合约来解决此问题。该标准的采用率正在增长,但尚未普及。
Soulbound Tokens (ERC-5192)
灵魂绑定代币(ERC-5192)
For non-transferable NFTs (credentials, memberships, achievements), ERC-5192 adds a minimal interface. In OpenZeppelin v5, the simplest approach is overriding :
locked(tokenId)_updatesolidity
function _update(address to, uint256 tokenId, address auth)
internal override returns (address)
{
address from = super._update(to, tokenId, auth);
require(from == address(0) || to == address(0), "Non-transferable");
return from;
}solidity
function _update(address to, uint256 tokenId, address auth)
internal override returns (address)
{
address from = super._update(to, tokenId, auth);
require(from == address(0) || to == address(0), "Non-transferable");
return from;
}Well-Known NFT Contracts (Ethereum Mainnet)
知名NFT合约(以太坊主网)
| Collection | Address | Notes |
|---|---|---|
| CryptoPunks (original) | | NOT ERC-721. Pre-dates the standard (June 2017). Custom contract with its own marketplace built in. Had a critical bug where sale ETH was credited to the buyer, not the seller. |
| Wrapped CryptoPunks | | ERC-721 wrapper around original punks |
| Bored Ape Yacht Club | | 10,000 apes, standard ERC-721 |
| Azuki | | Uses ERC721A for gas-optimized batch minting |
| Pudgy Penguins | | 8,888 penguins |
| 集合 | 地址 | 说明 |
|---|---|---|
| CryptoPunks(原版) | | 非ERC-721标准。早于该标准(2017年6月)。自定义合约,内置市场。曾存在严重漏洞:销售ETH会转入买家而非卖家账户。 |
| Wrapped CryptoPunks | | 原版CryptoPunks的ERC-721包装合约 |
| Bored Ape Yacht Club | | 10,000只猿猴,标准ERC-721合约 |
| Azuki | | 使用ERC721A实现gas优化的批量铸造 |
| Pudgy Penguins | | 8,888只企鹅 |