HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • Web3 完整技术体系

    • Web3 完整技术体系
    • 区块链基础与密码学
    • 第一章:比特币原理与实现
    • 第二章:以太坊架构与核心概念
    • Solidity智能合约开发基础
    • 04-Solidity进阶与安全
    • 05-ERC标准详解
    • 06-DeFi核心协议-去中心化交易所
    • 07-DeFi核心协议-借贷协议
    • 08-DeFi核心协议-稳定币
    • NFT与元宇宙
    • Layer2扩容方案
    • 跨链技术
    • Web3前端开发
    • 链下数据与预言机
    • 智能合约测试与部署
    • MEV与交易优化
    • DAO治理
    • 项目实战:完整DeFi借贷协议

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

特性ERC721ERC1155
代币类型每个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
 添加紧急暂停功能
 进行代码审计
 部署前在测试网测试

十、总结

核心要点

  1. ERC721 vs ERC1155

    • ERC721: 每个Token唯一
    • ERC1155: 批量操作,Gas效率高
  2. 元数据存储

    • IPFS: 去中心化,防篡改
    • Arweave: 永久存储
    • 链上: 最去中心化但成本高
  3. NFT市场

    • 上架/下架
    • 直接购买
    • 出价/拍卖
    • 版税分配
  4. 盲盒机制

    • Chainlink VRF: 真随机
    • 伪随机: 简单但不安全
    • Commit-Reveal: 折中方案
  5. 版税标准

    • ERC2981: 版税接口
    • 二级市场集成

下一步学习

  • 10-Layer2扩容方案.md - 了解如何降低NFT铸造和交易的Gas费用
  • 12-Web3前端开发.md - 深入学习NFT市场的前端开发

实战建议

  1. 先在测试网部署和测试
  2. 使用OpenZeppelin库而不是从头编写
  3. 代码审计后再上主网
  4. 关注Gas优化
  5. 设计合理的经济模型
Prev
08-DeFi核心协议-稳定币
Next
Layer2扩容方案