ERC-721 與 ERC-1155 技術實作完整指南:非同質化代幣標準的工程實現與進階應用

本文深入探討 ERC-721 和 ERC-1155 兩大 NFT 標準的技術實作細節,從合約架構到安全考量,從元資料設計到擴展應用,提供工程師級別的完整技術指南。我們涵蓋完整程式碼範例、標準介面定義、可升級合約實現、分片 NFT 等進階主題,幫助開發者掌握非同質化代幣開發的核心技術。

ERC-721 與 ERC-1155 技術實作完整指南:非同質化代幣標準的工程實現與進階應用

概述

非同質化代幣(Non-Fungible Token,NFT)是以太坊生態系統中最重要的創新之一,徹底改變了數位資產所有權的概念。與可互換的 ERC-20 代幣不同,NFT 代表獨一無二的數位資產,具有不可分割、不可複製的特性。本文深入探討 ERC-721 和 ERC-1155 兩大 NFT 標準的技術實作細節,從合約架構到安全考量,從元資料設計到擴展應用,提供工程師級別的完整技術指南。

ERC-721 標準深度解析

標準規範與核心介面

ERC-721 標準由 Dieter Shirley 於 2017 年提出,並在 EIP-721 中正式定義。該標準的核心特點是每個代幣 ID 都是唯一的,這使得每個代幣可以代表一個獨一無二的資產。標準定義了以下核心介面:

// ERC-721 核心介面定義(IERC721.sol)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Metadata.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";

/**
 * @dev ERC-721 代幣標準介面
 * 參考: https://eips.ethereum.org/EIPS/eip-721
 */
interface IERC721 is IERC165 {
    // 轉移事件:當代幣從一個地址轉移到另一個地址時觸發
    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);

    // 查詢代幣餘額:返回指定地址持有的代幣數量
    function balanceOf(address owner) external view returns (uint256);

    // 查詢所有者:返回指定代幣 ID 的當前所有者
    function ownerOf(uint256 tokenId) external view returns (address);

    // 安全轉移:將代幣從一個地址轉移到另一個地址,包含callback驗證
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;

    // 安全轉移(不攜帶數據)
    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 setApprovalForAll(address operator, bool approved) external;

    // 查詢被批准的地址:返回被授權可以轉移特定代幣的地址
    function getApproved(uint256 tokenId) external view returns (address);

    // 查詢操作者權限:返回指定操作者是否被授權轉移所有代幣
    function isApprovedForAll(address owner, address operator) external view returns (bool);
}

/**
 * @dev ERC-721 元資料擴展介面
 */
interface IERC721Metadata is IERC721 {
    // 返回代幣名稱
    function name() external view returns (string memory);
    
    // 返回代幣符號
    function symbol() external view returns (string memory);
    
    // 返回特定代幣的 URI
    function tokenURI(uint256 tokenId) external view returns (string memory);
}

/**
 * @dev ERC-721 接收者介面
 * 用於確保合約能夠接收 ERC-721 代幣
 */
interface IERC721Receiver {
    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) external returns (bytes4);
}

完整 ERC-721 實作

以下是一個完整的 ERC-721 代幣合約實作,展示了所有關鍵功能的工程實現:

// ERC-721 完整實現(MyNFT.sol)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

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

/**
 * @title MyNFT
 * @dev ERC-721 代幣的完整實現示例
 * 包含鑄造、URI管理、批量轉移等進階功能
 */
