NFT与元宇宙
章节导读
NFT(Non-Fungible Token,非同质化代币)是区块链技术在数字资产领域最成功的应用之一。从2021年开始,NFT市场经历了爆发式增长,涌现出CryptoPunks、Bored Ape Yacht Club等现象级项目。
本章将深入讲解NFT的技术原理、标准实现、交易市场搭建以及元宇宙应用。不仅学习ERC721和ERC1155标准,还将实现一个完整的NFT发行和交易平台,包括盲盒机制、版税分配等核心功能。
学习路线
NFT基础
├── ERC721标准
├── ERC1155标准
├── 元数据与IPFS
└── NFT的应用场景
NFT进阶
├── NFT Marketplace
├── 盲盒(Mystery Box)
├── 版税(Royalty)
└── 动态NFT
元宇宙技术
├── 虚拟土地
├── 游戏道具
└── 社交身份
一、NFT基础概念
1.1 什么是NFT
NFT vs FT(同质化代币)
| 特性 | FT(如ERC20) | NFT(如ERC721) |
|---|---|---|
| 可替换性 | 可替换(每个代币相同) | 不可替换(每个代币唯一) |
| 可分割性 | 可分割(0.5 ETH) | 不可分割(要么拥有要么不拥有) |
| 唯一标识 | 无 | 有唯一的Token ID |
| 应用场景 | 货币、积分、股权 | 艺术品、游戏道具、域名 |
NFT的核心特性
唯一性: 每个NFT都有唯一的Token ID
不可分割: 不能拆分成0.5个NFT
所有权: 区块链记录明确的所有者
可交易: 可以在市场上买卖
元数据: 包含图片、属性等信息
1.2 ERC721标准
ERC721是最常用的NFT标准,定义了NFT的基本接口。
核心接口
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC721 {
// 查询余额
function balanceOf(address owner) external view returns (uint256 balance);
// 查询所有者
function ownerOf(uint256 tokenId) external view returns (address owner);
// 安全转账(会检查接收方是否能接收NFT)
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 operator);
// 授权所有NFT
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);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
}
ERC721Metadata扩展
interface IERC721Metadata {
// NFT集合的名称
function name() external view returns (string memory);
// NFT集合的符号
function symbol() external view returns (string memory);
// NFT的元数据URI
function tokenURI(uint256 tokenId) external view returns (string memory);
}
1.3 完整的ERC721实现
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ERC721 {
// 状态变量
string public name;
string public symbol;
// Token ID -> 所有者
mapping(uint256 => address) private _owners;
// 所有者 -> NFT数量
mapping(address => uint256) private _balances;
// Token ID -> 被授权的地址
mapping(uint256 => address) private _tokenApprovals;
// 所有者 -> 操作员 -> 是否授权
mapping(address => mapping(address => bool)) private _operatorApprovals;
// 事件
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);
constructor(string memory _name, string memory _symbol) {
name = _name;
symbol = _symbol;
}
// 查询余额
function balanceOf(address owner) public view returns (uint256) {
require(owner != address(0), "Invalid owner");
return _balances[owner];
}
// 查询所有者
function ownerOf(uint256 tokenId) public view returns (address) {
address owner = _owners[tokenId];
require(owner != address(0), "Token does not exist");
return owner;
}
// 授权
function approve(address to, uint256 tokenId) public {
address owner = ownerOf(tokenId);
require(to != owner, "Cannot approve to current owner");
require(
msg.sender == owner || isApprovedForAll(owner, msg.sender),
"Not authorized"
);
_tokenApprovals[tokenId] = to;
emit Approval(owner, to, tokenId);
}
// 查询授权
function getApproved(uint256 tokenId) public view returns (address) {
require(_owners[tokenId] != address(0), "Token does not exist");
return _tokenApprovals[tokenId];
}
// 授权所有NFT
function setApprovalForAll(address operator, bool approved) public {
require(operator != msg.sender, "Cannot approve to self");
_operatorApprovals[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
// 查询是否授权所有
function isApprovedForAll(address owner, address operator) public view returns (bool) {
return _operatorApprovals[owner][operator];
}
// 转账
function transferFrom(
address from,
address to,
uint256 tokenId
) public {
require(_isApprovedOrOwner(msg.sender, tokenId), "Not authorized");
_transfer(from, to, tokenId);
}
// 安全转账
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) public {
safeTransferFrom(from, to, tokenId, "");
}
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes memory data
) public {
require(_isApprovedOrOwner(msg.sender, tokenId), "Not authorized");
_safeTransfer(from, to, tokenId, data);
}
// 内部函数:转账
function _transfer(
address from,
address to,
uint256 tokenId
) internal {
require(ownerOf(tokenId) == from, "Not token owner");
require(to != address(0), "Invalid recipient");
// 清除授权
delete _tokenApprovals[tokenId];
// 更新余额
_balances[from] -= 1;
_balances[to] += 1;
// 更新所有者
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
}
// 内部函数:安全转账
function _safeTransfer(
address from,
address to,
uint256 tokenId,
bytes memory data
) internal {
_transfer(from, to, tokenId);
require(_checkOnERC721Received(from, to, tokenId, data), "Non-ERC721 receiver");
}
// 内部函数:铸造
function _mint(address to, uint256 tokenId) internal {
require(to != address(0), "Invalid recipient");
require(_owners[tokenId] == address(0), "Token already exists");
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(address(0), to, tokenId);
}
// 内部函数:销毁
function _burn(uint256 tokenId) internal {
address owner = ownerOf(tokenId);
// 清除授权
delete _tokenApprovals[tokenId];
_balances[owner] -= 1;
delete _owners[tokenId];
emit Transfer(owner, address(0), tokenId);
}
// 检查是否授权或所有者
function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) {
address owner = ownerOf(tokenId);
return (spender == owner ||
getApproved(tokenId) == spender ||
isApprovedForAll(owner, spender));
}
// 检查接收方是否能接收ERC721
function _checkOnERC721Received(
address from,
address to,
uint256 tokenId,
bytes memory data
) private returns (bool) {
if (to.code.length > 0) {
try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 retval) {
return retval == IERC721Receiver.onERC721Received.selector;
} catch {
return false;
}
} else {
return true;
}
}
}
// ERC721接收者接口
interface IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
}
1.4 实战:创建NFT集合
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract MyNFT is ERC721, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
// 基础URI(用于拼接tokenURI)
string private _baseTokenURI;
// 最大供应量
uint256 public constant MAX_SUPPLY = 10000;
// 铸造价格
uint256 public mintPrice = 0.01 ether;
constructor(string memory baseURI) ERC721("My NFT Collection", "MNFT") {
_baseTokenURI = baseURI;
}
// 铸造NFT
function mint() public payable {
require(_tokenIds.current() < MAX_SUPPLY, "Max supply reached");
require(msg.value >= mintPrice, "Insufficient payment");
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
_safeMint(msg.sender, newTokenId);
}
// 批量铸造
function mintBatch(uint256 quantity) public payable {
require(quantity <= 10, "Max 10 per tx");
require(_tokenIds.current() + quantity <= MAX_SUPPLY, "Exceeds max supply");
require(msg.value >= mintPrice * quantity, "Insufficient payment");
for (uint256 i = 0; i < quantity; i++) {
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
_safeMint(msg.sender, newTokenId);
}
}
// Owner铸造(空投)
function mintTo(address to) public onlyOwner {
require(_tokenIds.current() < MAX_SUPPLY, "Max supply reached");
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
_safeMint(to, newTokenId);
}
// 设置铸造价格
function setMintPrice(uint256 newPrice) public onlyOwner {
mintPrice = newPrice;
}
// 设置Base URI
function setBaseURI(string memory baseURI) public onlyOwner {
_baseTokenURI = baseURI;
}
// 提现
function withdraw() public onlyOwner {
uint256 balance = address(this).balance;
payable(owner()).transfer(balance);
}
// 重写tokenURI
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
require(_exists(tokenId), "Token does not exist");
return string(abi.encodePacked(_baseTokenURI, Strings.toString(tokenId), ".json"));
}
// 查询已铸造数量
function totalSupply() public view returns (uint256) {
return _tokenIds.current();
}
}
二、NFT元数据与IPFS
2.1 元数据标准
NFT的元数据通常存储在链下(IPFS、Arweave等),链上只存储元数据的URI。
标准元数据格式(JSON)
{
"name": "NFT #1",
"description": "This is my first NFT",
"image": "ipfs://QmXxxx.../image.png",
"external_url": "https://mynft.com/1",
"attributes": [
{
"trait_type": "Background",
"value": "Blue"
},
{
"trait_type": "Eyes",
"value": "Laser"
},
{
"trait_type": "Rarity",
"value": "Legendary",
"display_type": "string"
},
{
"trait_type": "Power",
"value": 95,
"display_type": "number",
"max_value": 100
}
]
}
2.2 IPFS存储
什么是IPFS
IPFS(InterPlanetary File System)是一个去中心化的文件存储系统,使用内容寻址(Content Addressing)。
传统HTTP:
https://example.com/image.png (位置寻址)
IPFS:
ipfs://QmXxxx.../image.png (内容寻址)
内容寻址的优势
1. 防篡改: 文件内容变化 → 哈希变化 → URI变化
2. 去中心化: 不依赖单一服务器
3. 永久存储: 只要有节点存储,文件就不会丢失
4. 高效分发: 可以从最近的节点下载
上传文件到IPFS(使用Pinata)
const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');
async function uploadToIPFS(filePath) {
const url = 'https://api.pinata.cloud/pinning/pinFileToIPFS';
const data = new FormData();
data.append('file', fs.createReadStream(filePath));
const response = await axios.post(url, data, {
maxBodyLength: 'Infinity',
headers: {
'Content-Type': `multipart/form-data; boundary=${data._boundary}`,
'pinata_api_key': process.env.PINATA_API_KEY,
'pinata_secret_api_key': process.env.PINATA_SECRET_KEY
}
});
return `ipfs://${response.data.IpfsHash}`;
}
// 上传元数据
async function uploadMetadata(metadata) {
const url = 'https://api.pinata.cloud/pinning/pinJSONToIPFS';
const response = await axios.post(url, metadata, {
headers: {
'pinata_api_key': process.env.PINATA_API_KEY,
'pinata_secret_api_key': process.env.PINATA_SECRET_KEY
}
});
return `ipfs://${response.data.IpfsHash}`;
}
// 使用示例
async function main() {
// 1. 上传图片
const imageURI = await uploadToIPFS('./image.png');
console.log('Image URI:', imageURI);
// 2. 创建元数据
const metadata = {
name: "My NFT #1",
description: "This is my first NFT",
image: imageURI,
attributes: [
{ trait_type: "Background", value: "Blue" },
{ trait_type: "Eyes", value: "Laser" }
]
};
// 3. 上传元数据
const metadataURI = await uploadMetadata(metadata);
console.log('Metadata URI:', metadataURI);
}
main();
2.3 动态元数据
有些NFT的元数据会根据链上状态动态变化。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
contract DynamicNFT is ERC721 {
using Strings for uint256;
// Token ID -> 等级
mapping(uint256 => uint256) public levels;
constructor() ERC721("Dynamic NFT", "DNFT") {}
// 升级
function levelUp(uint256 tokenId) public {
require(ownerOf(tokenId) == msg.sender, "Not owner");
levels[tokenId]++;
}
// 动态生成SVG图片
function generateSVG(uint256 tokenId) internal view returns (string memory) {
uint256 level = levels[tokenId];
return string(abi.encodePacked(
'<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400">',
'<rect width="400" height="400" fill="black"/>',
'<text x="50%" y="50%" font-size="48" fill="white" text-anchor="middle">',
'Level: ', level.toString(),
'</text>',
'</svg>'
));
}
// 动态生成元数据
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
require(_exists(tokenId), "Token does not exist");
string memory svg = generateSVG(tokenId);
string memory imageURI = string(abi.encodePacked(
'data:image/svg+xml;base64,',
Base64.encode(bytes(svg))
));
string memory json = string(abi.encodePacked(
'{',
'"name": "Dynamic NFT #', tokenId.toString(), '",',
'"description": "An NFT that evolves",',
'"image": "', imageURI, '",',
'"attributes": [',
'{"trait_type": "Level", "value": ', levels[tokenId].toString(), '}',
']',
'}'
));
return string(abi.encodePacked(
'data:application/json;base64,',
Base64.encode(bytes(json))
));
}
function mint() public {
uint256 tokenId = totalSupply() + 1;
_safeMint(msg.sender, tokenId);
levels[tokenId] = 1; // 初始等级为1
}
function totalSupply() public view returns (uint256) {
return _tokenIdCounter;
}
uint256 private _tokenIdCounter;
}
三、ERC1155:多代币标准
3.1 ERC1155 vs ERC721
| 特性 | ERC721 | ERC1155 |
|---|---|---|
| 代币类型 | 每个ID唯一 | 每个ID可以有多个副本 |
| Gas效率 | 低(每个NFT独立) | 高(批量操作) |
| 批量转账 | 不支持 | 支持 |
| 混合代币 | 只支持NFT | 支持NFT+FT混合 |
| 应用场景 | 艺术品、域名 | 游戏道具、门票 |
ERC1155的优势
1. Gas效率: 批量铸造100个NFT,ERC1155比ERC721省90%的Gas
2. 灵活性: 同一个合约可以同时管理NFT和FT
3. 原子交换: 一次交易可以交换多种代币
3.2 ERC1155核心接口
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC1155 {
// 查询余额
function balanceOf(address account, uint256 id) external view returns (uint256);
// 批量查询余额
function balanceOfBatch(
address[] calldata accounts,
uint256[] calldata ids
) external view returns (uint256[] memory);
// 授权
function setApprovalForAll(address operator, bool approved) external;
// 查询授权
function isApprovedForAll(address account, address operator) external view returns (bool);
// 转账
function safeTransferFrom(
address from,
address to,
uint256 id,
uint256 amount,
bytes calldata data
) external;
// 批量转账
function safeBatchTransferFrom(
address from,
address to,
uint256[] calldata ids,
uint256[] calldata amounts,
bytes calldata data
) external;
// 事件
event TransferSingle(
address indexed operator,
address indexed from,
address indexed to,
uint256 id,
uint256 value
);
event TransferBatch(
address indexed operator,
address indexed from,
address indexed to,
uint256[] ids,
uint256[] values
);
event ApprovalForAll(address indexed account, address indexed operator, bool approved);
event URI(string value, uint256 indexed id);
}
3.3 实战:游戏道具NFT
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract GameItems is ERC1155, Ownable {
// 道具ID
uint256 public constant SWORD = 1;
uint256 public constant SHIELD = 2;
uint256 public constant POTION = 3;
uint256 public constant GOLD = 4; // 游戏货币(FT)
// 道具供应量(0表示无限)
mapping(uint256 => uint256) public maxSupply;
mapping(uint256 => uint256) public totalSupply;
constructor() ERC1155("https://game.example/api/item/{id}.json") {
// 设置最大供应量
maxSupply[SWORD] = 1000;
maxSupply[SHIELD] = 1000;
maxSupply[POTION] = 0; // 无限
maxSupply[GOLD] = 0; // 无限
}
// 铸造道具
function mint(
address to,
uint256 id,
uint256 amount
) public onlyOwner {
if (maxSupply[id] > 0) {
require(totalSupply[id] + amount <= maxSupply[id], "Exceeds max supply");
}
totalSupply[id] += amount;
_mint(to, id, amount, "");
}
// 批量铸造
function mintBatch(
address to,
uint256[] memory ids,
uint256[] memory amounts
) public onlyOwner {
for (uint256 i = 0; i < ids.length; i++) {
if (maxSupply[ids[i]] > 0) {
require(
totalSupply[ids[i]] + amounts[i] <= maxSupply[ids[i]],
"Exceeds max supply"
);
}
totalSupply[ids[i]] += amounts[i];
}
_mintBatch(to, ids, amounts, "");
}
// 销毁道具
function burn(
address from,
uint256 id,
uint256 amount
) public {
require(from == msg.sender || isApprovedForAll(from, msg.sender), "Not authorized");
totalSupply[id] -= amount;
_burn(from, id, amount);
}
// 合成道具(例如:2把剑 + 1个盾 = 1把神剑)
function craft() public {
uint256 LEGENDARY_SWORD = 5;
// 检查材料
require(balanceOf(msg.sender, SWORD) >= 2, "Need 2 swords");
require(balanceOf(msg.sender, SHIELD) >= 1, "Need 1 shield");
// 销毁材料
_burn(msg.sender, SWORD, 2);
_burn(msg.sender, SHIELD, 1);
// 铸造新道具
totalSupply[LEGENDARY_SWORD]++;
_mint(msg.sender, LEGENDARY_SWORD, 1, "");
}
// 设置URI
function setURI(string memory newuri) public onlyOwner {
_setURI(newuri);
}
}
前端使用示例
const { ethers } = require("ethers");
async function main() {
const gameItems = await ethers.getContractAt("GameItems", contractAddress);
// 查询余额
const swordBalance = await gameItems.balanceOf(userAddress, 1); // SWORD = 1
console.log("剑的数量:", swordBalance.toString());
// 批量查询
const balances = await gameItems.balanceOfBatch(
[userAddress, userAddress, userAddress],
[1, 2, 3] // SWORD, SHIELD, POTION
);
console.log("道具数量:", balances.map(b => b.toString()));
// 批量转账
await gameItems.safeBatchTransferFrom(
userAddress,
recipientAddress,
[1, 2], // SWORD, SHIELD
[1, 1], // 各转1个
"0x"
);
// 合成
await gameItems.craft();
}
四、NFT Marketplace
4.1 市场核心功能
买卖功能
├── 上架(List)
├── 下架(Delist)
├── 购买(Buy)
└── 出价(Offer)
拍卖功能
├── 英式拍卖(English Auction)
├── 荷兰式拍卖(Dutch Auction)
└── 盲拍(Sealed Bid Auction)
其他功能
├── 版税分配
├── 批量操作
└── 手续费
4.2 实现NFT市场
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract NFTMarketplace is ReentrancyGuard, Ownable {
// 上架信息
struct Listing {
address seller;
address nftContract;
uint256 tokenId;
uint256 price;
bool active;
}
// 出价信息
struct Offer {
address offerer;
uint256 price;
uint256 expiresAt;
}
// NFT合约 -> Token ID -> 上架信息
mapping(address => mapping(uint256 => Listing)) public listings;
// NFT合约 -> Token ID -> 出价者 -> 出价信息
mapping(address => mapping(uint256 => mapping(address => Offer))) public offers;
// 平台手续费(基点,1% = 100)
uint256 public feePercentage = 250; // 2.5%
// 累计手续费
uint256 public accumulatedFees;
// 事件
event Listed(
address indexed seller,
address indexed nftContract,
uint256 indexed tokenId,
uint256 price
);
event Sold(
address indexed seller,
address indexed buyer,
address indexed nftContract,
uint256 tokenId,
uint256 price
);
event Delisted(
address indexed seller,
address indexed nftContract,
uint256 indexed tokenId
);
event OfferMade(
address indexed offerer,
address indexed nftContract,
uint256 indexed tokenId,
uint256 price
);
event OfferAccepted(
address indexed seller,
address indexed buyer,
address indexed nftContract,
uint256 tokenId,
uint256 price
);
// 上架NFT
function list(
address nftContract,
uint256 tokenId,
uint256 price
) external {
require(price > 0, "Price must be greater than 0");
IERC721 nft = IERC721(nftContract);
require(nft.ownerOf(tokenId) == msg.sender, "Not token owner");
require(
nft.isApprovedForAll(msg.sender, address(this)) ||
nft.getApproved(tokenId) == address(this),
"Marketplace not approved"
);
listings[nftContract][tokenId] = Listing({
seller: msg.sender,
nftContract: nftContract,
tokenId: tokenId,
price: price,
active: true
});
emit Listed(msg.sender, nftContract, tokenId, price);
}
// 下架NFT
function delist(address nftContract, uint256 tokenId) external {
Listing storage listing = listings[nftContract][tokenId];
require(listing.active, "Not listed");
require(listing.seller == msg.sender, "Not seller");
listing.active = false;
emit Delisted(msg.sender, nftContract, tokenId);
}
// 购买NFT
function buy(address nftContract, uint256 tokenId) external payable nonReentrant {
Listing storage listing = listings[nftContract][tokenId];
require(listing.active, "Not listed");
require(msg.value >= listing.price, "Insufficient payment");
address seller = listing.seller;
uint256 price = listing.price;
// 标记为已售
listing.active = false;
// 计算手续费
uint256 fee = (price * feePercentage) / 10000;
uint256 sellerProceeds = price - fee;
// 转移NFT
IERC721(nftContract).safeTransferFrom(seller, msg.sender, tokenId);
// 支付给卖家
(bool success, ) = seller.call{value: sellerProceeds}("");
require(success, "Transfer to seller failed");
// 累计手续费
accumulatedFees += fee;
// 退还多余的ETH
if (msg.value > price) {
(bool refundSuccess, ) = msg.sender.call{value: msg.value - price}("");
require(refundSuccess, "Refund failed");
}
emit Sold(seller, msg.sender, nftContract, tokenId, price);
}
// 出价
function makeOffer(
address nftContract,
uint256 tokenId,
uint256 duration
) external payable {
require(msg.value > 0, "Offer must be greater than 0");
require(duration > 0 && duration <= 30 days, "Invalid duration");
offers[nftContract][tokenId][msg.sender] = Offer({
offerer: msg.sender,
price: msg.value,
expiresAt: block.timestamp + duration
});
emit OfferMade(msg.sender, nftContract, tokenId, msg.value);
}
// 接受出价
function acceptOffer(
address nftContract,
uint256 tokenId,
address offerer
) external nonReentrant {
IERC721 nft = IERC721(nftContract);
require(nft.ownerOf(tokenId) == msg.sender, "Not token owner");
Offer storage offer = offers[nftContract][tokenId][offerer];
require(offer.price > 0, "No offer");
require(block.timestamp <= offer.expiresAt, "Offer expired");
uint256 price = offer.price;
address buyer = offer.offerer;
// 删除出价
delete offers[nftContract][tokenId][offerer];
// 如果在市场上架,取消上架
if (listings[nftContract][tokenId].active) {
listings[nftContract][tokenId].active = false;
}
// 计算手续费
uint256 fee = (price * feePercentage) / 10000;
uint256 sellerProceeds = price - fee;
// 转移NFT
nft.safeTransferFrom(msg.sender, buyer, tokenId);
// 支付给卖家
(bool success, ) = msg.sender.call{value: sellerProceeds}("");
require(success, "Transfer to seller failed");
// 累计手续费
accumulatedFees += fee;
emit OfferAccepted(msg.sender, buyer, nftContract, tokenId, price);
}
// 取消出价
function cancelOffer(address nftContract, uint256 tokenId) external nonReentrant {
Offer storage offer = offers[nftContract][tokenId][msg.sender];
require(offer.price > 0, "No offer");
uint256 refundAmount = offer.price;
// 删除出价
delete offers[nftContract][tokenId][msg.sender];
// 退款
(bool success, ) = msg.sender.call{value: refundAmount}("");
require(success, "Refund failed");
}
// 设置手续费
function setFeePercentage(uint256 newFee) external onlyOwner {
require(newFee <= 1000, "Fee too high"); // 最高10%
feePercentage = newFee;
}
// 提取手续费
function withdrawFees() external onlyOwner {
uint256 amount = accumulatedFees;
accumulatedFees = 0;
(bool success, ) = owner().call{value: amount}("");
require(success, "Withdrawal failed");
}
}
4.3 拍卖合约
英式拍卖(价高者得)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract EnglishAuction is ReentrancyGuard {
// 拍卖信息
struct Auction {
address seller;
address nftContract;
uint256 tokenId;
uint256 startPrice;
uint256 startTime;
uint256 endTime;
address highestBidder;
uint256 highestBid;
bool ended;
}
// 拍卖ID -> 拍卖信息
mapping(uint256 => Auction) public auctions;
// 拍卖ID -> 出价者 -> 出价金额(用于退款)
mapping(uint256 => mapping(address => uint256)) public bids;
uint256 public auctionCounter;
// 事件
event AuctionCreated(
uint256 indexed auctionId,
address indexed seller,
address indexed nftContract,
uint256 tokenId,
uint256 startPrice,
uint256 endTime
);
event BidPlaced(
uint256 indexed auctionId,
address indexed bidder,
uint256 amount
);
event AuctionEnded(
uint256 indexed auctionId,
address indexed winner,
uint256 amount
);
// 创建拍卖
function createAuction(
address nftContract,
uint256 tokenId,
uint256 startPrice,
uint256 duration
) external {
require(duration >= 1 hours && duration <= 7 days, "Invalid duration");
IERC721 nft = IERC721(nftContract);
require(nft.ownerOf(tokenId) == msg.sender, "Not token owner");
// 转移NFT到合约
nft.transferFrom(msg.sender, address(this), tokenId);
auctionCounter++;
auctions[auctionCounter] = Auction({
seller: msg.sender,
nftContract: nftContract,
tokenId: tokenId,
startPrice: startPrice,
startTime: block.timestamp,
endTime: block.timestamp + duration,
highestBidder: address(0),
highestBid: 0,
ended: false
});
emit AuctionCreated(
auctionCounter,
msg.sender,
nftContract,
tokenId,
startPrice,
block.timestamp + duration
);
}
// 出价
function bid(uint256 auctionId) external payable {
Auction storage auction = auctions[auctionId];
require(block.timestamp >= auction.startTime, "Auction not started");
require(block.timestamp < auction.endTime, "Auction ended");
require(!auction.ended, "Auction finalized");
require(msg.sender != auction.seller, "Seller cannot bid");
uint256 totalBid = bids[auctionId][msg.sender] + msg.value;
if (auction.highestBidder == address(0)) {
require(totalBid >= auction.startPrice, "Bid too low");
} else {
require(totalBid > auction.highestBid, "Bid not high enough");
}
auction.highestBidder = msg.sender;
auction.highestBid = totalBid;
bids[auctionId][msg.sender] = totalBid;
emit BidPlaced(auctionId, msg.sender, totalBid);
}
// 结束拍卖
function endAuction(uint256 auctionId) external nonReentrant {
Auction storage auction = auctions[auctionId];
require(block.timestamp >= auction.endTime, "Auction not ended");
require(!auction.ended, "Auction already finalized");
auction.ended = true;
if (auction.highestBidder != address(0)) {
// 转移NFT给中标者
IERC721(auction.nftContract).transferFrom(
address(this),
auction.highestBidder,
auction.tokenId
);
// 支付给卖家
(bool success, ) = auction.seller.call{value: auction.highestBid}("");
require(success, "Payment failed");
emit AuctionEnded(auctionId, auction.highestBidder, auction.highestBid);
} else {
// 没有人出价,NFT退还给卖家
IERC721(auction.nftContract).transferFrom(
address(this),
auction.seller,
auction.tokenId
);
emit AuctionEnded(auctionId, address(0), 0);
}
}
// 提取出价(未中标者)
function withdraw(uint256 auctionId) external nonReentrant {
Auction storage auction = auctions[auctionId];
require(auction.ended, "Auction not ended");
require(msg.sender != auction.highestBidder, "Winner cannot withdraw");
uint256 amount = bids[auctionId][msg.sender];
require(amount > 0, "No bid to withdraw");
bids[auctionId][msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Withdrawal failed");
}
}
荷兰式拍卖(价格递减)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
contract DutchAuction {
address public seller;
address public nftContract;
uint256 public tokenId;
uint256 public startPrice;
uint256 public endPrice;
uint256 public startTime;
uint256 public duration;
bool public ended;
constructor(
address _nftContract,
uint256 _tokenId,
uint256 _startPrice,
uint256 _endPrice,
uint256 _duration
) {
require(_startPrice > _endPrice, "Start price must be higher");
seller = msg.sender;
nftContract = _nftContract;
tokenId = _tokenId;
startPrice = _startPrice;
endPrice = _endPrice;
duration = _duration;
startTime = block.timestamp;
// 转移NFT到合约
IERC721(_nftContract).transferFrom(msg.sender, address(this), _tokenId);
}
// 获取当前价格
function getCurrentPrice() public view returns (uint256) {
if (block.timestamp >= startTime + duration) {
return endPrice;
}
uint256 elapsed = block.timestamp - startTime;
uint256 priceRange = startPrice - endPrice;
uint256 priceDecrease = (priceRange * elapsed) / duration;
return startPrice - priceDecrease;
}
// 购买
function buy() external payable {
require(!ended, "Auction ended");
uint256 currentPrice = getCurrentPrice();
require(msg.value >= currentPrice, "Insufficient payment");
ended = true;
// 转移NFT
IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
// 支付给卖家
(bool success, ) = seller.call{value: currentPrice}("");
require(success, "Payment failed");
// 退还多余的ETH
if (msg.value > currentPrice) {
(bool refundSuccess, ) = msg.sender.call{value: msg.value - currentPrice}("");
require(refundSuccess, "Refund failed");
}
}
// 取消拍卖(卖家)
function cancel() external {
require(msg.sender == seller, "Not seller");
require(!ended, "Auction ended");
ended = true;
// 退还NFT
IERC721(nftContract).transferFrom(address(this), seller, tokenId);
}
}
五、盲盒(Mystery Box)
5.1 盲盒原理
盲盒是一种随机开箱机制,用户购买盲盒时不知道会获得什么NFT。
实现方式
方式1: 预生成随机数(链下)
优点: Gas低,可预览
缺点: 中心化,可能作弊
方式2: 链上随机数(Chainlink VRF)
优点: 去中心化,公平
缺点: Gas高,需要等待
方式3: 延迟揭示(Commit-Reveal)
优点: 公平,Gas适中
缺点: 需要两步操作
5.2 使用Chainlink VRF实现盲盒
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
contract MysteryBoxNFT is ERC721, VRFConsumerBaseV2 {
// Chainlink VRF
VRFCoordinatorV2Interface COORDINATOR;
uint64 subscriptionId;
bytes32 keyHash;
uint32 callbackGasLimit = 200000;
uint16 requestConfirmations = 3;
uint32 numWords = 1;
// 请求ID -> 用户地址
mapping(uint256 => address) public requestIdToSender;
// 稀有度配置
enum Rarity { Common, Rare, Epic, Legendary }
// 稀有度权重(总和10000)
uint256 public constant COMMON_WEIGHT = 6000; // 60%
uint256 public constant RARE_WEIGHT = 3000; // 30%
uint256 public constant EPIC_WEIGHT = 900; // 9%
uint256 public constant LEGENDARY_WEIGHT = 100; // 1%
// Token ID -> 稀有度
mapping(uint256 => Rarity) public tokenRarity;
uint256 public tokenCounter;
uint256 public boxPrice = 0.01 ether;
// 基础URI
mapping(Rarity => string) public rarityBaseURIs;
event BoxOpened(address indexed user, uint256 indexed tokenId, Rarity rarity);
constructor(
uint64 _subscriptionId,
address _vrfCoordinator,
bytes32 _keyHash
) ERC721("Mystery Box NFT", "MBNFT") VRFConsumerBaseV2(_vrfCoordinator) {
COORDINATOR = VRFCoordinatorV2Interface(_vrfCoordinator);
subscriptionId = _subscriptionId;
keyHash = _keyHash;
// 设置不同稀有度的Base URI
rarityBaseURIs[Rarity.Common] = "ipfs://common/";
rarityBaseURIs[Rarity.Rare] = "ipfs://rare/";
rarityBaseURIs[Rarity.Epic] = "ipfs://epic/";
rarityBaseURIs[Rarity.Legendary] = "ipfs://legendary/";
}
// 购买并打开盲盒
function openBox() external payable {
require(msg.value >= boxPrice, "Insufficient payment");
// 请求随机数
uint256 requestId = COORDINATOR.requestRandomWords(
keyHash,
subscriptionId,
requestConfirmations,
callbackGasLimit,
numWords
);
requestIdToSender[requestId] = msg.sender;
}
// Chainlink VRF回调
function fulfillRandomWords(
uint256 requestId,
uint256[] memory randomWords
) internal override {
address user = requestIdToSender[requestId];
require(user != address(0), "Invalid request");
tokenCounter++;
uint256 newTokenId = tokenCounter;
// 根据随机数确定稀有度
Rarity rarity = determineRarity(randomWords[0]);
tokenRarity[newTokenId] = rarity;
// 铸造NFT
_safeMint(user, newTokenId);
emit BoxOpened(user, newTokenId, rarity);
// 清理映射
delete requestIdToSender[requestId];
}
// 根据随机数确定稀有度
function determineRarity(uint256 randomNumber) internal pure returns (Rarity) {
uint256 roll = randomNumber % 10000;
if (roll < LEGENDARY_WEIGHT) {
return Rarity.Legendary;
} else if (roll < LEGENDARY_WEIGHT + EPIC_WEIGHT) {
return Rarity.Epic;
} else if (roll < LEGENDARY_WEIGHT + EPIC_WEIGHT + RARE_WEIGHT) {
return Rarity.Rare;
} else {
return Rarity.Common;
}
}
// 重写tokenURI,根据稀有度返回不同的URI
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
require(_exists(tokenId), "Token does not exist");
Rarity rarity = tokenRarity[tokenId];
string memory baseURI = rarityBaseURIs[rarity];
return string(abi.encodePacked(baseURI, Strings.toString(tokenId), ".json"));
}
// 设置盒子价格
function setBoxPrice(uint256 newPrice) external {
boxPrice = newPrice;
}
// 提现
function withdraw() external {
payable(owner()).transfer(address(this).balance);
}
}
5.3 简化版盲盒(使用伪随机数)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
// 注意: 这种方式不够安全,矿工可以操纵
contract SimpleMysteryBox is ERC721 {
enum Rarity { Common, Rare, Epic, Legendary }
mapping(uint256 => Rarity) public tokenRarity;
uint256 public tokenCounter;
uint256 public boxPrice = 0.01 ether;
constructor() ERC721("Simple Mystery Box", "SMB") {}
// 购买并打开盲盒
function openBox() external payable {
require(msg.value >= boxPrice, "Insufficient payment");
tokenCounter++;
uint256 newTokenId = tokenCounter;
// 伪随机数(不安全!)
uint256 randomNumber = uint256(
keccak256(
abi.encodePacked(
block.timestamp,
block.difficulty,
msg.sender,
tokenCounter
)
)
);
Rarity rarity = determineRarity(randomNumber);
tokenRarity[newTokenId] = rarity;
_safeMint(msg.sender, newTokenId);
}
function determineRarity(uint256 randomNumber) internal pure returns (Rarity) {
uint256 roll = randomNumber % 100;
if (roll < 1) return Rarity.Legendary; // 1%
if (roll < 10) return Rarity.Epic; // 9%
if (roll < 40) return Rarity.Rare; // 30%
return Rarity.Common; // 60%
}
}
六、版税(Royalty)
6.1 ERC2981版税标准
ERC2981是NFT版税的标准接口,允许NFT在二级市场交易时,创作者获得一定比例的收益。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC2981 {
// 查询版税信息
function royaltyInfo(
uint256 tokenId,
uint256 salePrice
) external view returns (
address receiver,
uint256 royaltyAmount
);
}
6.2 实现版税NFT
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/interfaces/IERC2981.sol";
contract RoyaltyNFT is ERC721, IERC2981 {
// 版税接收者
address public royaltyReceiver;
// 版税比例(基点,1% = 100)
uint256 public royaltyPercentage = 500; // 5%
uint256 public tokenCounter;
constructor(address _royaltyReceiver) ERC721("Royalty NFT", "RNFT") {
royaltyReceiver = _royaltyReceiver;
}
// 铸造NFT
function mint(address to) external {
tokenCounter++;
_safeMint(to, tokenCounter);
}
// 实现ERC2981接口
function royaltyInfo(
uint256 tokenId,
uint256 salePrice
) external view override returns (address receiver, uint256 royaltyAmount) {
require(_exists(tokenId), "Token does not exist");
receiver = royaltyReceiver;
royaltyAmount = (salePrice * royaltyPercentage) / 10000;
}
// 设置版税接收者
function setRoyaltyReceiver(address newReceiver) external {
royaltyReceiver = newReceiver;
}
// 设置版税比例
function setRoyaltyPercentage(uint256 newPercentage) external {
require(newPercentage <= 1000, "Max 10%");
royaltyPercentage = newPercentage;
}
// 支持的接口
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, IERC165) returns (bool) {
return interfaceId == type(IERC2981).interfaceId || super.supportsInterface(interfaceId);
}
}
6.3 市场集成版税
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/interfaces/IERC2981.sol";
contract MarketplaceWithRoyalty {
struct Listing {
address seller;
uint256 price;
}
mapping(address => mapping(uint256 => Listing)) public listings;
uint256 public platformFee = 250; // 2.5%
// 上架
function list(address nftContract, uint256 tokenId, uint256 price) external {
IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
listings[nftContract][tokenId] = Listing({
seller: msg.sender,
price: price
});
}
// 购买(支持版税)
function buy(address nftContract, uint256 tokenId) external payable {
Listing memory listing = listings[nftContract][tokenId];
require(msg.value >= listing.price, "Insufficient payment");
uint256 price = listing.price;
address seller = listing.seller;
// 删除上架
delete listings[nftContract][tokenId];
// 计算平台手续费
uint256 platformAmount = (price * platformFee) / 10000;
// 查询版税
uint256 royaltyAmount = 0;
address royaltyReceiver = address(0);
// 检查是否支持ERC2981
if (IERC165(nftContract).supportsInterface(type(IERC2981).interfaceId)) {
(royaltyReceiver, royaltyAmount) = IERC2981(nftContract).royaltyInfo(tokenId, price);
}
// 计算卖家收益
uint256 sellerAmount = price - platformAmount - royaltyAmount;
// 转移NFT
IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
// 支付给卖家
(bool sellerSuccess, ) = seller.call{value: sellerAmount}("");
require(sellerSuccess, "Seller payment failed");
// 支付版税
if (royaltyAmount > 0 && royaltyReceiver != address(0)) {
(bool royaltySuccess, ) = royaltyReceiver.call{value: royaltyAmount}("");
require(royaltySuccess, "Royalty payment failed");
}
// 平台手续费留在合约中
}
}
七、完整项目:NFT发行和交易平台
7.1 项目架构
NFT平台
├── NFT合约(ERC721)
│ ├── 盲盒铸造
│ ├── 稀有度系统
│ └── 版税支持
│
├── 市场合约
│ ├── 上架/下架
│ ├── 直接购买
│ ├── 出价系统
│ └── 版税分配
│
└── 前端(React + ethers.js)
├── 钱包连接
├── 铸造界面
├── 市场列表
└── 个人资产
7.2 主NFT合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/interfaces/IERC2981.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract PremiumNFT is ERC721Enumerable, IERC2981, Ownable, ReentrancyGuard {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
// 稀有度
enum Rarity { Common, Rare, Epic, Legendary }
// NFT配置
uint256 public constant MAX_SUPPLY = 10000;
uint256 public mintPrice = 0.08 ether;
uint256 public maxMintPerTx = 10;
// 版税配置
address public royaltyReceiver;
uint256 public royaltyPercentage = 500; // 5%
// 销售阶段
enum Phase { Paused, Whitelist, Public }
Phase public currentPhase = Phase.Paused;
// 白名单
mapping(address => uint256) public whitelistAllowance;
mapping(address => uint256) public whitelistMinted;
// 元数据
string private _baseTokenURI;
mapping(uint256 => Rarity) public tokenRarity;
// 已揭示标志
bool public revealed = false;
string public unrevealedURI;
// 事件
event Minted(address indexed minter, uint256 indexed tokenId, Rarity rarity);
event PhaseChanged(Phase newPhase);
event Revealed();
constructor(
string memory baseURI,
string memory _unrevealedURI,
address _royaltyReceiver
) ERC721("Premium NFT Collection", "PNFT") {
_baseTokenURI = baseURI;
unrevealedURI = _unrevealedURI;
royaltyReceiver = _royaltyReceiver;
}
// 白名单铸造
function whitelistMint(uint256 quantity) external payable nonReentrant {
require(currentPhase == Phase.Whitelist, "Not whitelist phase");
require(whitelistAllowance[msg.sender] > 0, "Not whitelisted");
require(
whitelistMinted[msg.sender] + quantity <= whitelistAllowance[msg.sender],
"Exceeds whitelist allowance"
);
require(msg.value >= mintPrice * quantity, "Insufficient payment");
whitelistMinted[msg.sender] += quantity;
_mintBatch(msg.sender, quantity);
}
// 公开铸造
function publicMint(uint256 quantity) external payable nonReentrant {
require(currentPhase == Phase.Public, "Not public phase");
require(quantity <= maxMintPerTx, "Exceeds max per tx");
require(msg.value >= mintPrice * quantity, "Insufficient payment");
_mintBatch(msg.sender, quantity);
}
// 批量铸造内部函数
function _mintBatch(address to, uint256 quantity) internal {
require(_tokenIds.current() + quantity <= MAX_SUPPLY, "Exceeds max supply");
for (uint256 i = 0; i < quantity; i++) {
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
// 确定稀有度(简化版伪随机)
Rarity rarity = _determineRarity(newTokenId);
tokenRarity[newTokenId] = rarity;
_safeMint(to, newTokenId);
emit Minted(to, newTokenId, rarity);
}
}
// 确定稀有度
function _determineRarity(uint256 tokenId) internal view returns (Rarity) {
uint256 random = uint256(
keccak256(abi.encodePacked(block.timestamp, block.difficulty, tokenId))
) % 100;
if (random < 1) return Rarity.Legendary; // 1%
if (random < 10) return Rarity.Epic; // 9%
if (random < 40) return Rarity.Rare; // 30%
return Rarity.Common; // 60%
}
// 添加白名单
function addToWhitelist(address[] calldata addresses, uint256 allowance) external onlyOwner {
for (uint256 i = 0; i < addresses.length; i++) {
whitelistAllowance[addresses[i]] = allowance;
}
}
// 设置销售阶段
function setPhase(Phase newPhase) external onlyOwner {
currentPhase = newPhase;
emit PhaseChanged(newPhase);
}
// 设置铸造价格
function setMintPrice(uint256 newPrice) external onlyOwner {
mintPrice = newPrice;
}
// 揭示NFT
function reveal(string memory newBaseURI) external onlyOwner {
require(!revealed, "Already revealed");
revealed = true;
_baseTokenURI = newBaseURI;
emit Revealed();
}
// 重写tokenURI
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
require(_exists(tokenId), "Token does not exist");
if (!revealed) {
return unrevealedURI;
}
Rarity rarity = tokenRarity[tokenId];
string memory rarityFolder;
if (rarity == Rarity.Legendary) rarityFolder = "legendary";
else if (rarity == Rarity.Epic) rarityFolder = "epic";
else if (rarity == Rarity.Rare) rarityFolder = "rare";
else rarityFolder = "common";
return string(abi.encodePacked(
_baseTokenURI,
rarityFolder,
"/",
Strings.toString(tokenId),
".json"
));
}
// 实现ERC2981
function royaltyInfo(
uint256 tokenId,
uint256 salePrice
) external view override returns (address receiver, uint256 royaltyAmount) {
require(_exists(tokenId), "Token does not exist");
receiver = royaltyReceiver;
royaltyAmount = (salePrice * royaltyPercentage) / 10000;
}
// 设置版税
function setRoyalty(address receiver, uint256 percentage) external onlyOwner {
require(percentage <= 1000, "Max 10%");
royaltyReceiver = receiver;
royaltyPercentage = percentage;
}
// 提现
function withdraw() external onlyOwner {
uint256 balance = address(this).balance;
payable(owner()).transfer(balance);
}
// 支持的接口
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(ERC721Enumerable, IERC165)
returns (bool)
{
return interfaceId == type(IERC2981).interfaceId || super.supportsInterface(interfaceId);
}
}
7.3 前端集成(React + ethers.js)
// MintPage.jsx
import { useState, useEffect } from 'react';
import { ethers } from 'ethers';
const NFT_CONTRACT_ADDRESS = "0x...";
const NFT_ABI = [...]; // 合约ABI
function MintPage() {
const [provider, setProvider] = useState(null);
const [signer, setSigner] = useState(null);
const [contract, setContract] = useState(null);
const [account, setAccount] = useState('');
const [mintQuantity, setMintQuantity] = useState(1);
const [mintPrice, setMintPrice] = useState('0');
const [totalSupply, setTotalSupply] = useState(0);
const [maxSupply, setMaxSupply] = useState(0);
const [currentPhase, setCurrentPhase] = useState('Paused');
const [loading, setLoading] = useState(false);
const [txHash, setTxHash] = useState('');
// 连接钱包
async function connectWallet() {
if (typeof window.ethereum !== 'undefined') {
try {
await window.ethereum.request({ method: 'eth_requestAccounts' });
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const account = await signer.getAddress();
const contract = new ethers.Contract(NFT_CONTRACT_ADDRESS, NFT_ABI, signer);
setProvider(provider);
setSigner(signer);
setAccount(account);
setContract(contract);
loadContractData(contract);
} catch (error) {
console.error("Failed to connect wallet:", error);
alert("Failed to connect wallet");
}
} else {
alert("Please install MetaMask");
}
}
// 加载合约数据
async function loadContractData(contract) {
try {
const price = await contract.mintPrice();
const supply = await contract.totalSupply();
const max = await contract.MAX_SUPPLY();
const phase = await contract.currentPhase();
setMintPrice(ethers.utils.formatEther(price));
setTotalSupply(supply.toNumber());
setMaxSupply(max.toNumber());
const phases = ['Paused', 'Whitelist', 'Public'];
setCurrentPhase(phases[phase]);
} catch (error) {
console.error("Failed to load contract data:", error);
}
}
// 铸造NFT
async function mint() {
if (!contract) {
alert("Please connect wallet first");
return;
}
try {
setLoading(true);
const totalPrice = ethers.utils.parseEther(
(parseFloat(mintPrice) * mintQuantity).toString()
);
let tx;
if (currentPhase === 'Whitelist') {
tx = await contract.whitelistMint(mintQuantity, { value: totalPrice });
} else if (currentPhase === 'Public') {
tx = await contract.publicMint(mintQuantity, { value: totalPrice });
} else {
alert("Minting is paused");
setLoading(false);
return;
}
setTxHash(tx.hash);
await tx.wait();
alert(`Successfully minted ${mintQuantity} NFT(s)!`);
loadContractData(contract);
} catch (error) {
console.error("Mint failed:", error);
alert("Mint failed: " + error.message);
} finally {
setLoading(false);
}
}
// 监听事件
useEffect(() => {
if (contract) {
contract.on("Minted", (minter, tokenId, rarity) => {
console.log(`Minted: ${tokenId}, Rarity: ${rarity}`);
loadContractData(contract);
});
return () => {
contract.removeAllListeners("Minted");
};
}
}, [contract]);
return (
<div className="mint-page">
<h1>Premium NFT Collection</h1>
{!account ? (
<button onClick={connectWallet}>Connect Wallet</button>
) : (
<div>
<p>Connected: {account.slice(0, 6)}...{account.slice(-4)}</p>
<div className="mint-info">
<p>Phase: {currentPhase}</p>
<p>Price: {mintPrice} ETH</p>
<p>Supply: {totalSupply} / {maxSupply}</p>
</div>
<div className="mint-controls">
<label>
Quantity:
<input
type="number"
min="1"
max="10"
value={mintQuantity}
onChange={(e) => setMintQuantity(parseInt(e.target.value))}
/>
</label>
<p>Total: {(parseFloat(mintPrice) * mintQuantity).toFixed(4)} ETH</p>
<button onClick={mint} disabled={loading || currentPhase === 'Paused'}>
{loading ? 'Minting...' : 'Mint'}
</button>
</div>
{txHash && (
<p>
Transaction:{' '}
<a
href={`https://etherscan.io/tx/${txHash}`}
target="_blank"
rel="noopener noreferrer"
>
{txHash.slice(0, 10)}...
</a>
</p>
)}
</div>
)}
</div>
);
}
export default MintPage;
八、元宇宙应用
8.1 虚拟土地NFT
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract VirtualLand is ERC721 {
// 坐标 -> Token ID
mapping(int256 => mapping(int256 => uint256)) public coordToTokenId;
// Token ID -> 坐标
mapping(uint256 => Coordinates) public tokenIdToCoord;
struct Coordinates {
int256 x;
int256 y;
}
uint256 public tokenCounter;
uint256 public landPrice = 0.1 ether;
// 地块大小范围
int256 public constant MIN_COORD = -1000;
int256 public constant MAX_COORD = 1000;
constructor() ERC721("Virtual Land", "VLAND") {}
// 购买土地
function buyLand(int256 x, int256 y) external payable {
require(msg.value >= landPrice, "Insufficient payment");
require(x >= MIN_COORD && x <= MAX_COORD, "Invalid X coordinate");
require(y >= MIN_COORD && y <= MAX_COORD, "Invalid Y coordinate");
require(coordToTokenId[x][y] == 0, "Land already owned");
tokenCounter++;
uint256 newTokenId = tokenCounter;
coordToTokenId[x][y] = newTokenId;
tokenIdToCoord[newTokenId] = Coordinates(x, y);
_safeMint(msg.sender, newTokenId);
}
// 批量购买相邻土地
function buyLandBatch(int256 startX, int256 startY, uint256 width, uint256 height) external payable {
require(width > 0 && width <= 10, "Invalid width");
require(height > 0 && height <= 10, "Invalid height");
uint256 totalPlots = width * height;
require(msg.value >= landPrice * totalPlots, "Insufficient payment");
for (uint256 i = 0; i < width; i++) {
for (uint256 j = 0; j < height; j++) {
int256 x = startX + int256(i);
int256 y = startY + int256(j);
require(x >= MIN_COORD && x <= MAX_COORD, "Invalid X coordinate");
require(y >= MIN_COORD && y <= MAX_COORD, "Invalid Y coordinate");
require(coordToTokenId[x][y] == 0, "Land already owned");
tokenCounter++;
uint256 newTokenId = tokenCounter;
coordToTokenId[x][y] = newTokenId;
tokenIdToCoord[newTokenId] = Coordinates(x, y);
_safeMint(msg.sender, newTokenId);
}
}
}
// 查询土地所有者
function landOwner(int256 x, int256 y) external view returns (address) {
uint256 tokenId = coordToTokenId[x][y];
if (tokenId == 0) return address(0);
return ownerOf(tokenId);
}
// 查询用户所有土地
function landsOf(address owner) external view returns (Coordinates[] memory) {
uint256 balance = balanceOf(owner);
Coordinates[] memory lands = new Coordinates[](balance);
uint256 index = 0;
for (uint256 i = 1; i <= tokenCounter; i++) {
if (_exists(i) && ownerOf(i) == owner) {
lands[index] = tokenIdToCoord[i];
index++;
}
}
return lands;
}
}
8.2 可组合NFT(ERC998)
可组合NFT允许NFT拥有其他NFT或ERC20代币。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
// 简化版ERC998
contract ComposableNFT is ERC721, IERC721Receiver {
// 父NFT -> 子NFT合约 -> 子NFT ID -> 是否拥有
mapping(uint256 => mapping(address => mapping(uint256 => bool))) public childNFTs;
// 父NFT -> 子NFT列表
mapping(uint256 => ChildNFT[]) public childNFTList;
struct ChildNFT {
address nftContract;
uint256 tokenId;
}
uint256 public tokenCounter;
constructor() ERC721("Composable NFT", "CNFT") {}
// 铸造父NFT
function mint() external {
tokenCounter++;
_safeMint(msg.sender, tokenCounter);
}
// 添加子NFT
function addChildNFT(
uint256 parentTokenId,
address childContract,
uint256 childTokenId
) external {
require(ownerOf(parentTokenId) == msg.sender, "Not owner");
// 转移子NFT到本合约
IERC721(childContract).safeTransferFrom(
msg.sender,
address(this),
childTokenId
);
// 记录
childNFTs[parentTokenId][childContract][childTokenId] = true;
childNFTList[parentTokenId].push(ChildNFT(childContract, childTokenId));
}
// 移除子NFT
function removeChildNFT(
uint256 parentTokenId,
address childContract,
uint256 childTokenId
) external {
require(ownerOf(parentTokenId) == msg.sender, "Not owner");
require(childNFTs[parentTokenId][childContract][childTokenId], "Not a child");
// 删除记录
childNFTs[parentTokenId][childContract][childTokenId] = false;
// 转移子NFT回给所有者
IERC721(childContract).safeTransferFrom(
address(this),
msg.sender,
childTokenId
);
}
// 查询子NFT
function getChildNFTs(uint256 parentTokenId) external view returns (ChildNFT[] memory) {
return childNFTList[parentTokenId];
}
// 实现ERC721Receiver
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external pure override returns (bytes4) {
return this.onERC721Received.selector;
}
// 转移父NFT时,子NFT也一起转移
function _transfer(
address from,
address to,
uint256 tokenId
) internal virtual override {
super._transfer(from, to, tokenId);
// 注意: 这里简化了,实际应该递归转移所有子NFT
}
}
九、NFT安全最佳实践
9.1 常见安全问题
1. 重入攻击
// 不安全的提现
function withdraw() external {
uint256 amount = balances[msg.sender];
(bool success, ) = msg.sender.call{value: amount}(""); // 重入点
require(success);
balances[msg.sender] = 0; // 太晚了
}
// 安全的提现
function withdraw() external nonReentrant {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0; // 先更新状态
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
2. 整数溢出
// Solidity 0.8.0之前
uint256 max = type(uint256).max;
max + 1; // 溢出为0
// Solidity 0.8.0之后,自动检查溢出
// 如果溢出会自动revert
3. 伪随机数不安全
// 不安全: 矿工可以操纵
uint256 random = uint256(keccak256(abi.encodePacked(block.timestamp)));
// 安全: 使用Chainlink VRF
4. 前端运行(Front Running)
// 交易在mempool中可见,攻击者可以抢先交易
function mint() external payable {
// 攻击者看到你的交易后,用更高的Gas Price抢先铸造
}
// 缓解方案: 使用Commit-Reveal模式
9.2 安全检查清单
使用最新的Solidity版本(0.8.x)
使用OpenZeppelin等经过审计的库
添加ReentrancyGuard
检查所有external调用的返回值
使用require检查输入参数
不要依赖block.timestamp作为随机源
设置合理的Gas Limit
添加紧急暂停功能
进行代码审计
部署前在测试网测试
十、总结
核心要点
ERC721 vs ERC1155
- ERC721: 每个Token唯一
- ERC1155: 批量操作,Gas效率高
元数据存储
- IPFS: 去中心化,防篡改
- Arweave: 永久存储
- 链上: 最去中心化但成本高
NFT市场
- 上架/下架
- 直接购买
- 出价/拍卖
- 版税分配
盲盒机制
- Chainlink VRF: 真随机
- 伪随机: 简单但不安全
- Commit-Reveal: 折中方案
版税标准
- ERC2981: 版税接口
- 二级市场集成
下一步学习
- 10-Layer2扩容方案.md - 了解如何降低NFT铸造和交易的Gas费用
- 12-Web3前端开发.md - 深入学习NFT市场的前端开发
实战建议
- 先在测试网部署和测试
- 使用OpenZeppelin库而不是从头编写
- 代码审计后再上主网
- 关注Gas优化
- 设计合理的经济模型