跳到主要内容

NFT 非同质化代币

NFT(Non-Fungible Token,非同质化代币)是 Web3 最重要的应用之一。与同质化代币(如 ETH)不同,每个 NFT 都是独一无二的,可以代表数字艺术品、收藏品、游戏道具、虚拟土地等资产。本章将深入介绍 NFT 的原理和开发实践。

NFT 概述

什么是 NFT

NFT 是区块链上的唯一数字资产凭证。每个 NFT 都有唯一的标识符,不可分割,不可互换。NFT 的核心价值在于:

确权:NFT 为数字资产提供了所有权证明,解决了数字内容易复制、难确权的问题。

可验证:所有权记录在区块链上,任何人都可以验证真伪。

可交易:NFT 可以在二级市场自由交易,为创作者提供了变现渠道。

可编程:NFT 可以包含复杂的逻辑,如版税分成、解锁内容等。

NFT 的应用场景

领域应用示例
数字艺术艺术品交易OpenSea, Foundation
收藏品卡牌、头像CryptoPunks, BAYC
游戏游戏道具Axie Infinity, NBA Top Shot
音乐音乐作品Audius, Royal
虚拟世界虚拟土地Decentraland, The Sandbox
身份会员凭证POAP, ENS
现实资产实物资产映射房产、奢侈品

ERC-721 标准

ERC-721 是最广泛使用的 NFT 标准,定义了非同质化代币的接口。

标准接口

interface IERC721 {
// 事件
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);
function ownerOf(uint256 tokenId) external view returns (address);
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 getApproved(uint256 tokenId) external view returns (address);
function setApprovalForAll(address operator, bool approved) external;
function isApprovedForAll(address owner, address operator) external view returns (bool);
}

实现简单的 NFT

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

contract SimpleNFT is ERC721, Ownable {
using Counters for Counters.Counter;

Counters.Counter private _tokenIdCounter;

uint256 public constant MAX_SUPPLY = 10000;
uint256 public constant MINT_PRICE = 0.05 ether;

string public baseURI;
bool public saleActive = false;

mapping(uint256 => string) private _tokenURIs;

constructor(
string memory _name,
string memory _symbol,
string memory _baseURI
) ERC721(_name, _symbol) Ownable(msg.sender) {
baseURI = _baseURI;
}

// 公开铸造
function mint() external payable {
require(saleActive, "Sale not active");
require(msg.value >= MINT_PRICE, "Insufficient payment");
require(_tokenIdCounter.current() < MAX_SUPPLY, "Max supply reached");

uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();

_safeMint(msg.sender, tokenId);
}

// 批量铸造
function mintBatch(uint256 quantity) external payable {
require(saleActive, "Sale not active");
require(msg.value >= MINT_PRICE * quantity, "Insufficient payment");
require(_tokenIdCounter.current() + quantity <= MAX_SUPPLY, "Exceeds max supply");

for (uint256 i = 0; i < quantity; i++) {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(msg.sender, tokenId);
}
}

// 管理员铸造
function adminMint(address to, uint256 quantity) external onlyOwner {
require(_tokenIdCounter.current() + quantity <= MAX_SUPPLY, "Exceeds max supply");

for (uint256 i = 0; i < quantity; i++) {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(to, tokenId);
}
}

// 设置基础 URI
function setBaseURI(string memory _baseURI) external onlyOwner {
baseURI = _baseURI;
}

// 设置单个 token URI
function setTokenURI(uint256 tokenId, string memory _tokenURI) external onlyOwner {
require(_exists(tokenId), "Token does not exist");
_tokenURIs[tokenId] = _tokenURI;
}

// 开启/关闭销售
function toggleSale() external onlyOwner {
saleActive = !saleActive;
}

// 提取资金
function withdraw() external onlyOwner {
uint256 balance = address(this).balance;
(bool success, ) = payable(owner()).call{value: balance}("");
require(success, "Transfer failed");
}

// 获取 token URI
function tokenURI(uint256 tokenId) public view override returns (string memory) {
require(_exists(tokenId), "Token does not exist");

string memory _tokenURI = _tokenURIs[tokenId];
if (bytes(_tokenURI).length > 0) {
return _tokenURI;
}

return string(abi.encodePacked(baseURI, Strings.toString(tokenId), ".json"));
}

// 获取总供应量
function totalSupply() public view returns (uint256) {
return _tokenIdCounter.current();
}
}