contract MyNFT is ERC721, ERC721URIStorage, ERC721Burnable, Ownable {
    using Counters for Counters.Counter;
    
    // 代幣 ID 計數器
    Counters.Counter private _tokenIds;
    
    // 基礎 URI
    string private _baseTokenURI;
    
    // 每個地址的最大鑄造數量限制
    mapping(address => uint256) public mintLimits;
    
    // 代幣 ID 到創建時間的映射
    mapping(uint256 => uint256) public creationTimestamps;
    
    // 創世限定:只能鑄造特定數量的代幣
    uint256 public constant MAX_SUPPLY = 10000;
    
    // 許可清單
    mapping(address => bool) public whitelist;
    
    // 允許開始鑄造的時間
    uint256 public mintStartTime;
    
    /**
     * @dev 合約構造函數
     * @param name 代幣名稱
     * @param symbol 代幣符號
     * @param baseURI 基礎 URI
     */
    constructor(
        string memory name,
        string memory symbol,
        string memory baseURI
    ) ERC721(name, symbol) Ownable(msg.sender) {
        _baseTokenURI = baseURI;
        mintStartTime = block.timestamp;
    }
    
    /**
     * @dev 設置基礎 URI
     */
    function setBaseURI(string memory baseURI) external onlyOwner {
        _baseTokenURI = baseURI;
    }
    
    /**
     * @dev 設置mint開始時間
     */
    function setMintStartTime(uint256 timestamp) external onlyOwner {
        mintStartTime = timestamp;
    }
    
    /**
     * @dev 設置地址的鑄造限制
     */
    function setMintLimit(address user, uint256 limit) external onlyOwner {
        mintLimits[user] = limit;
    }
    
    /**
     * @dev 添加到許可清單
     */
    function addToWhitelist(address[] calldata users) external onlyOwner {
        for (uint256 i = 0; i < users.length; i++) {
            whitelist[users[i]] = true;
        }
    }
    
    /**
     * @dev 從許可清單移除
     */
    function removeFromWhitelist(address[] calldata users) external onlyOwner {
        for (uint256 i = 0; i < users.length; i++) {
            whitelist[users[i]] = false;
        }
    }
    
    /**
     * @dev 檢查是否在許可清單中
     */
    function isWhitelisted(address user) public view returns (bool) {
        return whitelist[user];
    }
    
    /**
     * @dev 公開鑄造函數(示例)
     * 實際項目應添加支付邏輯
     */
    function mint(address to) public returns (uint256) {
        require(block.timestamp >= mintStartTime, "Minting not started");
        require(_tokenIds.current() < MAX_SUPPLY, "Max supply reached");
        
        // 檢查 mint 限制
        if (mintLimits[msg.sender] > 0) {
            require(
                balanceOf(msg.sender) < mintLimits[msg.sender],
                "Mint limit reached"
            );
        }
        
        _tokenIds.increment();
        uint256 newTokenId = _tokenIds.current();
        
        _safeMint(to, newTokenId);
        creationTimestamps[newTokenId] = block.timestamp;
        
        return newTokenId;
    }
    
    /**
     * @dev 批量鑄造
     */
    function batchMint(address to, uint256 quantity) external returns (uint256[] memory) {
        require(block.timestamp >= mintStartTime, "Minting not started");
        require(
            _tokenIds.current() + quantity <= MAX_SUPPLY,
            "Would exceed max supply"
        );
        
        uint256[] memory tokenIds = new uint256[](quantity);
        
        for (uint256 i = 0; i < quantity; i++) {
            _tokenIds.increment();
            uint256 newTokenId = _tokenIds.current();
            
            _safeMint(to, newTokenId);
            creationTimestamps[newTokenId] = block.timestamp;
            tokenIds[i] = newTokenId;
        }
        
        return tokenIds;
    }
    
    /**
     * @dev 批量設置 URI
     */
    function setTokenURIs(uint256[] calldata tokenIds, string[] calldata uris) external {
        require(tokenIds.length == uris.length, "Length mismatch");
        
        for (uint256 i = 0; i < tokenIds.length; i++) {
            require(
                ownerOf(tokenIds[i]) == msg.sender || msg.sender == owner(),
                "Not authorized"
            );
            _setTokenURI(tokenIds[i], uris[i]);
        }
    }
    
    /**
     * @dev 批量轉移
     */
    function batchTransferFrom(
        address from,
        address to,
        uint256[] calldata tokenIds
    ) external {
        for (uint256 i = 0; i < tokenIds.length; i++) {
            require(
                ownerOf(tokenIds[i]) == from,
                "Token not owned by from address"
            );
            transferFrom(from, to, tokenIds[i]);
        }
    }
    
    /**
     * @dev 安全批量轉移
     */
    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] calldata tokenIds,
        bytes calldata data
    ) external {
        for (uint256 i = 0; i < tokenIds.length; i++) {
            safeTransferFrom(from, to, tokenIds[i], data);
        }
    }
    
    /**
     * @dev 獲取指定地址的所有代幣 ID
     */
    function tokensOfOwner(address owner) external view returns (uint256[] memory) {
        uint256 ownerTokenCount = balanceOf(owner);
        uint256[] memory ownedTokenIds = new uint256[](ownerTokenCount);
        
        uint256 currentTokenId = 1;
        uint256 ownedTokenIndex = 0;
        
        while (currentTokenId <= _tokenIds.current()) {
            try ownerOf(currentTokenId) returns (address tokenOwner) {
                if (tokenOwner == owner) {
                    ownedTokenIds[ownedTokenIndex] = currentTokenId;
                    ownedTokenIndex++;
                }
            } catch {
                // 忽略錯誤
            }
            currentTokenId++;
        }
        
        return ownedTokenIds;
    }
    
    /**
     * @dev 獲取代幣的創建時間
     */
    function getCreationTimestamp(uint256 tokenId) external view returns (uint256) {
        require(_exists(tokenId), "Token does not exist");
        return creationTimestamps[tokenId];
    }
    
    // 覆蓋函數
    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }
    
    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

