前言
本项目通过OpenZeppelin标准库实现一个完整的链上英式拍卖系统,涵盖NFT铸造、竞价、结算全流程。适合希望深入理解Web3拍卖机制、智能合约安全实践以及Hardhat测试框架的开发者。
技术栈:
- Solidity: 0.8.20+(支持最新安全特性)
- OpenZeppelin: 5.0.0+(经过审计的标准合约)
- Hardhat: 2.19.0+(开发、测试、部署一体化)
- ethers.js : 6.0+(与合约交互) 技术栈:
一、英式拍卖核心概念详解
1.1 定义与特征
英式拍卖(English Auction)是一种价格递增型公开竞价 机制,后出价者必须高于当前最高价(满足最小加价幅度),直至无人出价时最高者成交。 核心特征:
| 特征 | 链上实现方式 |
|---|---|
| 透明性 | 所有出价、时间、状态链上可查 |
| 价格递增 | require(msg.value > highestBid + MIN_INCREMENT) |
| 时间约束 | block.timestamp 控制拍卖周期 |
| 自动结算 | endAuction() 触发NFT与资金原子转移 |
1.2 优缺点分析
✅ 优势
- 价格发现最优:多轮竞价充分反映市场价值
- 去信任化:代码即规则,无需第三方拍卖行
- 资金即出价:出价同时锁定资金,防止恶意抬价
- 自动退款:新最高价产生时立即退还前出价者
❌ 劣势
- Gas成本高:每次出价都是链上交易
- 时间成本高:需等待整个拍卖周期
- 狙击攻击:恶意竞标者在最后时刻出价(需扩展期机制缓解)
- 资金效率低:非赢家资金被锁定至拍卖结束
1.3 与荷兰式拍卖对比
| 维度 | 英式拍卖 | 荷兰式拍卖 |
|---|---|---|
| 价格走势 | 从低到高递增 | 从高到低递减 |
| 成交速度 | 慢(需多轮竞争) | 快(第一个出价即成交) |
| 适用场景 | 艺术品、稀缺NFT | 大宗商品、代币批量发行 |
| Gas消耗 | 多次出价,总成本高 | 单次成交,成本低 |
| 价格预期 | 通常高于预期 | 通常低于预期 |
二、智能合约开发
2.1 NFT合约(BoykaNFT)
typescript
// 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提供销毁功能(未来可扩展燃烧机制)
2.2 英式拍卖合约(EnglishAuction)
ini
// 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合约部署脚本
javascript
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 英式拍卖合约部署脚本
javascript
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"];
部署命令:
bash
# 部署到本地Hardhat网络
npx hardhat deploy
# 部署到Sepolia测试网
npx hardhat deploy --network sepolia
合约测试
英式合约测试
javascript
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));
})
})
})
测试命令:
bash
# 运行所有测试
npx hardhat test
# 运行特定测试文件
npx hardhat test test/xxx.js
总结
以上完整呈现了英式拍卖智能合约从架构设计、代码实现到测试部署的全流程,核心依托OpenZeppelin经过审计的标准库,确保代码的安全性与可维护性。通过本项目,开发者可以掌握了英式拍卖机制的智能合约表达。如果您对荷兰式拍卖 (Dutch Auction)感兴趣------这种从高价递减、首个出价即成交的高效模式,在代币发行、大宗商品清算等场景应用广泛,建议阅读作者的姊妹篇《快速实现一个荷兰拍卖(Dutch Auction)合约》