前言
本文围绕 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) :
royaltyInfo(uint256 tokenId, uint256 salePrice):返回版税接收地址与应付金额(以基点计算,1 基点 = 0.01%)。- 可选实现:
_setTokenRoyalty(单 token 版税)、_setDefaultRoyalty(全局默认版税),用于设置版税规则。
-
关键特性:链上透明、可组合、兼容主流 NFT 标准,不依赖平台规则,由合约自主定义版税策略。
ERC-2981 能做什么
- 标准化版税信息存储与查询:在 NFT 合约中嵌入版税规则(比例、接收地址),市场通过统一接口读取,无需自定义解析逻辑。
- 自动分成触发 :二级市场交易时,合规市场调用
royaltyInfo计算分成,自动将版税划转至创作者 / 权利人,剩余款项给卖家。 - 灵活规则配置:支持单 token 独立版税、全局默认版税,可通过权限控制更新接收地址,适配动态分成场景。
- 跨平台互通:统一接口让 NFT 在不同市场交易时,版税规则一致,提升生态可组合性。
- 链上追溯:版税信息与交易记录上链,便于审计与纠纷排查,降低信任成本。
ERC-2981 解决了什么
| 痛点 | 解决方案 |
|---|---|
| 版税碎片化 | 统一接口替代各平台专有规则,开发者无需重复适配 |
| 收益不可靠 | 链上存储规则,减少依赖平台中心化结算的信用风险 |
| 跨平台不兼容 | 标准接口让市场无缝读取版税,保障创作者跨平台收益 |
| 信息不透明 | 公开可查询的版税比例与接收地址,避免暗箱操作 |
| 开发成本高 | 复用 OpenZeppelin 等库的实现,降低版税功能开发门槛 |
使用场景
- 数字艺术 / 收藏品:艺术家铸造 NFT 时设置 5%-10% 版税,每次转售自动分成,如 CryptoPunks(兼容后)、Art Blocks 项目。
- 游戏资产:游戏道具 NFT 转售时,开发者按比例获取分成,用于持续开发与运营,适配 ERC-1155 批量资产场景。
- 音乐 / 影视 NFT:版权方在二次交易中获得收益,支持多权利人按比例分配(需结合多签 / 分账合约)。
- IP 衍生品:IP 方通过版税获取长期收益,如品牌联名 NFT 的持续分成。
- 创作者 DAO / 社区:版税收入进入 DAO 金库,用于生态建设或社区分红,提升治理效率。
- 跨链 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 生态从 "交易" 向 "持续价值分配" 演进的重要落地参考。