ERC-721 元資料設計

元資料是 NFT 的核心組成部分,定義了代幣的視覺呈現和附加屬性。ERC-721 標準通過 tokenURI 函數返回一個 JSON 物件,包含以下欄位:

{
    "name": "My NFT #1234",
    "description": "這是一個獨一無二的數位收藏品",
    "image": "https://example.com/images/1234.png",
    "external_url": "https://example.com/nft/1234",
    "attributes": [
        {
            "trait_type": "Background",
            "value": "Blue"
        },
        {
            "trait_type": "Character",
            "value": "Warrior",
            "display_type": "string"
        },
        {
            "trait_type": "Power",
            "value": 85,
            "display_type": "number",
            "max_value": 100
        },
        {
            "trait_type": "Rarity",
            "value": "Legendary",
            "display_type": "string"
        }
    ],
    "properties": {
        "category": "character",
        "creator": "0x1234567890123456789012345678901234567890",
        "edition": 1,
        "series": "Hero Collection"
    }
}

元資料的設計需要考慮以下幾個關鍵點:

集中式存儲 vs 去中心化存儲:傳統做法是將元資料存儲在 IPFS 或 Arweave 等去中心化存儲網路上,確保元資料不可篡改。然而,由於 IPFS 需要固定的 CID,且大多數 NFT 項目使用 Pinata 或 Infura 等中心化服務進行 pin,這種方式實際上存在單點故障風險。更安全的做法是將完整元資料(包括圖片)存儲在 IPFS 上,並使用 IPNS 或域名綁定來實現可更新的元資料。

動態元資料:某些應用場景需要動態更新 NFT 元資料,例如遊戲角色的屬性變化。這可以通過智能合約實現,合約中存儲元資料的基礎 URI,並在 tokenURI 函數中動態組裝 JSON。

ERC-1155 標準深度解析

標準概述與設計理念

ERC-1155 是由 Enjin 團隊於 2019 年提出的多代幣標準,設計理念是將同質化代幣和非同質化代幣的功能整合到單一智能合約中。與 ERC-721 每個代幣類型需要獨立合約不同,ERC-1155 允許在單一合約中管理任意數量的代幣類型。

// ERC-1155 核心介面
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155MetadataURI.sol";

/**
 * @dev ERC-1155 多代幣標準介面
 * 參考: https://eips.ethereum.org/EIPS/eip-1155
 */
interface IERC1155 is IERC165 {
    // 單一代幣轉移事件
    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
    );
    
    // URI 更新事件
    event URI(string value, uint256 indexed id);

    // 查詢帳戶餘額(特定代幣 ID)
    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 value,
        bytes calldata data
    ) external;

    // 安全批量轉移
    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] calldata ids,
        uint256[] calldata values,
        bytes calldata data
    ) external;
}

