NFT 标准与数字资产:ERC-721、ERC-1155、ERC-6551 与 Soulbound Token

全面梳理 NFT 三大核心标准 ERC-721、ERC-1155 与 ERC-6551 Token Bound Account 的技术实现,深入探讨 Soulbound Token 的不可转让设计与应用场景,并覆盖 OpenSea 元数据规范、EIP-2981 版税机制与 gas 优化策略。

导语:从 CryptoKitties 到数字资产的范式革命

2017 年,CryptoKitties 引发以太坊网络拥堵,让人们第一次意识到"非同质化代币"(Non-Fungible Token,NFT)的巨大潜力。NFT 的本质是区块链上独一无二的数字资产凭证,它用密码学保证稀缺性与所有权可追溯性。

从技术视角看,NFT 的演进是一部 ERC 标准迭代史:ERC-721 定义了独一无二的基础框架,ERC-1155 引入了半同质化的批量管理能力,ERC-6551 赋予了 NFT 以智能合约账户的身份,而 Soulbound Token(SBT)则探索了不可转让资产的新形式。理解这些标准,是构建 Web3 数字资产生态的基石。

一句话总结:NFT 标准的演进轨迹是"独一无二 → 批量管理 → 账户绑定 → 身份凭证",每一次跃迁都在扩展区块链数字资产的可能性边界。


1. ERC-721:独一无二数字资产的基石

1.1 核心接口设计

ERC-721 是最基础的 NFT 标准,它定义了每个 token 都是独一无二的,不能互相替代。

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

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

contract MyERC721 is ERC721, Ownable {
    uint256 private _tokenIdCounter;
    string private _baseTokenURI;

    constructor(string memory name, string memory symbol, string memory baseURI)
        ERC721(name, symbol)
        Ownable(msg.sender)
    {
        _baseTokenURI = baseURI;
    }

    // 铸造:每个调用生成一个唯一 tokenId,映射到接收地址
    function mint(address to) public onlyOwner returns (uint256) {
        uint256 tokenId = _tokenIdCounter;
        _tokenIdCounter++;
        _safeMint(to, tokenId);
        return tokenId;
    }

    // 覆盖基类的 _baseURI,OpenSea 等市场会调用 tokenURI(tokenId)
    function _baseURI() internal view override returns (string memory) {
        return _baseTokenURI;
    }
}

合约继承 OpenZeppelin 的 ERC721 实现,只需覆盖 mint 和 _baseURI。tokenURI(tokenId) 会返回 JSON 元数据的 URL,前端或市场据此渲染 NFT 图像与属性。

1.2 状态与事件模型

// ERC-721 核心状态映射(OpenZeppelin 内部实现)
mapping(uint256 => address) private _owners;      // tokenId -> 所有者
mapping(address => uint256) private _balances;    // 地址持有的 NFT 数量
mapping(uint256 => address) private _tokenApprovals; // tokenId -> 授权地址

// 关键事件
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

每次 Transfer 都会在链上永久记录所有权变更,这是 NFT provenance(溯源)的基础。

一句话总结:ERC-721 用 tokenId → owner 的全局映射实现了数字资产的唯一性与可转让性,是 OpenSea、Blur 等市场底层的基础数据结构。


2. ERC-1155:半同质化与批量管理

2.1 为什么要 ERC-1155

ERC-721 中,一次转一个 NFT 要消耗 21,000 gas(转账)+ 约 50,000 gas(状态更新)。游戏项目中一次空投 1 万件 NFT gas 成本高昂。ERC-1155 的核心创新是:一个合约同时管理多种 token(同质化的和非同质化的),并支持一次交易批量操作。

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

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

contract MyERC1155 is ERC1155, Ownable {
    // tokenId 0 = 游戏金币(同质化,总量大)
    // tokenId 1..N = 装备/皮肤(非同质化或限量)
    uint256 public constant GOLD = 0;
    uint256 public constant SWORD = 1;
    uint256 public constant SHIELD = 2;

    constructor(string memory uri) ERC1155(uri) Ownable(msg.sender) {}

    // 创世铸造:一次性给接收者分配多种 token
    function mintBatch(
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) public onlyOwner {
        _mintBatch(to, ids, amounts, data);
    }

    // 单类型铸造
    function mint(address to, uint256 id, uint256 amount) public onlyOwner {
        _mint(to, id, amount, "");
    }
}

2.2 批量转账的 gas 优化

// ERC-1155 核心:批量操作只需一次 nonce 和安全检查
function safeBatchTransferFrom(
    address from,
    address to,
    uint256[] calldata ids,
    uint256[] calldata amounts,
    bytes calldata data
) external {
    // 一次性验证 from 的授权
    // 一次性更新所有余额
    // 一次性触发 TransferBatch 事件
}

