30秒搞懂ERC-2981:NFT版税的终极解决方案!

前言

本文围绕 ERC-2981 版税标准展开,先系统梳理其核心定义、功能、解决的行业痛点及典型使用场景,再基于 OpenZeppelin 库整合 ERC-721 与 ERC-2981 标准实现版税 NFT 智能合约,最后通过 Hardhat V3 完成合约的开发、测试、部署全流程落地。

概述

ERC-2981 是以太坊 NFT 的链上版税标准,为 ERC-721/ERC-1155 合约提供统一的版税查询接口,让市场能自动获取分成规则并向创作者支付二级市场收益,核心解决早期版税碎片化、不可靠与跨平台不兼容问题,广泛用于数字艺术、游戏道具等需持续收益的 NFT 场景

ERC-2981 是什么

  • 定义 :以太坊改进提案 EIP-2981(又称 ERC-2981),是 NFT 领域的标准化版税查询接口标准,兼容 ERC-721 与 ERC-1155,通过 EIP-165 接口识别,不强制市场执行版税,而是提供统一的链上版税信息查询能力。

  • 核心接口(IERC2981)

    1. royaltyInfo(uint256 tokenId, uint256 salePrice):返回版税接收地址与应付金额(以基点计算,1 基点 = 0.01%)。
    2. 可选实现:_setTokenRoyalty(单 token 版税)、_setDefaultRoyalty(全局默认版税),用于设置版税规则。
  • 关键特性:链上透明、可组合、兼容主流 NFT 标准,不依赖平台规则,由合约自主定义版税策略。

ERC-2981 能做什么

  1. 标准化版税信息存储与查询:在 NFT 合约中嵌入版税规则(比例、接收地址),市场通过统一接口读取,无需自定义解析逻辑。
  2. 自动分成触发 :二级市场交易时,合规市场调用royaltyInfo计算分成,自动将版税划转至创作者 / 权利人,剩余款项给卖家。
  3. 灵活规则配置:支持单 token 独立版税、全局默认版税,可通过权限控制更新接收地址,适配动态分成场景。
  4. 跨平台互通:统一接口让 NFT 在不同市场交易时,版税规则一致,提升生态可组合性。
  5. 链上追溯:版税信息与交易记录上链,便于审计与纠纷排查,降低信任成本。

ERC-2981 解决了什么

痛点 解决方案
版税碎片化 统一接口替代各平台专有规则,开发者无需重复适配
收益不可靠 链上存储规则,减少依赖平台中心化结算的信用风险
跨平台不兼容 标准接口让市场无缝读取版税,保障创作者跨平台收益
信息不透明 公开可查询的版税比例与接收地址,避免暗箱操作
开发成本高 复用 OpenZeppelin 等库的实现,降低版税功能开发门槛

使用场景

  1. 数字艺术 / 收藏品:艺术家铸造 NFT 时设置 5%-10% 版税,每次转售自动分成,如 CryptoPunks(兼容后)、Art Blocks 项目。
  2. 游戏资产:游戏道具 NFT 转售时,开发者按比例获取分成,用于持续开发与运营,适配 ERC-1155 批量资产场景。
  3. 音乐 / 影视 NFT:版权方在二次交易中获得收益,支持多权利人按比例分配(需结合多签 / 分账合约)。
  4. IP 衍生品:IP 方通过版税获取长期收益,如品牌联名 NFT 的持续分成。
  5. 创作者 DAO / 社区:版税收入进入 DAO 金库,用于生态建设或社区分红,提升治理效率。
  6. 跨链 NFT:通过跨链桥同步版税信息,实现多链交易时的自动分成(需跨链协议支持)。

智能合约开发、测试、部署

版税NFT智能合约

typescript 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Strings.sol"; // 新增:用于tokenURI