元数据标准

NFT 元数据通常遵循 OpenSea 标准:

{
"name": "NFT Name #1",
"description": "Description of the NFT",
"image": "ipfs://Qm.../1.png",
"external_url": "https://example.com/nft/1",
"attributes": [
{
"trait_type": "Background",
"value": "Blue"
},
{
"trait_type": "Eyes",
"value": "Laser"
},
{
"display_type": "boost_number",
"trait_type": "Power",
"value": 100
}
]
}

属性类型

display_type说明
字符串属性
boost_number增益数值
boost_percentage增益百分比
number普通数值
date日期时间

ERC-1155 多代币标准

ERC-1155 允许一个合约管理多种代币类型,包括同质化和非同质化代币。

标准接口

interface IERC1155 {
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);

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;
}

实现 ERC-1155

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract GameItems is ERC1155, Ownable {
using Counters for Counters.Counter;

// 物品 ID
uint256 public constant GOLD = 0;
uint256 public constant SWORD = 1;
uint256 public constant SHIELD = 2;
uint256 public constant CROWN = 3;

// 物品名称
mapping(uint256 => string) public itemNames;

// 物品是否为 NFT(数量为 1)
mapping(uint256 => bool) public isNFT;

constructor() ERC1155("https://game.example/api/item/{id}.json") Ownable(msg.sender) {
itemNames[GOLD] = "Gold";
itemNames[SWORD] = "Sword";
itemNames[SHIELD] = "Shield";
itemNames[CROWN] = "Crown";

isNFT[CROWN] = true;
}

// 铸造同质化代币
function mintFungible(
address to,
uint256 id,
uint256 amount
) external onlyOwner {
require(!isNFT[id], "Use mintNFT for NFT items");
_mint(to, id, amount, "");
}

// 铸造 NFT
function mintNFT(
address to,
uint256 id
) external onlyOwner {
require(isNFT[id], "Use mintFungible for fungible items");
_mint(to, id, 1, "");
}

// 批量铸造
function mintBatch(
address to,
uint256[] memory ids,
uint256[] memory amounts
) external onlyOwner {
_mintBatch(to, ids, amounts, "");
}

// 燃烧
function burn(
address from,
uint256 id,
uint256 amount
) external {
require(
from == msg.sender || isApprovedForAll(from, msg.sender),
"Not authorized"
);
_burn(from, id, amount);
}

// 批量燃烧
function burnBatch(
address from,
uint256[] memory ids,
uint256[] memory amounts
) external {
require(
from == msg.sender || isApprovedForAll(from, msg.sender),
"Not authorized"
);
_burnBatch(from, ids, amounts);
}

// 设置 URI
function setURI(string memory newuri) external onlyOwner {
_setURI(newuri);
}

// 获取物品名称
function getItemName(uint256 id) external view returns (string memory) {
return itemNames[id];
}
}

NFT 市场合约

