🖼️ 第19课:NFT标准

DeFi 阶段四 ✅ 验证通过

🎯 学习目标:深入理解ERC721/ERC1155 NFT标准,掌握所有权验证、转账安全、元数据规范,从零实现NFT合约,理解NFT市场与版税机制。

📖 一、NFT本质:非同质化代币

同质化(Fungible)vs 非同质化(Non-Fungible)

同质化(ERC20):1个BTC = 1个BTC,可以互换
• 1 USDT = 1 USDT,没有区别
• 可以分割:0.5个ETH

非同质化(ERC721):每个代币独一无二
• CryptoPunk #3100 ≠ CryptoPunk #7804
• 不可分割:要么拥有整个NFT,要么没有
• 每个tokenId对应唯一资产

1.1 NFT应用场景

场景示例代表项目
数字艺术唯一画作/音乐Art Blocks, Sound.xyz
收藏品头像/卡牌CryptoPunks, BAYC
游戏资产装备/角色/土地Axie Infinity, Decentraland
身份凭证会员卡/证书POAP, ENS
现实资产房产/奢侈品RealT, Courtyard

📖 二、ERC721标准详解

// IERC721.sol — 核心接口
interface IERC721 {
    // ═══════ 查询函数 ═══════
    function balanceOf(address owner) external view returns (uint256);
    function ownerOf(uint256 tokenId) external view returns (address);
    function tokenURI(uint256 tokenId) external view returns (string memory);

    // ═══════ 转账函数 ═══════
    // 安全转账(检查接收方能否处理NFT)
    function safeTransferFrom(address from, address to, uint256 tokenId) external;
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) 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);
    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);
}

// 接收NFT的合约必须实现此接口
interface IERC721Receiver {
    function onERC721Received(
        address operator, address from, uint256 tokenId, bytes memory data
    ) external returns (bytes4);
}

2.1 safeTransferFrom vs transferFrom

为什么需要"安全"转账?

如果一个NFT被转账到一个不认识NFT的合约,这个NFT就永远锁死了——那个合约没有任何方法把它转走。

safeTransferFrom在转账后会调用接收方的onERC721Received,如果接收方返回正确的magic value(0x150b7a02),说明它能处理NFT;否则交易回滚。

⚠️ 但safeTransferFrom不是万能的——恶意合约可以正确返回magic value但不实现转账逻辑。它只防止意外锁死,不防止恶意锁死

📖 三、实现NFT合约

// contracts/CollectibleNFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

/**
 * @title CollectibleNFT
 * @dev 带版税、可枚举的NFT收藏品
 */
contract CollectibleNFT is ERC721, ERC721URIStorage, ERC721Enumerable, Ownable {
    using Strings for uint256;

    uint256 public maxSupply;
    uint256 public mintPrice;
    uint256 private _nextTokenId;
    string private _baseTokenURI;
    
    // 版税信息 (EIP-2981)
    uint256 public royaltyBasisPoints;  // 基点,如500=5%
    address public royaltyRecipient;

    event Minted(address indexed minter, uint256 tokenId);

    constructor(
        string memory _name,
        string memory _symbol,
        uint256 _maxSupply,
        uint256 _mintPrice,
        uint256 _royaltyBps
    ) ERC721(_name, _symbol) Ownable(msg.sender) {
        maxSupply = _maxSupply;
        mintPrice = _mintPrice;
        royaltyBasisPoints = _royaltyBps;
        royaltyRecipient = msg.sender;
    }

    // ═══════ 铸造函数 ═══════
    function mint() external payable {
        require(_nextTokenId < maxSupply, "Max supply reached");
        require(msg.value >= mintPrice, "Insufficient payment");

        uint256 tokenId = _nextTokenId;
        _nextTokenId++;
        
        _safeMint(msg.sender, tokenId);
        _setTokenURI(tokenId, string(abi.encodePacked(_baseTokenURI, tokenId.toString(), ".json")));

        // 退回多余ETH
        if (msg.value > mintPrice) {
            (bool success,) = msg.sender.call{value: msg.value - mintPrice}("");
            require(success, "Refund failed");
        }

        emit Minted(msg.sender, tokenId);
    }

    // ═══════ 版税 (EIP-2981) ═══════
    function royaltyInfo(uint256 tokenId, uint256 salePrice)
        external view
        returns (address, uint256)
    {
        require(_ownerOf(tokenId) != address(0), "Token not exists");
        uint256 royaltyAmount = (salePrice * royaltyBasisPoints) / 10000;
        return (royaltyRecipient, royaltyAmount);
    }

    // ═══════ 管理函数 ═══════
    function setBaseURI(string memory baseURI) external onlyOwner {
        _baseTokenURI = baseURI;
    }

    function setRoyalty(address _recipient, uint256 _bps) external onlyOwner {
        require(_bps <= 1000, "Royalty too high"); // 最高10%
        royaltyRecipient = _recipient;
        royaltyBasisPoints = _bps;
    }

    function withdraw() external onlyOwner {
        (bool success,) = owner().call{value: address(this).balance}("");
        require(success, "Withdraw failed");
    }

    // ═══════ 重写函数 ═══════
    function tokenURI(uint256 tokenId)
        public view override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId)
        public view override(ERC721, ERC721URIStorage, ERC721Enumerable)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }

    function _update(address to, uint256 tokenId, address auth)
        internal override(ERC721, ERC721Enumerable)
        returns (address)
    {
        return super._update(to, tokenId, auth);
    }
}

