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-721 | ERC-1155 |
|---|---|---|
| 代幣類型數量 | 每個合約一種 | 同一合約支援多種 |
| 代幣 ID | 每個代幣 ID 唯一 | 可重複(FT)或唯一(NFT) |
| 批量轉移 | 需要額外實現 | 內建支援 |
| Gas 效率 | 單一合約部署較高 | 批量操作更高效 |
| 簡單性 | 簡單直觀 | 相對複雜 |
| 兼容性 | 廣泛支援 | 較新,支援較少 |
Gas 成本分析:
假設部署一個包含 1000 個代幣的集合:
ERC-721 實現:
- 部署合約:~2,000,000 gas
- 鑄造 1000 個 NFT:~1,500,000 gas
- 1000 次餘額查詢:~30,000 gas
ERC-1155 實現:
- 部署合約:~1,800,000 gas
- 批量鑄造 1000 個代幣:~800,000 gas
- 1000 次餘額查詢:~30,000 gas
實際節省取決於具體實現和操作類型。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 優化、權限控制都是需要仔細考慮的關鍵因素。隨著以太坊生態的持續發展,這些標準也在不斷演進,未來可能會出現更多創新的應用模式。
參考資料
- EIP-721: Non-Fungible Token Standard - https://eips.ethereum.org/EIPS/eip-721
- EIP-1155: Multi Token Standard - https://eips.ethereum.org/EIPS/eip-1155
- OpenZeppelin ERC-721 Documentation - https://docs.openzeppelin.com/contracts/4.x/erc721
- OpenZeppelin ERC-1155 Documentation - https://docs.openzeppelin.com/contracts/4.x/erc1155
- ERC-721 Metadata JSON Schema - https://docs.openzeppelin.com/contracts/4.x/erc721#metadata
相關文章
- 以太坊智慧合約設計模式完整指南:常見架構與工程實踐 — 本文深入探討以太坊智慧合約開發中最關鍵的設計模式,從基礎的訪問控制模式到進階的代理升級模式,提供完整的程式碼範例與工程實踐指導。涵蓋 Ownable、AccessControl、可升級代理、ReentrancyGuard、Pull Payment、Pausable、Oracle 集成等核心模式,幫助開發者構建安全、高效、可維護的智慧合約。
- 以太坊代幣轉帳完整指南:ERC-20、ERC-721 與地址驗證深度實作 — 本指南從工程師視角出發,提供完整的代幣轉帳流程說明、地址格式驗證實作、以及常見錯誤的處理策略。涵蓋地址格式驗證的密碼學基礎、ERC-20 代幣轉帳的完整流程與程式碼範例、ERC-721 NFT 轉帳的安全考量、以及 Solidity、JavaScript 和 Python 三種語言的實作範例。同時深入探討跨鏈代幣轉帳、代幣轉帳的最佳實踐與錯誤診斷方法。
- OpenZeppelin 可升級智能合約完整指南:代理模式、升級機制與最佳實踐 — 可升級智能合約是以太坊生態系統中最重要的技術創新之一,它解決了區塊鏈「代碼即法律」特性帶來的兩難困境。OpenZeppelin 提供了完整的可升級合約開發框架,包括透明代理、UUPS、Beacon代理等多種模式。本文深入探討可升級合約的技術原理、代理模式比較、存儲管理、升級安全性與最佳實踐,為開發者提供完整的技術參考。
- ERC-4626 Tokenized Vault 完整實現指南:從標準規範到生產級合約 — 本文深入探討 ERC-4626 標準的技術細節,提供完整的生產級合約實現。內容涵蓋標準接口定義、資產與份額轉換的數學模型、收益策略整合、費用機制設計,並提供可直接部署的 Solidity 代碼範例。通過本指南,開發者可以構建安全可靠的代幣化 vault 系統。
- Solidity 智慧合約完整實作指南:從 ERC-20 到可升級合約的工程實踐 — 本文從工程實踐角度深入講解 Solidity 智慧合約的完整開發流程,涵蓋 ERC-20 代幣合約的完整實現、基於角色的存取控制系統、可升級代理模式、以及使用 Foundry 框架的全面測試策略。我們提供了可直接用於生產環境的程式碼範例,包括完整的 ERC20 實現、AccessControl 角色管理、透明代理合約、以及包含模糊測試的測試套件。透過本文,開發者將掌握編寫安全、高效、可升級智慧合約的核心技能。
延伸閱讀與來源
- Ethereum.org Developers 官方開發者入口與技術文件
- EIPs 以太坊改進提案
這篇文章對您有幫助嗎?
請告訴我們如何改進:
評論
發表評論
注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。
目前尚無評論,成為第一個發表評論的人吧!