文章目录
原文链接:https://trufflesuite.com/guides/nft-royalty/#lets-write-an-erc-2981
概述
本文我们将简要介绍什么是以太坊改进提案(EIP)和以太坊意见征求稿(ERC),以及它们是如何使用的,实现一个 ERC-2981标准的 NFT 版税标准的合约案例。
什么是 EIP(Ethereum Improvement Proposals)?
官网:https://eips.ethereum.org/
EIP全称Ethereum Improvement Proposal(以太坊改进提案),是以太坊特定新功能和流程的技术设计文档。
EIP 作为主要机制
- 提出新功能。
- 收集社区对某个问题的技术意见。
- 记录已经进入以太坊的设计决策。
EIP根据所涉及的领域不同大体可以分为三大类Standards Track EIP、Meta EIP和Informational EIP:
-
Standards Track EIP
它描述的是任何改变以太坊所有或大多数实现细节的EIP。进一步分为四个子类:Core、Networking、Interface、ERC。 -
Meta:描述了以太坊周边相关事物的改变过程,例如对决策过程的改变。
-
Informational:提供一般信息或描述以太坊设计问题,但不提出新功能。用户可以自由地忽略Informational EIP,因为信息性 EIP 不一定代表以太坊社区的推荐。
什么是 ERC(Ethereum Request For Comment )?
ERC的全称是Ethereum Request For Comment ,即以太坊意见征求稿。主要是用来记录以太坊上应用级的开发标准和约定。例如Token标准、名称注册表、URI 方案、库/包格式和钱包格式(EIP75、EIP85)。ERC 指定了合约需要实现的一组必需功能,以便应用程序和其他合约可以理解如何与它们交互。 例如,最流行的标准之一是ERC-721标准,它定义了 NFT 是什么。因为应用程序知道 ERC-721 是什么样子,所以它知道它可以在合约上与哪些函数和属性交互。
请注意,ERC 不被视为core EIP,因此是否采用该标准取决于开发人员。因此,提高对 ERC 的认识对其作用至关重要。
为什么EIPs 和 ERCs 重要?
EIP 是以太坊治理的核心方式。任何人都可以提出它们,社区成员可以评论、辩论和协作来决定它是否应该被采纳!您可以在此处找到提交指南。
ERC 是智能合约的可组合性的动力!可组合性定义了 dapp 和合约相互交互的能力。例如,ERC-2981 NFT 版税标准定义了如何在合约上存储版税信息,以便在市场等 dapp 进行销售时,他们知道如何获得补偿艺术家所需的版税信息!
ERC-2981 是什么?
如上所述,ERC-2981 是版税标准。为了符合 ERC-2981 标准,智能合约必须具有以下功能:
pragma solidity ^0.6.0;
import "./IERC165.sol";
///
/// @dev Interface for the NFT Royalty Standard
///
interface IERC2981 is IERC165 {
/// ERC165 bytes to add to interface array - set in parent contract
/// implementing this standard
///
/// bytes4(keccak256("royaltyInfo(uint256,uint256)")) == 0x2a55205a
/// bytes4 private constant _INTERFACE_ID_ERC2981 = 0x2a55205a;
/// _registerInterface(_INTERFACE_ID_ERC2981);
/// @notice Called with the sale price to determine how much royalty
// is owed and to whom.
/// @param _tokenId - the NFT asset queried for royalty information
/// @param _salePrice - the sale price of the NFT asset specified by _tokenId
/// @return receiver - address of who should be sent the royalty payment
/// @return royaltyAmount - the royalty payment amount for _salePrice
function royaltyInfo(
uint256 _tokenId,
uint256 _salePrice
) external view returns (
address receiver,
uint256 royaltyAmount
);
}
interface IERC165 {
/// @notice Query if a contract implements an interface
/// @param interfaceID The interface identifier, as specified in ERC-165
/// @dev Interface identification is specified in ERC-165. This function
/// uses less than 30,000 gas.
/// @return `true` if the contract implements `interfaceID` and
/// `interfaceID` is not 0xffffffff, `false` otherwise
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
ps:ERC-165是一种允许合约声明其对接口的支持的标准。这将使市场能够检查 NFT 是否支持版税标准!在市场合约中可能看起来像这样:
bytes4 private constant _INTERFACE_ID_ERC2981 = 0x2a55205a;
function checkRoyalties(address _contract) internal returns (bool) {
(bool success) = IERC165(_contract).supportsInterface(_INTERFACE_ID_ERC2981);
return success;
}
在我们开始编写代码之前,我们先了解一些关于 NFT 版税标准的重要警告。
-
版权支付规范没有强制。根据该信息采取行动取决于市场。目前,Coinbase NFT、Rarible、SuperRare 和 Zora 为 ERC-2981 支付版税。如果你的 NFT 在 OpenSea 上出售,你将必须通过他们的网站单独设置你的 NFT 的版税。
让我们写一个 ERC-2981合约
我们已经介绍了什么是 EIP、什么是 ERC 以及它们如何表示 ERC-2981,我们实际编写一个实现 ERC-2981 版税标准的 ERC-721 NFT 智能合约。您可以在此处找到完整的代码。我们将导入 Open Zeppelin 的合约,它提供安全的、预写的 ERC 实现,我们的合约可以继承这些实现!
下载安装系统依赖
- Node.js, v12 or higher
- truffle
- ganache UI or ganache CLI
创建 Infura 帐户和项目
要将您的 DApp 连接到以太坊主网和测试网,您需要一个 Infura 帐户。在这里注册一个帐户。
登录后,创建一个项目!我们称之为nft-royalty,然后从下拉列表中选择 Web3 API
注册MetaMask 钱包
要与浏览器中的dapp互动,您需要一个MetaMask钱包。在此处注册一个帐户。
获取测试Eth
为了部署到公共测试网络,您需要一些测试ETH来支付您的gas费用!Paradigm 是一个很棒的多链水龙头,它一次将资金存放在8个不同的网络中。
设置您的项目
Truffle 用来构建您的 truffle 项目并添加示例合约和测试。我们创建一个名为nft-royalty的项目。
truffle init nft-royalty
cd nft-royalty
truffle create contract RoyalPets
truffle create test TestRoyalties
执行完,项目目录结构如下:
nft-royalty
├── contracts
│ └── RoyalPets.sol
├── migrations
│ └── 1_deploy_contracts.js
├── test
│ └── test_royalties.js
└── truffle-config.js
编写 NFT 智能合约
Open Zeppelin 已经提供了安全的、写好的 ERC-2981 和 ERC-721 合约实现,我们可以继承!要下载它们,只需调用npm i "@openzeppelin/contracts"
。
借助 OpenZeppelin,我们有几种方法可以识别 NFT 合约是否符合版税标准。由于我们的基础合约是 ERC-721,我们可以选择继承 OpenZeppelin 的版税合约ERC721Royalty。该合约覆盖了该_burn功能,以清除NFT的版税信息。
重要的提示!此函数和来自OpenZeppelin的
_burn
函数都不检查tokenId
所有权。这意味着任何人都可以销毁这个 NFT。如果您想避免这种情况,请添加require
检查该条件的检查。
function _burn(uint256 tokenId) internal virtual override {
super._burn(tokenId);
_resetTokenRoyalty(tokenId);
}
创建合约后,我们要设置默认的版税接收人和百分比。请注意,OpenZeppelin 使用基点计算特许权使用费。为了将默认接收人设置为合约所有者并将费用设置为 1%,可以在构造函数中进行设置:
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Royalty.sol";
contract RoyalPets is ERC721Royalty {
constructor() ERC721("RoyalPets", "RP") {
_setDefaultRoyalty(msg.sender, 100);
}
}
在本教程中,我们使用 OpenZeppelin 的ERC721URIStorage
扩展。在这种情况下,我们希望它也继承OpenZeppelin ERC2981合约的属性,如下所示:
import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
contract RoyalPets is ERC721URIStorage, ERC2981 {
constructor() ERC721("RoyalPets", "RP") {
_setDefaultRoyalty(msg.sender, 100);
}
}
此时,我们可以看到vscode报错,ERC721URIStorage
和ERC2981
都实现了supportsInterface
方法!为了解决这个问题,我们也需要在RoyalPets 中自己实现。在这个函数中添加:
function supportsInterface(bytes4 interfaceId)
public view virtual override(ERC721, ERC2981)
returns (bool) {
return super.supportsInterface(interfaceId);
}
此外,因为我们不再继承ERC721Royalty,所以我们不再拥有它的_burn
方法。需要我们自己添加:
function _burn(uint256 tokenId)
internal virtual override {
super._burn(tokenId);
_resetTokenRoyalty(tokenId);
}
为了让外部账户可以销毁他们的 NFT,开放了一个公开销毁方法:
function burnNFT(uint256 tokenId)
public {
_burn(tokenId);
}
最后,我们将添加 NFT 铸币功能。案例将创建两种类型的铸造函数:一种使用默认版税信息铸造代币,另一种在每个代币的基础上指定版税信息。
如前所述,Infura 博客涵盖了这些基础知识。一个细微的差别是我们不会使用静态元数据文件来填充tokenURI. 两个铸币函数如下所示:
function mintNFT(address recipient, string memory tokenURI)
public onlyOwner
returns (uint256) {
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_safeMint(recipient, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
function mintNFTWithRoyalty(address recipient, string memory tokenURI, address royaltyReceiver, uint96 feeNumerator)
public onlyOwner
returns (uint256) {
uint256 tokenId = mintNFT(recipient, tokenURI);
_setTokenRoyalty(tokenId, royaltyReceiver, feeNumerator);
return tokenId;
}
最终的智能合约,如下:
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract RoyalPets is ERC721URIStorage, ERC2981, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
constructor() ERC721("RoyalPets", "RP") {
_setDefaultRoyalty(msg.sender, 100);
}
function supportsInterface(bytes4 interfaceId)
public view virtual override(ERC721, ERC2981)
returns (bool) {
return super.supportsInterface(interfaceId);
}
function _burn(uint256 tokenId) internal virtual override {
super._burn(tokenId);
_resetTokenRoyalty(tokenId);
}
function burnNFT(uint256 tokenId)
public onlyOwner {
_burn(tokenId);
}
function mintNFT(address recipient, string memory tokenURI)
public onlyOwner
returns (uint256) {
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_safeMint(recipient, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
function mintNFTWithRoyalty(address recipient, string memory tokenURI, address royaltyReceiver, uint96 feeNumerator)
public onlyOwner
returns (uint256) {
uint256 tokenId = mintNFT(recipient, tokenURI);
_setTokenRoyalty(tokenId, royaltyReceiver, feeNumerator);
return tokenId;
}
}
本地部署智能合约
为了部署我们的智能合约,我们需要对migrations/1_deploy_contracts.js
修改:
const RoyalPets = artifacts.require("Royalpets");
module.exports = function (deployer) {
deployer.deploy(RoyalPets);
};
接下来,我们启动一个本地 Ganache 实例。
有多种方法可以做到这一点:通过 VS Code 扩展、Ganache CLI 和 Ganche 图形用户界面。2种方式都有自己的优势,您可以在此处查看Ganache v7 版本的功能。
在本教程中,我们将使用 GUI。运行Ganche 图形用户界面,创建一个工作区,然后点击保存。它会在 HTTP://127.0.0.1:7545 上创建一个正在运行的 Ganache 实例。
接下来,在truffle-config.js
中取消下面代码注释,将 development
中的端口号修改为 7545 。
development: {
host: "127.0.0.1", // Localhost (default: none)
port: 7545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
}
然后运行truffle migrate
,默认为development
网络,即可部署!
也可以从 VS Code 扩展进行部署。build/contracts
然后,您可以在 VS Code 扩展中或在 Ganache UI中查看您构建的合约!
测试你的智能合约
如果您想在不编写完整测试的情况下即时测试您的智能合约命令,您可以使用truffle develop
或truffle console
。在这里阅读更多相关信息。
出于本教程的目的,我们将继续编写 Javascript 测试。
请注意,使用 Truffle,您可以选择使用 Javascript、Typescript 或 Solidity 编写测试。
const RoyalPets = artifacts.require("RoyalPets");
contract("RoyalPets", function (accounts) {
it("should support the ERC721 and ERC2198 standards", async () => {
const royalPetsInstance = await RoyalPets.deployed();
const ERC721InterfaceId = "0x80ac58cd";
const ERC2981InterfaceId = "0x2a55205a";
var isERC721 = await royalPetsInstance.supportsInterface(ERC721InterfaceId);
var isER2981 = await royalPetsInstance.supportsInterface(ERC2981InterfaceId);
assert.equal(isERC721, true, "RoyalPets is not an ERC721");
assert.equal(isER2981, true, "RoyalPets is not an ERC2981");
});
it("should return the correct royalty info when specified and burned", async () => {
const royalPetsInstance = await RoyalPets.deployed();
await royalPetsInstance.mintNFT(accounts[0], "fakeURI");
// Override royalty for this token to be 10% and paid to a different account
await royalPetsInstance.mintNFTWithRoyalty(accounts[0], "fakeURI", accounts[1], 1000);
const defaultRoyaltyInfo = await royalPetsInstance.royaltyInfo.call(1, 1000);
var tokenRoyaltyInfo = await royalPetsInstance.royaltyInfo.call(2, 1000);
const owner = await royalPetsInstance.owner.call();
assert.equal(defaultRoyaltyInfo[0], owner, "Default receiver is not the owner");
// Default royalty percentage taken should be 1%.
assert.equal(defaultRoyaltyInfo[1].toNumber(), 10, "Royalty fee is not 10");
assert.equal(tokenRoyaltyInfo[0], accounts[1], "Royalty receiver is not a different account");
// Default royalty percentage taken should be 1%.
assert.equal(tokenRoyaltyInfo[1].toNumber(), 100, "Royalty fee is not 100");
// Royalty info should be set back to default when NFT is burned
await royalPetsInstance.burnNFT(2);
tokenRoyaltyInfo = await royalPetsInstance.royaltyInfo.call(2, 1000);
assert.equal(tokenRoyaltyInfo[0], owner, "Royalty receiver has not been set back to default");
assert.equal(tokenRoyaltyInfo[1].toNumber(), 10, "Royalty has not been set back to default");
});
});
执行命令:
truffle test
执行结果:
Contract: RoyalPets
✔ should support the ERC721 and ERC2198 standards (67ms)
✔ should return the correct royalty info when specified and burned (1077ms)
2 passing (1s)
铸造一个 NFT 并在您的移动钱包或 OpenSea 中查看它!
如果您想铸造NFT并在MetaMask 钱包中查看它,则需要将合约部署到公共测试网或主网上。为此,您需要从Infura项目和MetAmask Wallet Secret Key中获取Infura Project API。在您的文件夹的根目录,添加一个.env
文件,输入该信息。
警告:不要泄漏或提交此文件。我们建议将
.env
添加到.gitignore
文件中。
MNEMONIC="YOUR SECRET KEY"
INFURA_API_KEY="YOUR INFURA_API_KEY"
在ruffle-config.js
文件的顶部,添加如下代码以获取这个信息:
require('dotenv').config();
const mnemonic = process.env["MNEMONIC"];
const infuraApiKey = process.env["INFURA_API_KEY"];
const HDWalletProvider = require('@truffle/hdwallet-provider');
最后,将 Goerli 网络添加到以下networks
列表中:
goerli: {
provider: () => new HDWalletProvider(mnemonic, `https://goerli.infura.io/v3/${infuraApiKey}`),
network_id: 5, // Goerli's network id
chain_id: 5, // Goerli's chain id
gas: 5500000, // Gas limit used for deploys.
confirmations: 2, // # of confirmations to wait between deployments. (default: 0)
timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
skipDryRun: true // Skip dry run before migrations? (default: false for public nets)
}
最终的truffle-config.js
,如下
require('dotenv').config();
const mnemonic = process.env["MNEMONIC"];
const infuraApiKey = process.env["INFURA_API_KEY"];
const HDWalletProvider = require('@truffle/hdwallet-provider');
module.exports = {
networks: {
development: {
host: "127.0.0.1", // Localhost (default: none)
port: 7545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
},
goerli: {
provider: () => new HDWalletProvider(mnemonic, `https://goerli.infura.io/v3/${infuraApiKey}`),
network_id: 5, // Goerli's network id
chain_id: 5, // Goerli's chain id
gas: 5500000, // Gas limit used for deploys.
confirmations: 2, // # of confirmations to wait between deployments. (default: 0)
timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
skipDryRun: true // Skip dry run before migrations? (default: false for public nets)
}
},
// Set default mocha options here, use special reporters, etc.
mocha: {
// timeout: 100000
},
// Configure your compilers
compilers: {
solc: {
version: "0.8.15", // Fetch exact version from solc-bin (default: truffle's version)
}
},
};
需要为 dotenv
和 @truffle/hdwallet-provider
dev依赖项。
npm i --save-dev dotenv
npm i --save-dev @truffle/hdwallet-provider
truffle migrate --network goerli
最后,运行 truffle migrate --network goerli
进行部署!
然后,要快速与Goerli网络进行交互,我们可以使用truffle console --network goerli
,并调用合适的合约函数。我们已经将一些元数据固定在IPFS上,供您使用,tokenURI: ipfs://bafybeiffapvkruv2vwtomswqzxiaxdgm2dflet2cxmh6t4ixrgaezumbw4
。
命令执行如下:
truffle migrate --network goerli
truffle(goerli)> const contract = await RoyalPets.deployed()
undefined
truffle(goerli)> await contract.mintNFT("YOUR ADDRESS", "ipfs://bafybeiffapvkruv2vwtomswqzxiaxdgm2dflet2cxmh6t4ixrgaezumbw4")
如果您想使用自己的元数据,可以使用Truffle或Infura。在此处查看指南:
要在您的手机钱包上查看您的 NFT,请手机打开 MetaMask ,切换到 Goerli 网络,然后打开 NFTs 选项卡!要在 OpenSea 上查看,您必须部署到主网或Polygon。否则,如果您将合约部署到 rinkeby,您可以在 https://testnets.opensea.io/
上查看它。请注意,合并后 rinkeby 将被弃用。
如果您不想在Infura项目中监视您的交易,也可以通过Truffle Dashboard设置,这使您可以通过Metamask部署和签署交易 - 并且永远不会泄漏您的私钥!为此,只需运行:
truffle dashboard
truffle migrate --network dashboard
truffle console --network dashboard
未来扩展
至此,您已经写了一份NFT智能合约,可以查询版税信息。请注意将元数据上传到IPFS的更深入的指南!有关代码的详细演练,可以在YouTube上观看直播。在Web3的未来版本中,通过实现ERC-4907以及创建存在各种NFT标准的NFT租赁市场,可以看到我们如何通过实现ERC-4907来生成基本的ERC-721s!
您可能会考虑的其他一些扩展实现oyaltyInfo的方式。Gemini是一个很酷的博客,详细介绍了一些decaying royalties, multisig royalties, and stepped royalties在这里。如果您尝试任何一个,请告诉我们!
如果您想关于内容进行谈论,建议您在此处展开讨论。如果您想展示自己的建造或与Unleashed社区一起交流,请加入我们的 Discord!最后,不要忘记在Twitter上关注我们,以获取所有Truffle的最新更新。