/**
 * @dev ERC-1155 元資料擴展
 */
interface IERC1155MetadataURI is IERC1155 {
    // 返回代幣 ID 對應的 URI
    // 客戶端會自動將 {id} 替換為實際的代幣 ID
    function uri(uint256 id) external view returns (string memory);
}

ERC-1155 完整實作

// ERC-1155 完整實現(MultiToken.sol)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

/**
 * @title MultiToken
 * @dev ERC-1155 多代幣標準實現
 */
contract MultiToken is ERC1155, ERC1155Burnable, Ownable {
    using Strings for uint256;
    
    // 代幣類型 ID 到 URI 的映射
    mapping(uint256 => string) private _tokenURIs;
    
    // 代幣類型 ID 到供應量的映射
    mapping(uint256 => uint256) private _totalSupply;
    
    // 代幣類型 ID 到最大供應量的映射(0 表示無限制)
    mapping(uint256 => uint256) public maxSupply;
    
    // 代幣類型 ID 到鑄造者的映射
    mapping(uint256 => address) public creators;
    
    // 代幣類型 ID 到凍結狀態的映射
    mapping(uint256 => bool) public frozen;
    
    // 合約層面的 URI(用於展示合約資訊)
    string public contractURI;
    
    // 代幣類型 ID 計數器
    uint256 public nextTokenId;
    
    // 創世代幣類型 ID(從 1 開始)
    uint256 public constant TOKEN_ID_START = 1;
    
    /**
     * @dev 構造函數
     * @param uri_ 基礎 URI
     * @param contractURI_ 合約層面 URI
     */
    constructor(string memory uri_, string memory contractURI_)
        ERC1155(uri_)
        Ownable(msg.sender)
    {
        contractURI = contractURI_;
        nextTokenId = TOKEN_ID_START;
    }
    
    /**
     * @dev 創建新的代幣類型
     * @param initialSupply 初始供應量
     * @param maxSupply_ 最大供應量(0 表示無限制)
     * @param uri_ 代幣 URI
     * @param isNF 是否為非同質化代幣
     * @return 新創建的代幣類型 ID
     */
    function createToken(
        uint256 initialSupply,
        uint256 maxSupply_,
        string memory uri_,
        bool isNF
    ) external onlyOwner returns (uint256) {
        uint256 newTokenId = nextTokenId++;
        
        // 設置 URI
        _tokenURIs[newTokenId] = uri_;
        
        // 設置最大供應量
        maxSupply[newTokenId] = maxSupply_;
        
        // 記錄創建者
        creators[newTokenId] = msg.sender;
        
        // 如果是 NFT(供應量為 1)或需要初始鑄造
        if (isNF) {
            require(initialSupply <= 1, "NFT supply must be <= 1");
            if (initialSupply == 1) {
                _mint(msg.sender, newTokenId, 1, "");
                _totalSupply[newTokenId] = 1;
            }
        } else if (initialSupply > 0) {
            require(
                maxSupply_ == 0 || initialSupply <= maxSupply_,
                "Initial supply exceeds max"
            );
            _mint(msg.sender, newTokenId, initialSupply, "");
            _totalSupply[newTokenId] = initialSupply;
        }
        
        // 設置 URI(會觸發 URI 事件)
        emit URI(uri_, newTokenId);
        
        return newTokenId;
    }
    
    /**
     * @dev 批量創建代幣類型
     */
    function batchCreateToken(
        uint256[] calldata initialSupplies,
        uint256[] calldata maxSupplies,
        string[] calldata uris,
        bool[] calldata isNFs
    ) external onlyOwner returns (uint256[] memory) {
        require(
            initialSupplies.length == uris.length &&
            uris.length == maxSupplies.length &&
            maxSupplies.length == isNFs.length,
            "Length mismatch"
        );
        
        uint256[] memory tokenIds = new uint256[](uris.length);
        
        for (uint256 i = 0; i < uris.length; i++) {
            tokenIds[i] = createToken(
                initialSupplies[i],
                maxSupplies[i],
                uris[i],
                isNFs[i]
            );
        }
        
        return tokenIds;
    }
    
    /**
     * @dev 鑄造代幣(增加供應量)
     */
    function mint(
        address to,
        uint256 id,
        uint256 amount,
        bytes calldata data
    ) external {
        require(!frozen[id], "Token type frozen");
        
        // 檢查最大供應量
        uint256 max = maxSupply[id];
        if (max > 0) {
            require(
                _totalSupply[id] + amount <= max,
                "Would exceed max supply"
            );
        }
        
        // 檢查權限:只有創建者或合約所有者可以鑄造
        require(
            creators[id] == msg.sender || msg.sender == owner(),
            "Not authorized to mint"
        );
        
        _mint(to, id, amount, data);
        _totalSupply[id] += amount;
    }
    
    /**
     * @dev 批量鑄造代幣
     */
    function batchMint(
        address to,
        uint256[] calldata ids,
        uint256[] calldata amounts,
        bytes calldata data
    ) external {
        require(ids.length == amounts.length, "Length mismatch");
        
        for (uint256 i = 0; i < ids.length; i++) {
            uint256 id = ids[i];
            uint256 amount = amounts[i];
            
            require(!frozen[id], "Token type frozen");
            
            uint256 max = maxSupply[id];
            if (max > 0) {
                require(
                    _totalSupply[id] + amount <= max,
                    "Would exceed max supply"
                );
            }
            
            require(
                creators[id] == msg.sender || msg.sender == owner(),
                "Not authorized to mint"
            );
            
            _totalSupply[id] += amount;
        }
        
        _mintBatch(to, ids, amounts, data);
    }
    
    /**
     * @dev 批量轉移
     */
    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] calldata ids,
        uint256[] calldata amounts,
        bytes calldata data
    ) public override {
        super.safeBatchTransferFrom(from, to, ids, amounts, data);
    }
    
    /**
     * @dev 凍結代幣類型(禁止後續鑄造)
     */
    function freeze(uint256 id) external onlyOwner {
        require(creators[id] == msg.sender, "Not creator");
        frozen[id] = true;
    }
    
    /**
     * @dev 解凍代幣類型
     */
    function unfreeze(uint256 id) external onlyOwner {
        require(creators[id] == msg.sender, "Not creator");
        frozen[id] = false;
    }
    
    /**
     * @dev 更新代幣 URI
     */
    function setURI(uint256 id, string memory newURI) external {
        require(creators[id] == msg.sender || msg.sender == owner(), "Not authorized");
        _tokenURIs[id] = newURI;
        emit URI(newURI, id);
    }
    
    /**
     * @dev 設置合約 URI
     */
    function setContractURI(string memory newContractURI) external onlyOwner {
        contractURI = newContractURI;
    }
    
    /**
     * @dev 獲取代幣 URI
     */
    function uri(uint256 id) public view override returns (string memory) {
        return _tokenURIs[id];
    }
    
    /**
     * @dev 獲取代幣總供應量
     */
    function totalSupply(uint256 id) external view returns (uint256) {
        return _totalSupply[id];
    }
    
    /**
     * @dev 獲取多個代幣的總供應量
     */
    function totalSupplyBatch(uint256[] calldata ids)
        external
        view
        returns (uint256[] memory)
    {
        uint256[] memory supplies = new uint256[](ids.length);
        for (uint256 i = 0; i < ids.length; i++) {
            supplies[i] = _totalSupply[ids[i]];
        }
        return supplies;
    }
    
    /**
     * @dev 檢查代幣是否存在
     */
    function exists(uint256 id) external view returns (bool) {
        return _totalSupply[id] > 0;
    }
    
    /**
     * @dev 獲取代幣創建者
     */
    function getCreator(uint256 id) external view returns (address) {
        return creators[id];
    }
    
    /**
     * @dev 實現 ERC-165
     */
    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC1155)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