相比于 ERC-721 每次 transfer 都要独立重入锁检查和事件 emit,ERC-1155 的 safeBatchTransferFrom 显著降低了单位 token 的 gas 开销。一次空投 100 个 NFT,ERC-721 可能需要数百万 gas,ERC-1155 只需十几万。

2.3 ERC-1155 的单合约多资产模型

tokenId类型特性
0游戏金币同质化,无上限铸造
1传说之剑NFT(amount = 1),唯一
2新手礼包券半同质化(amount > 1 但有限)

一句话总结:ERC-1155 用"单合约 + 多 tokenId + 批量操作"的设计,把 ERC-721 与 ERC-20 的能力统一到同一个合约里,是 GameFi 和元宇宙项目的首选标准。


3. ERC-6551:Token Bound Account,让 NFT 变成钱包

3.1 ERC-6551 的设计动机

普通 NFT 只是"被动的"资产凭证,无法自主持有其他 token 或与智能合约交互。ERC-6551 引入了Token Bound Account(TBA)——每个 ERC-721 token 都可以绑定一个智能合约账户,这个账户有地址、可以持有 ETH/ERC-20/ERC-721、可以签名交易。

// ERC-6551 Registry:为每个 token 创建对应的 TBA 合约实例
interface IERC6551Registry {
    function createAccount(
        address implementation,  // TBA 实现合约地址
        bytes32 salt,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId
    ) external returns (address account);

    function account(
        address implementation,
        bytes32 salt,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId
    ) external view returns (address account);
}

3.2 Token Bound Account 的实现

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

import "erc6551/interfaces/IERC6551Account.sol";
import "erc6551/interfaces/IERC6551Executable.sol";

contract TokenBoundAccount is IERC6551Account, IERC6551Executable {
    uint256 public state;  // nonce,每次执行后递增

    receive() external payable {}

    // 执行任意调用:只有 NFT 的所有者才能调用
    function execute(
        address to,
        uint256 value,
        bytes calldata data,
        uint256 operation   // 0 = CALL, 1 = DELEGATECALL
    ) external payable returns (bytes memory result) {
        require(_isValidSigner(msg.sender), "Not authorized");
        state++;
        (bool success, bytes memory ret) = to.call{value: value}(data);
        require(success, "Execution failed");
        return ret;
    }

    // 谁拥有绑定的 NFT,谁就拥有这个账户
    function _isValidSigner(address signer) internal view returns (bool) {
        (uint256 chainId, address tokenContract, uint256 tokenId) = token();
        if (chainId != block.chainid) return false;
        return IERC721(tokenContract).ownerOf(tokenId) == signer;
    }

    function token() public view returns (uint256, address, uint256) {
        bytes memory footer = new bytes(0x60);
        assembly {
            extcodecopy(address(), add(footer, 0x20), 0x4d, 0x60)
        }
        return abi.decode(footer, (uint256, address, uint256));
    }
}

TBA 的核心原理是:Registry 为 (chainId, tokenContract, tokenId) 的三元组创建一个确定性的合约地址(类似 CREATE2)。因此,任何人都可以预先计算出某个 NFT 的 TBA 地址并向其转账。当 NFT 在二级市场转售后,TBA 的控制权也随之转移——资产跟着 NFT 走。

一句话总结:ERC-6551 把 NFT 从"静态凭证"升级为"动态账户",让角色、装备、成就等全部资产可以内聚绑定到一个 token 上随其流转——这是链上身份系统的关键拼图。


4. Soulbound Token:不可转让的身份凭证

4.1 SBT 的设计理念

Soulbound Token(灵魂绑定代币)由 Vitalik Buterin 等人在论文《Decentralized Society: Finding Web3’s Soul》中提出。核心思想是:某些 token 应当与持有者永久绑定,不可转让、不可出售,例如学位证书、DAO 成员身份、出席证明(POAP)。

4.2 SBT 的 Solidity 实现

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

    // 记录 tokenId -> 是否可恢复(issuer 可收回的特殊场景)
    mapping(uint256 => bool) public recoverable;
    mapping(uint256 => string) public tokenURIs;

    constructor(string memory name, string memory symbol)
        ERC721(name, symbol)
        Ownable(msg.sender)
    {}

    // 铸造:由发行方直接绑定到目标地址
    function mint(address to, string memory uri) external onlyOwner {
        uint256 tokenId = _tokenIdCounter;
        _tokenIdCounter++;
        _safeMint(to, tokenId);
        tokenURIs[tokenId] = uri;
    }

    // 关键:覆盖 transfer 相关函数,禁止所有转移
    function _update(address to, uint256 tokenId, address auth)
        internal
        override
        returns (address)
    {
        address from = _ownerOf(tokenId);
        // 允许铸造(from == address(0))和销毁(to == address(0))
        // 禁止所有 from != 0 且 to != 0 的转移
        require(
            from == address(0) || to == address(0),
            "SoulboundToken: token is non-transferable"
        );
        return super._update(to, tokenId, auth);
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override
        returns (string memory)
    {
        return tokenURIs[tokenId];
    }
}

