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

  • 木西
  • 发布于 3小时前
  • 阅读 20

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

前言

本文围绕 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智能合约

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

}

### 编译指令

npx hardhat compile

## 智能合约部署脚本

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

### 部署指令

npx hardhat run ./scripts/xxx.ts

## 智能合约测试脚本

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

});

### 测试指令

npx hardhat test ./test/xxx.ts


# 总结
至此,关于ERC-2981 版税标准从理论梳理到代码实现、工程落地的全流程实践,既验证了该标准的核心价值,也为开发者提供了可直接复用的版税 NFT 开发范式,是 NFT 生态从 “交易” 向 “持续价值分配” 演进的重要落地参考。
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
木西
木西
0x5D5C...2dD7
江湖只有他的大名,没有他的介绍。