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 开发的速查表。