contract MyRoyaltyNFT is ERC721, ERC2981, Ownable {
    // 使用 uint256 替代 Counters.Counter(5.x版本已移除该库)
    uint256 private _nextTokenId;
    
    string private _baseTokenURI;
    uint96 private constant MAX_ROYALTY_BPS = 1000; // 10% 版税上限

    // 新增:可选的最大供应量限制(设为0则无限制)
    uint256 public immutable maxSupply;

    // 新增:合约部署事件
    event Minted(address indexed to, uint256 indexed tokenId);

    constructor(
        string memory name,
        string memory symbol,
        string memory baseURI,
        address royaltyReceiver,
        uint96 royaltyBps,
        uint256 _maxSupply // 新增参数,设为0表示无上限
    ) ERC721(name, symbol) Ownable(msg.sender) {
        _baseTokenURI = baseURI;
        maxSupply = _maxSupply;
        
        // 设置默认版税(basis points: 100 = 1%)
        require(royaltyBps <= MAX_ROYALTY_BPS, "Royalty too high");
        _setDefaultRoyalty(royaltyReceiver, royaltyBps);
    }

    // ======================== 铸造功能 ========================
    
    // 优化:原生递增 + 可选供应上限
    function safeMint(address to) public onlyOwner {
        require(
            maxSupply == 0 || _nextTokenId < maxSupply, 
            "Max supply reached"
        );
        
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        emit Minted(to, tokenId); // 记录铸造事件
    }

    // 批量铸造(新增:高效铸造多个)
    function safeMintBatch(address[] calldata recipients) external onlyOwner {
        for (uint256 i = 0; i < recipients.length; i++) {
            safeMint(recipients[i]);
        }
    }

    // 获取已铸造总量
    function totalSupply() external view returns (uint256) {
        return _nextTokenId;
    }

    // ======================== 版税管理 ========================
    
    // 优化:统一版税验证逻辑
    function setTokenRoyalty(
        uint256 tokenId,
        address receiver,
        uint96 feeNumerator
    ) external onlyOwner {
        _validateRoyalty(feeNumerator);
        _setTokenRoyalty(tokenId, receiver, feeNumerator);
    }

    function setDefaultRoyalty(
        address receiver, 
        uint96 feeNumerator
    ) external onlyOwner {
        _validateRoyalty(feeNumerator);
        _setDefaultRoyalty(receiver, feeNumerator);
    }

    function resetTokenRoyalty(uint256 tokenId) external onlyOwner {
        _resetTokenRoyalty(tokenId);
    }

    // 内部函数:验证版税比例
    function _validateRoyalty(uint96 feeNumerator) internal pure {
        require(feeNumerator <= MAX_ROYALTY_BPS, "Royalty exceeds 10%");
    }

    // ======================== 元数据 ========================
    
    // 优化:自动拼接tokenId
    function tokenURI(uint256 tokenId) 
        public 
        view 
        virtual 
        override 
        returns (string memory) 
    {
        _requireOwned(tokenId); // 5.x推荐:替代require(_exists())
        
        string memory baseURI = _baseURI();
        return bytes(baseURI).length > 0 
            ? string.concat(baseURI, Strings.toString(tokenId), ".json") // 自动添加.json扩展名
            : "";
    }

    function _baseURI() internal view virtual override returns (string memory) {
        return _baseTokenURI;
    }

    function setBaseURI(string memory newBaseURI) external onlyOwner {
        _baseTokenURI = newBaseURI;
    }

    // ======================== 接口支持 ========================
    
    // 必须重写 supportsInterface 以支持 ERC165 接口检测
    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override(ERC721, ERC2981)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

编译指令

python 复制代码
npx hardhat compile

智能合约部署脚本

javascript 复制代码
// scripts/deploy.js
import { network, artifacts } from "hardhat";
async function main() {
  // 连接网络
  const { viem } = await network.connect({ network: network.name });//指定网络进行链接
  
  // 获取客户端
  const [deployer] = await viem.getWalletClients();
  const publicClient = await viem.getPublicClient();
 
  const deployerAddress = deployer.account.address;
   console.log("部署者的地址:", deployerAddress);
  // 加载合约
  const artifact = await artifacts.readArtifact("MyRoyaltyNFT");
  const ipfsjsonuri="https://zygomorphic-magenta-bobolink.myfilebase.com/ipfs/QmQT8VpmWQVhUhoDCEK1mdHXaFaJ3KawkRxHm96GUhrXLB"
  // 部署(构造函数参数:recipient, initialOwner)
  const hash = await deployer.deployContract({
    abi: artifact.abi,//获取abi
    bytecode: artifact.bytecode,//硬编码
    args: ["MyRoyaltyNFT","MRNFT",ipfsjsonuri,deployerAddress,100,0],//nft名称,nft符号,ipfsjsonuri,部署者地址, royaltiesNumerator,royaltiesDenominator
  });

  // 等待确认并打印地址
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  console.log("合约地址:", receipt.contractAddress);
}

main().catch(console.error);

部署指令

arduino 复制代码
npx hardhat run ./scripts/xxx.ts

智能合约测试脚本

php 复制代码
import assert from "node:assert/strict";
import { describe, it,beforeEach  } from "node:test";
import { formatEther,parseEther } from 'viem'
import { network } from "hardhat";
describe("MyRoyaltyNFT", async function () {
    let viem: any;
    let publicClient: any;
    let owner: any, user1: any, user2: any, user3: any;
    let deployerAddress: string;
    let MyRoyaltyNFT: any;
    beforeEach (async function () {
        const { viem } = await network.connect();
         publicClient = await viem.getPublicClient();//创建一个公共客户端实例用于读取链上数据(无需私钥签名)。
         [owner,user1,user2,user3] = await viem.getWalletClients();//获取第一个钱包客户端 写入联合交易
        deployerAddress = owner.account.address;//钱包地址
       const ipfsjsonuri="https://zygomorphic-magenta-bobolink.myfilebase.com/ipfs/QmQT8VpmWQVhUhoDCEK1mdHXaFaJ3KawkRxHm96GUhrXLB";
       
        MyRoyaltyNFT = await viem.deployContract("MyRoyaltyNFT", [
            "My Royalty NFT",
            "MRNFT",
            ipfsjsonuri,
            deployerAddress,
            200,//版税1%
            0,
        ]);//部署合约
        console.log("MyRoyaltyNFT合约地址:", MyRoyaltyNFT.address); 
    });
    it("测试MyRoyaltyNFT", async function () {
        //查询nft名称和符号
       const name= await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "name",
            args: [],
        });
       const symbol= await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "symbol",
            args: [],
        });
        //查询总供应量和最大供应量
        const totalSupply= await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "totalSupply",
            args: [],
        });
        const maxSupply= await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "maxSupply",
            args: [],
        });
        //查询合约拥有者
       const ownerAddress= await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "owner",
            args: [],
        });
        console.log(name,symbol,totalSupply,maxSupply,ownerAddress)
        //铸造单个nft
        await owner.writeContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "safeMint",
            args: [user1.account.address],
        });
        //批量铸造nft
        const nftaddress=[user1.account.address,user2.account.address,user3.account.address]
        await owner.writeContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "safeMintBatch",
            args: [nftaddress],
        });

        //查询单个nft的tokenURI
        const TokenURI= await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "tokenURI",
            args: [0],
        });
        console.log(TokenURI)
        //查询余额和拥有者
        const balanceOf=await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "balanceOf",
            args: [user1.account.address],
        });
        console.log(balanceOf)
        //查询nft的拥有者
        const ownerOf=await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "ownerOf",
            args: [0],
        });
        console.log(ownerOf)
        //查询版税信息
        const royaltyInfo=await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "royaltyInfo",
            args: [0,parseEther("2")],
        });
        console.log(royaltyInfo)
        const GETAPPROVED=await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "getApproved",
            args: [0],
        });
        console.log(GETAPPROVED)
        //设置BaseURI
        const ipfsjsonuri1="https://zygomorphic-magenta-bobolink.myfilebase.com/ipfs/QmcN49MKt4MbSXSGckAcpvFqtea43uuPD2tvmuER1mG67s";
       const setBaseURI=await owner.writeContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "setBaseURI",
            args: [ipfsjsonuri1],
        });
        console.log(setBaseURI)
        //查询更新后的tokenURI
        const TokenURI1= await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "tokenURI",
            args: [0],
        });
        console.log("更新后",TokenURI1)
        //设置默认版税
        // const SETDEFAULTROYALTY=await owner.writeContract({
        //     address: MyRoyaltyNFT.address,
        //     abi: MyRoyaltyNFT.abi,
        //     functionName: "setDefaultRoyalty",
        //     args: [user3.account.address,"500"],
        // });
        // console.log(SETDEFAULTROYALTY)
        //设置版税
       const setTokenRoyalty = await owner.writeContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "setTokenRoyalty",
            args: [0,user3.account.address,"500"],
        });
        //查询版税信息
        const royaltyInfo1=await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "royaltyInfo",
            args: [0,parseEther("3")],
        });
        console.log("更新后版税信息",royaltyInfo1)
        //转账nft
        const TRANSFERFROM=await user1.writeContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "transferFrom",
            args: [user1.account.address,user2.account.address,0],
        });
        //查询nft的新拥有者
        const ownerOf1=await publicClient.readContract({
            address: MyRoyaltyNFT.address,
            abi: MyRoyaltyNFT.abi,
            functionName: "ownerOf",
            args: [0],
        });
        console.log(ownerOf1)
    });

});

