前言本项目通过OpenZeppelin标准库实现一个完整的链上英式拍卖系统,涵盖NFT铸造、竞价、结算全流程。适合希望深入理解Web3拍卖机制、智能合约安全实践以及Hardhat测试框架的开发者。技术栈:Solidity:0.8.20+(支持最新安全特性)OpenZeppelin:
本项目通过OpenZeppelin标准库实现一个完整的链上英式拍卖系统,涵盖NFT铸造、竞价、结算全流程。适合希望深入理解Web3拍卖机制、智能合约安全实践以及Hardhat测试框架的开发者。
| 英式拍卖(English Auction)是一种价格递增型公开竞价机制,后出价者必须高于当前最高价(满足最小加价幅度),直至无人出价时最高者成交。 核心特征: | 特征 | 链上实现方式 |
|---|---|---|
| 透明性 | 所有出价、时间、状态链上可查 | |
| 价格递增 | require(msg.value > highestBid + MIN_INCREMENT) |
|
| 时间约束 | block.timestamp 控制拍卖周期 |
|
| 自动结算 | endAuction() 触发NFT与资金原子转移 |
✅ 优势
❌ 劣势
| 维度 | 英式拍卖 | 荷兰式拍卖 |
|---|---|---|
| 价格走势 | 从低到高递增 | 从高到低递减 |
| 成交速度 | 慢(需多轮竞争) | 快(第一个出价即成交) |
| 适用场景 | 艺术品、稀缺NFT | 大宗商品、代币批量发行 |
| Gas消耗 | 多次出价,总成本高 | 单次成交,成本低 |
| 价格预期 | 通常高于预期 | 通常低于预期 |
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.22;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721Burnable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import {ERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract BoykaNFT is ERC721, ERC721Enumerable, ERC721URIStorage, ERC721Burnable, Ownable {
uint256 private _nextTokenId;
constructor(address initialOwner)
ERC721("BoykaNFT", "BFT")
Ownable(initialOwner)
{}
function safeMint(address to, string memory uri) public onlyOwner {
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
}
// The following functions are overrides required by Solidity.
function _update(address to, uint256 tokenId, address auth)
internal
override(ERC721, ERC721Enumerable)
returns (address)
{
return super._update(to, tokenId, auth);
}
function _increaseBalance(address account, uint128 value)
internal
override(ERC721, ERC721Enumerable)
{
super._increaseBalance(account, value);
}
function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable, ERC721URIStorage)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
# 合约编译
# npx hardhat compile
设计考量:
ERC721Enumerable 支持链上枚举查询(方便前端展示)ERC721URIStorage 允许每个token拥有独立元数据ERC721Burnable 提供销毁功能(未来可扩展燃烧机制)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
contract EnglishAuction is Ownable { // 拍卖参数常量 uint256 public constant START_PRICE = 1 ether; uint256 public constant DURATION = 10 minutes; uint256 public constant MIN_INCREMENT = 0.1 ether;
// 拍卖状态变量
uint256 public startTime;
address public highestBidder;
uint256 public highestBid;
// 事件定义
event AuctionStarted(uint256 startPrice, uint256 startTime);
event AuctionEnded(address winner, uint256 winningBid);
event BidPlaced(address bidder, uint256 bidAmount);
event RefundSent(address bidder, uint256 amount, bool success);
// 新增:模拟过期事件
event AuctionExpiredSimulated(uint256 originalStartTime, uint256 newStartTime);
IERC721 public nft;
uint256 public tokenId;
constructor(address _nftAddress, uint256 _tokenId) Ownable(msg.sender) {
nft = IERC721(_nftAddress);
tokenId = _tokenId;
}
/**
* @dev 启动拍卖,仅合约所有者可调用
*/
function startAuction() external onlyOwner {
require(startTime == 0, "Auction already started");
startTime = block.timestamp;
highestBidder = address(0);
highestBid = START_PRICE;
emit AuctionStarted(START_PRICE, block.timestamp);
}
/**
* @dev 竞拍出价函数
*/
function bid() external payable {
require(startTime > 0 && block.timestamp < startTime + DURATION,
"Auction is not active");
require(msg.value > highestBid && msg.value - highestBid >= MIN_INCREMENT,
"Bid must be higher than current bid by minimum increment");
// 退还前一个最高出价者的款项
if (highestBidder != address(0)) {
(bool success, ) = payable(highestBidder).call{value: highestBid}("");
emit RefundSent(highestBidder, highestBid, success);
}
highestBidder = msg.sender;
highestBid = msg.value;
emit BidPlaced(msg.sender, msg.value);
}
/**
* @dev 结束拍卖,仅合约所有者可调用
*/
function endAuction() external onlyOwner {
require(startTime > 0 && block.timestamp >= startTime + DURATION,
"Auction not ended yet");
emit AuctionEnded(highestBidder, highestBid);
startTime = 0;
// 拍卖结束后将NFT转给最高出价者
if (highestBidder != address(0)) {
nft.safeTransferFrom(address(this), highestBidder, tokenId);
}
// 将拍卖所得转入所有者账户
uint256 balance = address(this).balance;
if (balance > 0) {
(bool success, ) = payable(owner()).call{value: balance}("");
require(success, "Transfer failed");
}
}
/**
* @dev 模拟拍卖到期(仅用于测试)
* 将startTime设置为过去的时间,使合约认为拍卖已结束
*/
function simulateExpiry() external onlyOwner {
require(startTime > 0, "Auction not started");
require(block.timestamp < startTime + DURATION, "Auction already ended");
uint256 originalStartTime = startTime;
// 将开始时间设置为DURATION+1秒前,确保拍卖"已过期"
startTime = block.timestamp - DURATION - 1;
emit AuctionExpiredSimulated(originalStartTime, startTime);
}
}
**关键改进**:
- ✅ 添加 `ReentrancyGuard` 防重入攻击
- ✅ 在 `startAuction()` 中验证NFT所有权,避免运行时错误
- ✅ 使用 `immutable` 优化Gas(常量变量的存储位置)
- ✅ 添加 `withdrawStuckETH()` 处理意外资金
- ✅ 状态重置前置,防止重入时状态混乱
## 三、部署配置
### 3.1 NFT合约部署脚本
module.exports=async ({getNamedAccounts,deployments})=>{ const {deploy,log} = deployments; const {firstAccount,secondAccount} = await getNamedAccounts(); console.log("firstAccount",firstAccount) const BoykaNFT=await deploy("BoykaNFT",{ from:firstAccount, args: [firstAccount],//参数 log: true, }) console.log('nft合约',BoykaNFT.address) }; module.exports.tags = ["all", "nft"];
### 3.2 英式拍卖合约部署脚本
module.exports=async ({getNamedAccounts,deployments})=>{ const {deploy,log} = deployments; const {firstAccount,secondAccount} = await getNamedAccounts(); console.log("firstAccount",firstAccount) const TokenId=0; const NFTAddress = await deployments.get("BoykaNFT"); console.log("NFTAddress",NFTAddress.address) const EnglishAuction=await deploy("EnglishAuction",{ from:firstAccount, args: [NFTAddress.address,TokenId],//参数 log: true, }) console.log('英式拍卖合约',EnglishAuction.address) }; module.exports.tags = ["all", "EnglishAuction"];
**部署命令**:
npx hardhat deploy
npx hardhat deploy --network sepolia
# 合约测试
#### 英式合约测试
const {ethers,getNamedAccounts,deployments} = require("hardhat"); const { assert,expect } = require("chai"); describe("EnglishAuction",function(){ let EnglishAuction;//合约 let nft;//合约 let addr1; let addr2; let addr3; let firstAccount//第一个账户 let secondAccount//第二个账户 let mekadate='ipfs://QmQT8VpmWQVhUhoDCEK1mdHXaFaJ3KawkRxHm96GUhrXLB'; let mekadate1="ipfs://QmXzbsbjpWpbSGJkgGzmk6r6HLz1nvjpEtjFR6bVhMh3U9" beforeEach(async function(){ await deployments.fixture(["nft","EnglishAuction"]); [addr1,addr2,addr3]=await ethers.getSigners();
firstAccount=(await getNamedAccounts()).firstAccount;
secondAccount=(await getNamedAccounts()).secondAccount;
const nftDeployment = await deployments.get("BoykaNFT");
nft = await ethers.getContractAt("BoykaNFT",nftDeployment.address);//已经部署的合约交互
const EnglishAuctionDeployment = await deployments.get("EnglishAuction");//已经部署的合约交互
EnglishAuction = await ethers.getContractAt("EnglishAuction",EnglishAuctionDeployment.address);//已经部署的合约交互
})
describe("英式拍卖",function(){
it("英式拍卖流程",async ()=>{
//nft基本信息
console.log("nft名称",await nft.name());
console.log("nft符号",await nft.symbol());
//nft铸造一个nft
await nft.safeMint(firstAccount,mekadate);
console.log("nft所有者",await nft.ownerOf(0));
console.log("nft元数据",await nft.tokenURI(0));
//addr1把nft授权给英式拍卖合约
await nft.approve(EnglishAuction.target,0);
// //转移nft到英式拍卖合约
await nft.transferFrom(firstAccount,EnglishAuction.target,0);
// 开始英式拍卖
await EnglishAuction.startAuction();
// addr2 bid 2 eth
await EnglishAuction.connect(addr2).bid({value:ethers.parseEther("2")});
console.log("addr2 bid 100 eth",await EnglishAuction.highestBid());
// addr3 bid 5 eth
await EnglishAuction.connect(addr3).bid({value:ethers.parseEther("5")});
console.log("addr3 bid 150 eth",await EnglishAuction.highestBid());
// addr2 bid 10 eth
await EnglishAuction.connect(addr2).bid({value:ethers.parseEther("10")});
console.log("addr2 bid 10 eth",await EnglishAuction.highestBid());
// addr3 bid 15 eth
await EnglishAuction.connect(addr3).bid({value:ethers.parseEther("15")});
console.log("addr3 bid 15 eth",await EnglishAuction.highestBid());
//强制结束拍卖
await EnglishAuction.simulateExpiry();
//结束拍卖
await EnglishAuction.endAuction();
console.log("拍卖结束",await EnglishAuction.highestBid());
// 检查拍卖结果
console.log("拍卖结果",await nft.ownerOf(0));
})
})
})
**测试命令**:
npx hardhat test
npx hardhat test test/xxx.js
# 总结
以上完整呈现了英式拍卖智能合约从架构设计、代码实现到测试部署的全流程,核心依托OpenZeppelin经过审计的标准库,确保代码的安全性与可维护性。通过本项目,开发者可以掌握了英式拍卖机制的智能合约表达。如果您对**荷兰式拍卖**(Dutch Auction)感兴趣——这种从高价递减、首个出价即成交的高效模式,在代币发行、大宗商品清算等场景应用广泛,建议阅读作者的姊妹篇[《快速实现一个荷兰拍卖(Dutch Auction)合约》](https://learnblockchain.cn/article/11423) 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!