前言本文用Hardhat+OpenZeppelin5.x,完成一条「可铸造、可提现、带版税」的ERC-721代币主网流水线,分别为智能合约和前端两部分,本文主要介绍智能合约相关开发的内容;前期准备使用hardhat创建项目:这里就不赘述了具体可以作者的另一篇文章《智能合约开发
本文用 Hardhat + OpenZeppelin 5.x,完成一条「可铸造、可提现、带版税」的 ERC-721 代币主网流水线,分别为智能合约和前端两部分,本文主要介绍智能合约相关开发的内容;
前期准备
- 使用hardhat创建项目:这里就不赘述了具体可以作者的另一篇文章《智能合约开发、测试、部署全流程(实操篇)》
- 如果感觉麻烦也可以使用Remix在线编辑器来快速验证:可以参考《 Remix IDE 智能合约开发全指南:从编码到部署调试》
编译合约
合约说明:
- 铸造
create()
(含退款)- 提现
withdraw()
- ERC-2981 默认版税 & 单 Token 版税
- 权限控制(Ownable)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/*
安装:
npm i @openzeppelin/contracts@5.4
*/
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
contract SimpleClosedNFT is ERC721URIStorage, ERC2981, Ownable {
uint256 public nextTokenId = 1;
uint96 public constant FEE_DENOMINATOR = 10_000; // 100% = 10000
mapping(uint256 => uint256) public mintPrice; // tokenId => price in wei
constructor(
string memory _name,
string memory _symbol,
address _initialOwner,
uint96 _defaultRoyalty // 500 = 5%
) ERC721(_name, _symbol) Ownable(_initialOwner) {
_setDefaultRoyalty(_initialOwner, _defaultRoyalty);
}
/* ========== 铸造 ========== */
function create(
string calldata tokenURI,
uint256 price,
uint96 royaltyBps // 可选:单个作品的版税,0 则沿用默认
) external payable returns (uint256 tokenId) {
require(msg.value >= price, "Insufficient payment");
tokenId = nextTokenId++;
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, tokenURI);
mintPrice[tokenId] = price;
// 如果传了 royaltyBps,则单独设置
if (royaltyBps > 0) {
_setTokenRoyalty(tokenId, msg.sender, royaltyBps);
}
// 退回多余 ETH
if (msg.value > price) {
(bool ok, ) = msg.sender.call{value: msg.value - price}("");
require(ok, "Refund failed");
}
}
/* ========== 提现 ========== */
function withdraw() external onlyOwner {
(bool ok, ) = owner().call{value: address(this).balance}("");
require(ok, "Withdraw failed");
}
/* ========== 支持 721 + 2981 ========== */
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721URIStorage, ERC2981) // ✅ 只写真正声明 override 的合约
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
编译指令:npx hardhat compile
部署说明:主要使用hardhat-deploy插件快速部署合约
hardhat.config.json配置相关:
# 说明:按照顺序
require("@nomicfoundation/hardhat-ethers");//ethers V6
require('hardhat-deploy');//部署插件
require("hardhat-deploy-ethers");//部署插件解决getContract
部署核心代币
module.exports = async ({getNamedAccounts,deployments})=>{
const getNamedAccount = (await getNamedAccounts()).firstAccount;
const NFTName = "SimpleClosedNFT";
const NFTSymbol = "SCNFT";
const DefaultRoyalty = 500;
const {deploy,log} = deployments;
const SimpleClosedNFT=await deploy("SimpleClosedNFT",{
from:getNamedAccount,
args: [NFTName,NFTSymbol,getNamedAccount,DefaultRoyalty],//参数 string memory _name,string memory _symbol,address _initialOwner,uint96 _defaultRoyalty // 500 = 5%
log: true,
})
// await hre.run("verify:verify", {
// address: TokenC.address,
// constructorArguments: [TokenName, TokenSymbol],
// });
console.log('SimpleClosedNFT合约地址',SimpleClosedNFT.address)
}
module.exports.tags = ["all", "SimpleClosedNFT"];
部署指令:npx hardhat deploy
测试说明:主要针对合约核心功能进行测试
const {ethers,getNamedAccounts,deployments} = require("hardhat");
const { assert,expect } = require("chai");
describe("SimpleClosedNFT",function(){
let addr1,addr2;
let firstAccount//第一个账户
let secondAccount//第二个账户
beforeEach(async function(){
await deployments.fixture(["SimpleClosedNFT"]);
[addr1,addr2]=await ethers.getSigners();
firstAccount=(await getNamedAccounts()).firstAccount;
secondAccount=(await getNamedAccounts()).secondAccount;
const SimpleClosedNFTDeployment = await deployments.get("SimpleClosedNFT");
SimpleClosedNFT = await ethers.getContractAt("SimpleClosedNFT",SimpleClosedNFTDeployment.address);//已经部署的合约交互
});
describe("SimpleClosedNFT 测试",function(){
it("读取合约基本信息以及查看账户余额",async function(){
console.log('代币名',await SimpleClosedNFT.name())
console.log('代币符号',await SimpleClosedNFT.symbol())
});
it("铸造一个nft",async function(){
const uri = "https://zygomorphic-magenta-bobolink.myfilebase.com/ipfs/QmZdC1RywVL2mPry9TSP128ZUrvPJCJPtndqMUpv5TNctn";
const price = ethers.parseEther("0.1");
const royaltyBps=500;
await SimpleClosedNFT.connect(addr1).create(uri,price,royaltyBps,{value:price})
console.log(await SimpleClosedNFT.tokenURI(1))
console.log("nft的所有者",await SimpleClosedNFT.ownerOf(1))
const mintPrice=await SimpleClosedNFT.mintPrice(1)
console.log("nft的价格",ethers.formatEther(mintPrice))
});
it("设置版税",async function(){
const uri = "https://zygomorphic-magenta-bobolink.myfilebase.com/ipfs/QmZdC1RywVL2mPry9TSP128ZUrvPJCJPtndqMUpv5TNctn";
const price = ethers.parseEther("0.1");
const royaltyBps=1000;//10%
await SimpleClosedNFT.connect(addr1).create(uri, price, royaltyBps, { value: price });
const info = await SimpleClosedNFT.royaltyInfo(1, 10000);//tokenid,salePrice(销售价格)
console.log("版税接收地址",info[0])
console.log("版税金额",info[1])
});
it("提现功能",async function(){
const uri = "https://zygomorphic-magenta-bobolink.myfilebase.com/ipfs/QmZdC1RywVL2mPry9TSP128ZUrvPJCJPtndqMUpv5TNctn";
const price = ethers.parseEther("0.1");
const royaltyBps=1000;//10%
await SimpleClosedNFT
.connect(addr1)
.create(uri, price, 0, {
value: price,
});
const owner = await SimpleClosedNFT.owner(); // 合约 owner 地址
const ownerBalBefore = await ethers.provider.getBalance(owner);
console.log("提现前",ownerBalBefore)
const tx = await SimpleClosedNFT.connect(await ethers.getSigner(owner)).withdraw();
const receipt = await tx.wait();
const ownerBalAfter = await ethers.provider.getBalance(owner);
const withdrawAmount = price; // 本例中只有一笔 0.1 ETH
const gasUsed = receipt.gasUsed * receipt.gasPrice;
console.log("提现后",ownerBalAfter)
console.log("Gas消耗",gasUsed)
console.log("提现金额",withdrawAmount)
expect(ownerBalAfter).to.equal(ownerBalBefore + withdrawAmount - gasUsed);
});
it("非Owner不能提取",async function(){
console.log("异常报错",await SimpleClosedNFT.connect(addr2).withdraw())
});
});
});
测试指令:npx hardhat test ./test/xxx.js
至此,合约部分已全部收尾:
代码、测试、部署脚本一条龙就绪,并在本地与测试网跑通。
补充一句行规—— “NFT 合约的测试代码行数通常是源码的 5~10 倍” ;安全与逻辑正确性只能靠测试兜底,而非 Solidity 语法本身。
下一步,我们将把这套经过充分验证的合约无缝接入前端,进入真正的用户交互阶段。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!