通过覆盖 _update 函数(OpenZeppelin v5 的 hook),SBT 在 ERC-721 的基础上实现了"铸造允许、销毁允许、转移禁止"的语义。

4.3 SBT 的应用场景

应用场景:
  - 学位证书:大学 mint SBT 到毕业生地址,证明其获得了某学位
  - DAO 成员身份:社区治理的成员凭证,不可出售避免"买票"
  - 出席证明(POAP):线下活动参与的链上记录
  - 信用评分:链上借贷历史的凭证,无法通过购买获得高分
  - 游戏成就:如「击败最终 Boss」勋章,绑定到玩家身份

一句话总结:SBT 用"不可转让"的约束重新定义了链上凭证的价值逻辑——它的稀缺性不再来自于可以交易,而来自于它代表的真实经历和身份。


5. OpenSea 元数据标准与 EIP-2981 版税

5.1 元数据 JSON 格式

NFT 合约的 tokenURI 返回一个指向 JSON 的 URL,OpenSea 等市场按如下格式解析:

{
  "name": "Legendary Sword #1",
  "description": "A blade forged in the fires of Mt. Ethereum.",
  "image": "https://example.com/images/1.png",
  "attributes": [
    { "trait_type": "Rarity", "value": "Legendary" },
    { "trait_type": "Attack", "display_type": "number", "value": 95 },
    { "trait_type": "Element", "value": "Fire" }
  ],
  "animation_url": "https://example.com/animations/1.glb"
}
  • image:主图像,支持 PNG/JPG/SVG,IPFS 链接更佳
  • attributes:特征属性,用于市场筛选和稀有度计算
  • animation_url:支持 3D 模型、视频等富媒体
  • 将元数据存储在 IPFS 上可保证不可篡改

5.2 EIP-2981:NFT 版税标准