创建一个简单的 NFT 市场:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract NFTMarket is Ownable, ReentrancyGuard {
struct Listing {
address seller;
address nftContract;
uint256 tokenId;
uint256 price;
bool active;
}

struct Auction {
address seller;
address nftContract;
uint256 tokenId;
uint256 startingPrice;
uint256 duration;
uint256 endTime;
address highestBidder;
uint256 highestBid;
bool active;
}

uint256 public feePercent = 250; // 2.5%
address public feeRecipient;

mapping(bytes32 => Listing) public listings;
mapping(bytes32 => Auction) public auctions;
mapping(address => uint256) public pendingWithdrawals;

event Listed(
bytes32 indexed listingId,
address indexed seller,
address nftContract,
uint256 tokenId,
uint256 price
);

event Sold(
bytes32 indexed listingId,
address indexed buyer,
uint256 price
);

event AuctionCreated(
bytes32 indexed auctionId,
address indexed seller,
address nftContract,
uint256 tokenId,
uint256 startingPrice,
uint256 duration
);

event BidPlaced(
bytes32 indexed auctionId,
address indexed bidder,
uint256 amount
);

event AuctionEnded(
bytes32 indexed auctionId,
address indexed winner,
uint256 amount
);

constructor(address _feeRecipient) Ownable(msg.sender) {
feeRecipient = _feeRecipient;
}

// 挂单
function listItem(
address nftContract,
uint256 tokenId,
uint256 price
) external nonReentrant returns (bytes32) {
require(price > 0, "Price must be > 0");

IERC721 nft = IERC721(nftContract);
require(nft.ownerOf(tokenId) == msg.sender, "Not owner");
require(
nft.isApprovedForAll(msg.sender, address(this)) ||
nft.getApproved(tokenId) == address(this),
"Not approved"
);

bytes32 listingId = keccak256(
abi.encodePacked(nftContract, tokenId, msg.sender, block.timestamp)
);

listings[listingId] = Listing({
seller: msg.sender,
nftContract: nftContract,
tokenId: tokenId,
price: price,
active: true
});

nft.transferFrom(msg.sender, address(this), tokenId);

emit Listed(listingId, msg.sender, nftContract, tokenId, price);

return listingId;
}

// 取消挂单
function cancelListing(bytes32 listingId) external nonReentrant {
Listing storage listing = listings[listingId];

require(listing.active, "Not active");
require(listing.seller == msg.sender, "Not seller");

listing.active = false;

IERC721(listing.nftContract).transferFrom(
address(this),
msg.sender,
listing.tokenId
);
}

// 购买
function buyItem(bytes32 listingId) external payable nonReentrant {
Listing storage listing = listings[listingId];

require(listing.active, "Not active");
require(msg.value >= listing.price, "Insufficient payment");

listing.active = false;

uint256 fee = (listing.price * feePercent) / 10000;
uint256 sellerProceeds = listing.price - fee;

pendingWithdrawals[listing.seller] += sellerProceeds;
pendingWithdrawals[feeRecipient] += fee;

IERC721(listing.nftContract).transferFrom(
address(this),
msg.sender,
listing.tokenId
);

if (msg.value > listing.price) {
pendingWithdrawals[msg.sender] += msg.value - listing.price;
}

emit Sold(listingId, msg.sender, listing.price);
}

// 创建拍卖
function createAuction(
address nftContract,
uint256 tokenId,
uint256 startingPrice,
uint256 duration
) external nonReentrant returns (bytes32) {
require(duration >= 1 hours && duration <= 7 days, "Invalid duration");

IERC721 nft = IERC721(nftContract);
require(nft.ownerOf(tokenId) == msg.sender, "Not owner");
require(
nft.isApprovedForAll(msg.sender, address(this)) ||
nft.getApproved(tokenId) == address(this),
"Not approved"
);

bytes32 auctionId = keccak256(
abi.encodePacked(nftContract, tokenId, msg.sender, block.timestamp)
);

auctions[auctionId] = Auction({
seller: msg.sender,
nftContract: nftContract,
tokenId: tokenId,
startingPrice: startingPrice,
duration: duration,
endTime: block.timestamp + duration,
highestBidder: address(0),
highestBid: 0,
active: true
});

nft.transferFrom(msg.sender, address(this), tokenId);

emit AuctionCreated(
auctionId,
msg.sender,
nftContract,
tokenId,
startingPrice,
duration
);

return auctionId;
}

// 出价
function placeBid(bytes32 auctionId) external payable nonReentrant {
Auction storage auction = auctions[auctionId];

require(auction.active, "Not active");
require(block.timestamp < auction.endTime, "Auction ended");
require(msg.value > auction.highestBid, "Bid too low");
require(msg.value >= auction.startingPrice, "Below starting price");

if (auction.highestBidder != address(0)) {
pendingWithdrawals[auction.highestBidder] += auction.highestBid;
}

auction.highestBidder = msg.sender;
auction.highestBid = msg.value;

emit BidPlaced(auctionId, msg.sender, msg.value);
}

// 结束拍卖
function endAuction(bytes32 auctionId) external nonReentrant {
Auction storage auction = auctions[auctionId];

require(auction.active, "Not active");
require(block.timestamp >= auction.endTime, "Auction not ended");

auction.active = false;

if (auction.highestBidder != address(0)) {
uint256 fee = (auction.highestBid * feePercent) / 10000;
uint256 sellerProceeds = auction.highestBid - fee;

pendingWithdrawals[auction.seller] += sellerProceeds;
pendingWithdrawals[feeRecipient] += fee;

IERC721(auction.nftContract).transferFrom(
address(this),
auction.highestBidder,
auction.tokenId
);
} else {
IERC721(auction.nftContract).transferFrom(
address(this),
auction.seller,
auction.tokenId
);
}

emit AuctionEnded(auctionId, auction.highestBidder, auction.highestBid);
}

// 取消拍卖
function cancelAuction(bytes32 auctionId) external nonReentrant {
Auction storage auction = auctions[auctionId];

require(auction.active, "Not active");
require(auction.seller == msg.sender, "Not seller");
require(auction.highestBidder == address(0), "Has bids");

auction.active = false;

IERC721(auction.nftContract).transferFrom(
address(this),
msg.sender,
auction.tokenId
);
}

// 提取资金
function withdraw() external nonReentrant {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "Nothing to withdraw");

pendingWithdrawals[msg.sender] = 0;

(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Transfer failed");
}

// 设置费用
function setFeePercent(uint256 _feePercent) external onlyOwner {
require(_feePercent <= 1000, "Fee too high"); // Max 10%
feePercent = _feePercent;
}

// 设置费用接收者
function setFeeRecipient(address _feeRecipient) external onlyOwner {
feeRecipient = _feeRecipient;
}
}