测试指令

bash 复制代码
npx hardhat test ./test/xxx.ts

总结

至此,关于ERC-2981 版税标准从理论梳理到代码实现、工程落地的全流程实践,既验证了该标准的核心价值,也为开发者提供了可直接复用的版税 NFT 开发范式,是 NFT 生态从 "交易" 向 "持续价值分配" 演进的重要落地参考。

相关推荐
友莘居士12 小时前
Windows下Node.js 执行Web3.js 的智能合约环境搭建
windows·node.js·web3
CertiK12 小时前
CertiK年度安全报告:2025年Web3损失同比增37%,钓鱼攻击与供应链事件成主要威胁
安全·web3
木西1 天前
ERC-4337 落地场景全盘点:游戏、DAO、DeFi 的下一个爆发点
web3·智能合约·solidity
小明的小名叫小明1 天前
Solidity入门(14)-Hardhat 3 单元测试基础与技巧
单元测试·区块链·solidity·hardhat
Swift社区1 天前
跨端路由设计:如何统一 RN 与 Web 的页面模型
前端·react.js·web3
Moonbeam Community2 天前
Polkadot 2025:从协议工程到可用的去中心化云平台
大数据·web3·去中心化·区块链·polkadot
OpenBuild.xyz2 天前
x402 V2:架构重构 + 多链兼容,定义智能代理支付新标准
web3·区块链
iMingzhen2 天前
区块链概述及比特币工作原理
web3·去中心化·区块链
币圈菜头2 天前
GAEA Carbon-Silicon Symbiotism NFT 解析:它在系统中扮演的角色,以及与空投权重的关系
人工智能·web3·去中心化·区块链