📖 四、ERC1155 — 多代币标准

ERC1155 = ERC20 + ERC721 的合体

• 一个合约管理多种代币(半同质化)
• 既可以有同质化代币(如金币,id=1,每个都一样)
• 也可以有非同质化代币(如传奇武器,id=100,只有1个)
批量转账:一笔交易转移多个不同代币
Gas效率:比部署多个ERC721合约节省90%+ Gas
// ERC1155核心接口
interface IERC1155 {
    function balanceOf(address account, uint256 id) external view returns (uint256);
    function balanceOfBatch(address[] memory accounts, uint256[] memory ids) external view returns (uint256[] memory);
    function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes memory data) external;
    function safeBatchTransferFrom(address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory 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);
}

// 游戏物品示例
contract GameItems is ERC1155 {
    uint256 public constant GOLD = 0;      // 同质化:金币
    uint256 public constant SWORD = 1;    // 半同质化:有10把相同的剑
    uint256 public constant LEGENDARY = 2; // 非同质化:唯一传说装备

    constructor() ERC1155("https://game.example/api/{id}.json") {
        _mint(msg.sender, GOLD, 1000000 * 10**18, "");    // 100万金币
        _mint(msg.sender, SWORD, 10, "");                    // 10把剑
        _mint(msg.sender, LEGENDARY, 1, "");                  // 1件传说装备
    }
}

📖 五、ERC721 vs ERC1155对比

特性ERC721ERC1155
代币类型仅非同质化同质化+非同质化
每个ID的数量始终为1任意数量
批量转账不支持(需多笔交易)支持(一笔交易多代币)
合约数量每种NFT一个合约所有代币一个合约
Gas效率较低较高
适用场景艺术品、收藏品游戏、多类型资产

🧪 练习

1. safeTransferFrom防止什么问题? 2. ERC1155相比ERC721的核心优势是什么? 3. EIP-2981版税标准的作用是什么?
ERC-721 vs ERC-1155 选择指南:
• 艺术品/域名/PFP → ERC-721 (每件唯一)
• 游戏道具/门票/票券 → ERC-1155 (批量+多类型)
• 需要批量转账 → ERC-1155 (一次交易多种代币)
• 需要简单性 → ERC-721 (生态更成熟)
🖼️

🏆 成就解锁:数字收藏家

你已掌握NFT标准!从ERC721所有权验证到ERC1155多代币管理,从版税机制到安全转账,你拥有了创建数字资产的能力。

关键收获:

✅ ERC721接口与安全转账机制
✅ NFT合约完整实现(铸造+版税+枚举)
✅ ERC1155多代币标准与批量操作
✅ EIP-2981版税标准
✅ NFT应用场景与市场机制

📋 课程目录