NFT 版税

EIP-2981 定义了 NFT 版税标准:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/interfaces/IERC2981.sol";

contract RoyaltyNFT is ERC721, IERC2981, Ownable {
uint256 private _tokenIdCounter;

address public royaltyRecipient;
uint256 public royaltyPercent; // 基点,10000 = 100%

constructor(
string memory _name,
string memory _symbol,
address _royaltyRecipient,
uint256 _royaltyPercent
) ERC721(_name, _symbol) Ownable(msg.sender) {
royaltyRecipient = _royaltyRecipient;
royaltyPercent = _royaltyPercent;
}

function mint() external {
uint256 tokenId = _tokenIdCounter++;
_safeMint(msg.sender, tokenId);
}

// EIP-2981 版税接口
function royaltyInfo(
uint256 _tokenId,
uint256 _salePrice
) external view override returns (address receiver, uint256 royaltyAmount) {
require(_exists(_tokenId), "Token does not exist");

receiver = royaltyRecipient;
royaltyAmount = (_salePrice * royaltyPercent) / 10000;
}

// 设置版税
function setRoyalty(
address _recipient,
uint256 _percent
) external onlyOwner {
require(_percent <= 1000, "Royalty too high"); // Max 10%
royaltyRecipient = _recipient;
royaltyPercent = _percent;
}

function supportsInterface(
bytes4 interfaceId
) public view override(ERC721, IERC165) returns (bool) {
return
interfaceId == type(IERC2981).interfaceId ||
super.supportsInterface(interfaceId);
}
}

灵魂绑定代币(SBT)

灵魂绑定代币是不可转移的 NFT,用于表示凭证、证书等:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract SoulBoundToken is ERC721, Ownable {
uint256 private _tokenIdCounter;

struct Credential {
string title;
string issuer;
uint256 issueDate;
string metadata;
}

mapping(uint256 => Credential) public credentials;

constructor() ERC721("SoulBound Credential", "SBC") Ownable(msg.sender) {}

// 颁发凭证
function issueCredential(
address to,
string memory title,
string memory issuer,
string memory metadata
) external onlyOwner returns (uint256) {
uint256 tokenId = _tokenIdCounter++;

credentials[tokenId] = Credential({
title: title,
issuer: issuer,
issueDate: block.timestamp,
metadata: metadata
});

_safeMint(to, tokenId);

return tokenId;
}

// 撤销凭证
function revokeCredential(uint256 tokenId) external onlyOwner {
require(_exists(tokenId), "Token does not exist");
_burn(tokenId);
delete credentials[tokenId];
}

// 禁止转移
function transferFrom(
address,
address,
uint256
) public pure override {
revert("SBT: token is non-transferable");
}

function safeTransferFrom(
address,
address,
uint256
) public pure override {
revert("SBT: token is non-transferable");
}

function safeTransferFrom(
address,
address,
uint256,
bytes memory
) public pure override {
revert("SBT: token is non-transferable");
}

function approve(address, uint256) public pure override {
revert("SBT: token is non-transferable");
}

function setApprovalForAll(address, bool) public pure override {
revert("SBT: token is non-transferable");
}

// 获取凭证信息
function getCredential(uint256 tokenId) external view returns (
string memory title,
string memory issuer,
uint256 issueDate,
string memory metadata
) {
require(_exists(tokenId), "Token does not exist");
Credential storage cred = credentials[tokenId];
return (cred.title, cred.issuer, cred.issueDate, cred.metadata);
}
}