interface IERC2981 is IERC165 {
    // 返回版税接收地址和应付金额
    function royaltyInfo(uint256 tokenId, uint256 salePrice)
        external
        view
        returns (address receiver, uint256 royaltyAmount);
}
contract MyERC721WithRoyalty is ERC721, IERC2981 {
    address public royaltyReceiver;
    uint96 public royaltyFraction; // 以 basis point 为单位,10000 = 100%

    constructor(address receiver, uint96 fraction) {
        royaltyReceiver = receiver;
        royaltyFraction = fraction;
    }

    function royaltyInfo(uint256, uint256 salePrice)
        external
        view
        override
        returns (address receiver, uint256 royaltyAmount)
    {
        receiver = royaltyReceiver;
        royaltyAmount = (salePrice * royaltyFraction) / 10000;
    }

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

EIP-2981 是链上版税的行业标准。市场调用 royaltyInfo 获取二级销售时应支付的版税金额和接收地址。但需注意,链上无法强制市场执行版税——OpenSea 等平台可选择性尊重或不尊重,这是 NFT 版税争议的根源。

一句话总结:OpenSea 元数据定义了"数字藏品长什么样",EIP-2981 定义了"二次销售谁分钱"——但链上标准只是建议,费用执行的最终决定权在交易市场。


6. 铸造与二级转售机制

6.1 铸造模式的演进

// 模式一:Owner 自主铸造(中心化控制)
function mint(address to) external onlyOwner;

// 模式二:付费公开铸造(Free mint / Paid mint)
function publicMint(uint256 quantity) external payable {
    require(msg.value >= MINT_PRICE * quantity, "Insufficient ETH");
    require(totalSupply() + quantity <= MAX_SUPPLY, "Exceeds supply");
    for (uint256 i = 0; i < quantity; i++) {
        _safeMint(msg.sender, _tokenIdCounter++);
    }
}

// 模式三:白名单 + Merkle Tree 铸造(防 bot)
function whitelistMint(
    uint256 quantity,
    bytes32[] calldata merkleProof
) external payable {
    bytes32 leaf = keccak256(abi.encodePacked(msg.sender, quantity));
    require(
        MerkleProof.verify(merkleProof, merkleRoot, leaf),
        "Invalid proof"
    );
    // ... mint
}

6.2 二级转售与地板价机制

NFT 的流动性主要来自二级市场(OpenSea、Blur、LooksRare)。二级市场合约通常基于以下模式:

// Seaport 风格的订单匹配(OpenSea 协议核心逻辑示意)
struct Order {
    address offerer;          // 卖家
    address token;            // NFT 合约
    uint256 tokenId;          // NFT ID
    uint256 price;            // 售价(ETH 或 ERC-20)
    bytes32 salt;             // 防重放
}

// 卖家签名订单,买家调用 fulfill 完成交易
function fulfillOrder(Order calldata order, bytes calldata signature) external {
    require(_verifySignature(order, signature), "Invalid signature");
    // 转移 NFT 给买家
    // 转移 ETH/WETH 给卖家
    // 扣除平台手续费和版税
}

一句话总结:铸造从"中心化控制"演进到"白名单 + Merkle Tree"的公平分发,二级转售靠订单签名匹配实现——理解这些模式,是每一个 NFT 项目方必须掌握的经济模型基础。


7. Gas 优化技巧

7.1 批量操作的 gas 节省

// 优化前:多次独立 mint,多次存储写入
for (uint i = 0; i < 10; i++) { _safeMint(user, id++); }

// 优化后:ERC-1155 一次性批量 mint,单次存储更新
uint256[] memory ids = new uint256[](10);
uint256[] memory amounts = new uint256[](10);
for (uint i = 0; i < 10; i++) { ids[i] = id++; amounts[i] = 1; }
_mintBatch(user, ids, amounts, "");

7.2 其他常用优化

技巧说明节省效果
ERC-721A(Azuki)批量铸造时复用存储槽铸造 5 个节省约 50% gas
去掉 ERC-165 supportsInterface如果确定只在特定市场使用部署 gas 减少
IPFS 替代链上元数据存储成本更低部署/更新成本大幅降低
unchecked 递增OpenZeppelin v5 已集成少量节省

一句话总结:ERC-721A 的批量铸造优化、ERC-1155 的多 token 合并、以及元数据上链 → IPFS 的迁移,是 NFT 项目 gas 优化的三板斧。


相关阅读


延伸阅读


以下是一个完整的混合 NFT 合约实现,结合了 ERC-1155 批量管理、ERC-6551 注册支持以及 EIP-2981 版税,可直接部署测试。

// 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/token/common/ERC2981.sol";

// 一个同时支持批量管理、版税和 TBA 兼容的 NFT 合约
contract HybridNFT is ERC1155, ERC2981, Ownable {
    uint256 public constant COLLECTION_SIZE = 10000;
    uint256 private _nextTokenId;
    uint256 public mintPrice = 0.01 ether;

    mapping(uint256 => string) private _tokenURIs;

    constructor(string memory uri) ERC1155(uri) Ownable(msg.sender) {
        // 设置默认版税:2.5%,接收者为合约部署者
        _setDefaultRoyalty(msg.sender, 250);
    }

    // 公开铸造(支持多数量)
    function publicMint(uint256 quantity) external payable {
        require(msg.value >= mintPrice * quantity, "Insufficient ETH");
        require(_nextTokenId + quantity <= COLLECTION_SIZE, "Exceeds supply");

        uint256 startId = _nextTokenId;
        uint256[] memory ids = new uint256[](quantity);
        uint256[] memory amounts = new uint256[](quantity);

        for (uint256 i = 0; i < quantity; i++) {
            ids[i] = startId + i;
            amounts[i] = 1;
            _tokenURIs[startId + i] = string(
                abi.encodePacked(uri(0), uint256ToString(startId + i), ".json")
            );
        }

        _nextTokenId += quantity;
        _mintBatch(msg.sender, ids, amounts, "");
    }

    // 覆盖 uri 以返回每个 token 的独立元数据
    function uri(uint256 tokenId)
        public
        view
        override
        returns (string memory)
    {
        return _tokenURIs[tokenId];
    }

    // 设置版税
    function setDefaultRoyalty(address receiver, uint96 feeNumerator)
        external
        onlyOwner
    {
        _setDefaultRoyalty(receiver, feeNumerator);
    }

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

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC1155, ERC2981)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }

    // 辅助:uint256 转字符串
    function uint256ToString(uint256 value)
        internal
        pure
        returns (string memory)
    {
        if (value == 0) return "0";
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) { digits++; temp /= 10; }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            digits--;
            buffer[digits] = bytes1(uint8(48 + (value % 10)));
            value /= 10;
        }
        return string(buffer);
    }
}

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「blockchain」更多文章

  1. DAO 与链上治理:Snapshot、Tally、治理代币与投票机制
  2. 智能合约部署与升级:代理模式、EIP-1967 与 CREATE2
  3. Layer1 公链内部:P2P 网络、交易池与状态同步