ERC-721 與 ERC-1155 的比較

選擇合適的代幣標準需要根據具體應用場景進行權衡。以下是兩種標準的詳細比較:

功能特性比較

特性ERC-721ERC-1155
代幣類型數量每個合約一種同一合約支援多種
代幣 ID每個代幣 ID 唯一可重複(FT)或唯一(NFT)
批量轉移需要額外實現內建支援
Gas 效率單一合約部署較高批量操作更高效
簡單性簡單直觀相對複雜
兼容性廣泛支援較新,支援較少

Gas 成本分析

假設部署一個包含 1000 個代幣的集合:

ERC-721 實現:

ERC-1155 實現:

實際節省取決於具體實現和操作類型。ERC-1155 在以下場景特別有優勢:遊戲內多種類型的道具、大規模的代幣化資產、需要在單一交易中轉移多種代幣的應用。

安全考量與最佳實踐

智慧合約安全要點

NFT 智慧合約面臨多種安全威脅,以下是關鍵的安全考量:

Reentrancy 攻擊防護:雖然 ERC-721 的轉移函數使用了 SafeMath 或 Checks-Effects-Interactions 模式,但在自定義的鉤子函數中仍需小心。建議使用 ReentrancyGuard:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureNFT is ERC721, ReentrancyGuard {
    function withdraw() external nonReentrant {
        // 提款邏輯
    }
}

