引言
NFT(非同质化代币)作为区块链技术的重要应用场景,已经从最初的数字艺术领域扩展到了游戏道具、身份认证、证书存证等众多领域。对于区块链开发者而言,掌握ERC721合约开发是一项必备技能。本教程将带领读者从零开始,构建一个功能完整的NFT智能合约,涵盖从标准接口理解到实际部署的全流程。
在开始之前,我们需要明确一个核心概念:什么是非同质化代币?与ERC20这类同质化代币不同,NFT具有唯一性和不可分割性。每一枚NFT都是独一无二的,就像现实世界中的收藏品。这种特性使得NFT特别适合用于代表数字艺术品、游戏道具、房地产契约等独特资产。

ERC721标准接口解析
ERC721标准定义了NFT合约必须实现的核心接口。这些接口确保了不同平台之间的互操作性,让NFT可以在各种交易市场、钱包和应用之间自由流通。
IERC721接口定义
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol";
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
IERC721接口包含以下核心函数。首先是balanceOf,用于查询指定地址持有的NFT数量:
solidity
function balanceOf(address owner) external view returns (uint256);
ownerOf函数则用于查询指定TokenID的所有者:
solidity
function ownerOf(uint256 tokenId) external view returns (address);
transferFrom是最基础的转账函数,允许当前所有者将NFT转移给另一个地址:
solidity
function transferFrom(address from, address to, uint256 tokenId) external payable;
除了基础的transferFrom,ERC721还提供了safeTransferFrom函数。这个函数会在转账前检查接收方是否为智能合约,如果是合约地址,它会调用合约的onERC721Received函数,确保合约能够安全接收NFT。这种设计防止了NFT被锁定在不支持NFT的合约中。
approve函数允许当前所有者授权另一个地址代表其转移特定NFT:
solidity
function approve(address to, uint256 tokenId) external payable;
setApprovalForAll则用于批量授权,允许授权某个地址代表所有者转移其持有的所有NFT:
solidity
function setApprovalForAll(address operator, bool approved) external;
getApproved和isApprovedForAll分别用于查询单个NFT的授权情况和某个操作员是否获得全局授权。
元数据扩展接口
IERC721Metadata接口允许合约提供NFT的名称、符号以及每个Token的元数据URI:
solidity
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function tokenURI(uint256 tokenId) external view returns (string memory);
其中tokenURI是最关键的一个函数。它返回一个URI字符串,指向该NFT的元数据JSON文件。这个JSON文件包含了NFT的名称、描述、图片URL等详细信息。OpenSea等NFT交易平台正是通过这个URI来获取和展示NFT的。
完整NFT合约实现
理解了ERC721标准后,我们来构建一个完整的NFT合约。这个合约将包含Mint功能、元数据管理以及基本的安全机制。
合约基础结构
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyNFT is ERC721, ERC721URIStorage, Ownable {
uint256 private _nextTokenId;
constructor(address initialOwner)
ERC721("MyNFT", "MNFT")
Ownable(initialOwner)
{}
function _baseURI() internal pure override returns (string memory) {
return "https://api.example.com/nft/";
}
function safeMint(address to, string memory uri) public onlyOwner {
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
}
function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721URIStorage)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
这个合约继承自OpenZeppelin的安全实现,大大简化了开发难度。ERC721URIStorage扩展提供了_setTokenURI函数,允许我们为每个NFT设置独特的元数据URI。
元数据JSON格式
每个NFT的元数据需要遵循特定的JSON格式。标准的元数据包括以下字段:
json
{
"name": "Pixel Hero #001",
"description": "A brave pixel hero ready for adventure in the blockchain realm.",
"image": "https://example.com/images/hero001.png",
"attributes": [
{
"trait_type": "Power",
"value": 85
},
{
"trait_type": "Speed",
"value": 72
},
{
"trait_type": "Rarity",
"value": "Legendary"
}
]
}
name和description字段用于显示NFT的基本信息。image字段指向NFT的可视化表示,通常是一个PNG或SVG文件。attributes数组允许我们定义NFT的各种属性特征,这些属性在OpenSea等平台会显示为NFT的特质标签,影响NFT的稀有度和价值。
增强版NFT合约
对于需要更复杂功能的NFT项目,我们可以添加批量Mint、白名单机制等功能:
solidity
contract AdvancedNFT is ERC721, ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;
// 白名单映射
mapping(address => bool) public whitelist;
// 每个白名单地址的最大Mint数量
mapping(address => uint256) public mintedCount;
uint256 public maxMintPerAddress = 5;
// NFT总量限制
uint256 public maxSupply = 10000;
uint256 public totalSupply;
// Mint价格
uint256 public mintPrice = 0.01 ether;
// 基础URI
string private _baseTokenURI;
constructor(string memory baseURI) ERC721("AdvancedNFT", "ANFT") {
_baseTokenURI = baseURI;
}
modifier onlyWhitelisted() {
require(whitelist[msg.sender], "Not on whitelist");
_;
}
function addToWhitelist(address[] calldata addresses) external onlyOwner {
for (uint256 i = 0; i < addresses.length; i++) {
whitelist[addresses[i]] = true;
}
}
function whitelistMint(string memory uri) external onlyWhitelisted {
require(
mintedCount[msg.sender] < maxMintPerAddress,
"Max mint limit reached"
);
require(totalSupply < maxSupply, "Max supply reached");
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
totalSupply++;
mintedCount[msg.sender]++;
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, uri);
}
function publicMint(string memory uri) external payable {
require(msg.value >= mintPrice, "Insufficient payment");
require(totalSupply < maxSupply, "Max supply reached");
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
totalSupply++;
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, uri);
// 退还多余的ETH
if (msg.value > mintPrice) {
payable(msg.sender).transfer(msg.value - mintPrice);
}
}
function withdraw() external onlyOwner {
payable(owner()).transfer(address(this).balance);
}
}
这个增强版合约实现了几个关键功能:白名单机制限制了只有白名单地址才能进行初始Mint;公开Mint允许任何人在白名单阶段结束后Mint,但需要支付ETH;总量限制确保NFT的稀缺性;提现功能允许合约所有者提取募集的ETH。
合约部署与测试
开发完合约后,需要进行充分的测试才能部署到主网。我推荐使用Hardhat或Foundry进行本地测试。
使用Hardhat测试
首先安装Hardhat:
bash
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat init
编写测试脚本:
javascript
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MyNFT", function () {
let myNFT;
let owner;
let addr1;
let addr2;
beforeEach(async function () {
[owner, addr1, addr2] = await ethers.getSigners();
const MyNFT = await ethers.getContractFactory("MyNFT");
myNFT = await MyNFT.deploy();
await myNFT.waitForDeployment();
});
describe("Minting", function () {
it("Should mint a new NFT", async function () {
const tokenURI = "https://example.com/token/1";
await expect(myNFT.safeMint(addr1.address, tokenURI))
.to.emit(myNFT, "Transfer")
.withArgs(ethers.ZeroAddress, addr1.address, 0);
expect(await myNFT.ownerOf(0)).to.equal(addr1.address);
expect(await myNFT.balanceOf(addr1.address)).to.equal(1);
});
it("Should set correct token URI", async function () {
const tokenURI = "https://example.com/token/1";
await myNFT.safeMint(addr1.address, tokenURI);
expect(await myNFT.tokenURI(0)).to.equal(tokenURI);
});
});
describe("Transfers", function () {
it("Should transfer NFT between accounts", async function () {
const tokenURI = "https://example.com/token/1";
await myNFT.safeMint(owner.address, tokenURI);
await myNFT.transferFrom(owner.address, addr1.address, 0);
expect(await myNFT.ownerOf(0)).to.equal(addr1.address);
});
});
});
运行测试:
bash
npx hardhat test
部署到测试网络
配置Hardhat网络后,可以部署到Goerli或Sepolia测试网络:
javascript
// hardhat.config.js
module.exports = {
solidity: "0.8.20",
networks: {
sepolia: {
url: process.env.SEPOLIA_RPC_URL,
accounts: [process.env.PRIVATE_KEY]
}
}
};
部署脚本:
javascript
const hre = require("hardhat");
async function main() {
const MyNFT = await hre.ethers.getContractFactory("MyNFT");
const myNFT = await MyNFT.deploy();
await myNFT.waitForDeployment();
console.log(`NFT deployed to: ${myNFT.target}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
执行部署:
bash
SEPOLIA_RPC_URL=your_rpc_url PRIVATE_KEY=your_private_key npx hardhat run scripts/deploy.js --network sepolia
安全考虑
开发NFT合约时,安全是首要考虑因素。以下是几个常见的安全陷阱:
重入攻击防护:ERC721的safeTransfer函数通过回调机制防止了NFT被锁定在不兼容的合约中,但开发者仍需注意业务逻辑中的潜在重入风险。使用OpenZeppelin的ReentrancyGuard可以有效防护。
权限控制:确保Mint、设置URI等敏感函数有适当的权限控制。使用onlyOwner修饰符限制管理员功能,同时考虑是否需要更复杂的权限管理机制如AccessControl。
输入验证:所有用户输入都需要严格验证。TokenURI应该是有效的字符串,Mint数量应该在合理范围内,价格计算需要精确。
元数据存储:对于去中心化存储,考虑使用IPFS存储元数据和图片文件,然后在tokenURI中使用IPFS网关URL。这确保了即使服务器宕机,NFT的元数据仍然可以访问。
总结
通过本教程,我们深入学习了ERC721 NFT合约的开发。从理解ERC721标准接口,到使用OpenZeppelin库构建安全合约,再到编写测试和部署脚本,你应该已经具备了独立开发NFT项目的能力。
NFT开发的核心在于理解非可替代性的概念,以及如何在智能合约中体现这种独特性。标准接口确保了互操作性,但真正让你的NFT项目脱颖而出的,是精心设计的元数据系统、安全的合约逻辑以及流畅的用户体验。
建议你在学习过程中不断实践,尝试修改和改进我们提供的代码示例。比如添加批量Mint功能、实现NFT升级机制、或者集成链上随机数生成有趣的NFT特性。实践是最好的学习方式,当你真正部署了一个NFT合约,看到它正常工作,那份成就感会让你更加热爱这个领域。
相关推荐:

发表回复