NFT 存储方案

IPFS 存储

NFT 元数据和图片通常存储在 IPFS 上:

const { create } = require('ipfs-http-client');
const fs = require('fs');

async function uploadToIPFS(imagePath, metadata) {
const ipfs = create({ url: 'https://ipfs.infura.io:5001/api/v0' });

// 上传图片
const imageFile = fs.readFileSync(imagePath);
const imageResult = await ipfs.add(imageFile);
const imageURI = `ipfs://${imageResult.path}`;

// 创建元数据
const fullMetadata = {
name: metadata.name,
description: metadata.description,
image: imageURI,
attributes: metadata.attributes
};

// 上传元数据
const metadataResult = await ipfs.add(JSON.stringify(fullMetadata));
const metadataURI = `ipfs://${metadataResult.path}`;

return metadataURI;
}

链上存储

对于小型数据,可以直接存储在链上:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract OnChainNFT is ERC721, Ownable {
uint256 private _tokenIdCounter;

mapping(uint256 => string) private _svgData;

constructor() ERC721("On Chain NFT", "OCN") Ownable(msg.sender) {}

function mint(string memory svgData) external onlyOwner {
uint256 tokenId = _tokenIdCounter++;
_svgData[tokenId] = svgData;
_safeMint(msg.sender, tokenId);
}

function tokenURI(uint256 tokenId) public view override returns (string memory) {
require(_exists(tokenId), "Token does not exist");

string memory svg = _svgData[tokenId];
string memory json = string(abi.encodePacked(
'{"name":"On Chain NFT #',
Strings.toString(tokenId),
'","description":"Fully on-chain NFT","image":"data:image/svg+xml;base64,',
Base64.encode(bytes(svg)),
'"}'
));

return string(abi.encodePacked(
"data:application/json;base64,",
Base64.encode(bytes(json))
));
}
}

library Base64 {
string internal constant TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

function encode(bytes memory data) internal pure returns (string memory) {
if (data.length == 0) return "";

string memory table = TABLE;
uint256 encodedLen = 4 * ((data.length + 2) / 3);

string memory result = new string(encodedLen + 32);

assembly {
mstore(result, encodedLen)

let tablePtr := add(table, 1)
let dataPtr := add(data, 32)
let endPtr := add(dataPtr, mload(data))
let resultPtr := add(result, 32)

for {} lt(dataPtr, endPtr) {} {
dataPtr := add(dataPtr, 3)
let input := mload(dataPtr)

mstore(resultPtr, shl(248, mload(add(tablePtr, and(shr(18, input), 0x3F)))))
resultPtr := add(resultPtr, 1)
mstore(resultPtr, shl(248, mload(add(tablePtr, and(shr(12, input), 0x3F)))))
resultPtr := add(resultPtr, 1)
mstore(resultPtr, shl(248, mload(add(tablePtr, and(shr(6, input), 0x3F)))))
resultPtr := add(resultPtr, 1)
mstore(resultPtr, shl(248, mload(add(tablePtr, and(input, 0x3F)))))
resultPtr := add(resultPtr, 1)
}

switch mod(mload(data), 3)
case 1 {
mstore(sub(resultPtr, 2), shl(248, 0x3d3d))
}
case 2 {
mstore(sub(resultPtr, 1), shl(248, 0x3d))
}
}

return result;
}
}

小结

本章介绍了 NFT 的核心概念和开发实践,包括 ERC-721、ERC-1155 标准,NFT 市场、版税和灵魂绑定代币。NFT 为数字资产提供了确权和交易的基础设施,应用场景不断扩展。开发 NFT 项目时,需要注意元数据存储、Gas 优化和安全性。下一章我们将提供 Web3 开发的速查表。

参考资料