整數溢位處理:使用 Solidity 0.8+ 內建的溢出檢查,或使用 SafeMath 庫:

// Solidity 0.8+ 自動處理溢出
uint256 totalMinted = _tokenIds.current() + amount;
// 如果溢出會 revert

// 或者手動使用 SafeMath
using SafeMath for uint256;
uint256 totalMinted = _tokenIds.current().add(amount);

權限控制:確保只有授權的地址可以執行敏感操作:

// 使用 Ownable
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyNFT is ERC721, Ownable {
    function mint(address to) public onlyOwner {
        // 鑄造邏輯
    }
}

// 使用 Role-Based Access Control
import "@openzeppelin/contracts/access/AccessControl.sol";

contract MyNFT is ERC721, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    
    function mint(address to) public onlyRole(MINTER_ROLE) {
        // 鑄造邏輯
    }
}

前端交互安全

假代幣檢測:在顯示 NFT 之前,應驗證合約是否為合法的 ERC-721 或 ERC-1155 合約:

// 檢測合約是否為 ERC-721
async function isERC721(web3, contractAddress) {
    const contract = new web3.eth.Contract(ERC721_ABI, contractAddress);
    try {
        // 嘗試調用 ERC-721 特有函數
        await contract.methods.supportsInterface('0x80ac58cd').call();
        return true;
    } catch (e) {
        return false;
    }
}

// 驗證代幣所有權
async function verifyOwnership(contractAddress, tokenId, ownerAddress) {
    const contract = new web3.eth.Contract(ERC721_ABI, contractAddress);
    const owner = await contract.methods.ownerOf(tokenId).call();
    return owner.toLowerCase() === ownerAddress.toLowerCase();
}

元資料驗證:驗證元資料的完整性和真實性:

async function validateMetadata(tokenURI) {
    // 檢查 URI 格式
    if (!tokenURI.startsWith('ipfs://') && !tokenURI.startsWith('https://')) {
        throw new Error('Invalid URI scheme');
    }
    
    // 獲取並解析元資料
    const response = await fetch(tokenURI);
    const metadata = await response.json();
    
    // 驗證必要欄位
    if (!metadata.name || !metadata.image) {
        throw new Error('Missing required metadata fields');
    }
    
    // 檢查圖片 URL
    if (!metadata.image.startsWith('ipfs://') && 
        !metadata.image.startsWith('https://')) {
        throw new Error('Invalid image URI');
    }
    
    return metadata;
}

進階應用模式

可升級 NFT

使用代理模式實現可升級的 NFT 合約:

// 代理合約示例
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

/**
 * @dev 可升級 NFT 實現
 */
contract NFTProxy is ERC1967Proxy {
    constructor(address _implementation, bytes memory _data)
        ERC1967Proxy(_implementation, _data)
    {}
}

// 初始化代理合約
function deployUpgradeable(address implementation, bytes memory initializerData) 
    public 
    returns (address) 
{
    bytes memory initializationCalldata = abi.encodeWithSignature(
        "initialize(string,string)",
        "MyNFT",
        "MNFT"
    );
    
    return address(new NFTProxy(implementation, initializationCalldata));
}

分片 NFT(Fractional NFT)

將 NFT 拆分為多個可互換的代幣份額:

// FractionalNFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

/**
 * @dev 分片 NFT 合約
 * 允許將一個 NFT 拆分為多個 ERC-20 代幣
 */
contract FractionalNFT is ERC20, ERC721, ReentrancyGuard {
    // NFT ID 到份額總數的映射
    mapping(uint256 => uint256) public shares;
    
    // NFT ID 到拍賣狀態的映射
    mapping(uint256 => bool) public listedForSale;
    
    // NFT ID 到最低價格的映射
    mapping(uint256 => uint256) public listPrices;
    
    // 代幣名稱和符號
    string private _name = "Fractional NFT";
    string private _symbol = "FNFT";
    
    /**
     * @dev 拆分 NFT
     * @param tokenId 要拆分的 NFT ID
     * @param shares_ 拆分的份額數量
     */
    function fractionalize(uint256 tokenId, uint256 shares_) 
        external 
        nonReentrant 
    {
        require(ownerOf(tokenId) == msg.sender, "Not owner");
        require(shares_ > 0, "Invalid shares");
        require(!listedForSale[tokenId], "Already listed");
        
        // 轉移 NFT 到合約
        _transfer(msg.sender, address(this), tokenId);
        
        // 記錄份額
        shares[tokenId] = shares_;
        
        // 鑄造 ERC-20 代幣
        _mint(msg.sender, shares_ * (10 ** decimals()));
    }
    
    /**
     * @dev 購買碎片(提議收購)
     * @param tokenId NFT ID
     */
    function buyFraction(uint256 tokenId) external payable nonReentrant {
        require(listedForSale[tokenId], "Not listed");
        require(msg.value >= listPrices[tokenId], "Insufficient payment");
        
        uint256 totalShares = shares[tokenId];
        uint256 pricePerShare = listPrices[tokenId] / totalShares;
        
        // 燒毀買家的 ERC-20 代幣份額
        _burn(msg.sender, totalShares);
        
        // 將 NFT 轉移給買家
        _transfer(address(this), msg.sender, tokenId);
        
        // 清除掛單狀態
        listedForSale[tokenId] = false;
        listPrices[tokenId] = 0;
    }
    
    /**
     * @dev 掛單出售(需要燒毀所有份額)
     * @param tokenId NFT ID
     * @param price 售價
     */
    function listForSale(uint256 tokenId, uint256 price) external nonReentrant {
        require(balanceOf(msg.sender) == shares[tokenId], "Must own all shares");
        
        listedForSale[tokenId] = true;
        listPrices[tokenId] = price;
    }
}

結論

ERC-721 和 ERC-1155 是以太坊 NFT 生態的兩大支柱標準,各有其適用場景和優勢。ERC-721 適合需要高度獨特性的數位收藏品,如藝術品、遊戲角色等;ERC-1155 則更適合需要管理大量同質化或非同質化資產的應用,如遊戲道具、票務系統等。

在實際項目中,開發者應根據具體需求選擇合適的標準,並遵循安全最佳實踐。元資料的設計、Gas 優化、權限控制都是需要仔細考慮的關鍵因素。隨著以太坊生態的持續發展,這些標準也在不斷演進,未來可能會出現更多創新的應用模式。


參考資料

  1. EIP-721: Non-Fungible Token Standard - https://eips.ethereum.org/EIPS/eip-721
  2. EIP-1155: Multi Token Standard - https://eips.ethereum.org/EIPS/eip-1155
  3. OpenZeppelin ERC-721 Documentation - https://docs.openzeppelin.com/contracts/4.x/erc721
  4. OpenZeppelin ERC-1155 Documentation - https://docs.openzeppelin.com/contracts/4.x/erc1155
  5. ERC-721 Metadata JSON Schema - https://docs.openzeppelin.com/contracts/4.x/erc721#metadata

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。

目前尚無評論,成為第一個發表評論的人吧!