快速实现一个英式拍卖(English Auction)合约

前言

本项目通过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)合约》

相关推荐
MetaverseMan4 小时前
智能合约升级(Upgradeable Smart Contracts)
区块链·智能合约
fyihdg5 小时前
前端调用Solidity智能合约连接MetaMask小狐狸钱包,并在Alchemy测试网发布
web3·智能合约
测试人社区-千羽18 小时前
边缘计算场景下的智能测试挑战
人工智能·python·安全·开源·智能合约·边缘计算·分布式账本
TechubNews1 天前
Stripe 拟于本月 12 日上线稳定币支付功能
web3·区块链
古城小栈1 天前
Go语言原生智能合约开发与部署完全指南
golang·区块链·智能合约
友莘居士2 天前
solidity中数据位置storage、memory、calldata的区别
区块链·memory·solidity·storage·calldata·数据位置
ZFJ_张福杰2 天前
【区块链】区块链智能合约:从原理到应用的完整入门指南
区块链·智能合约
Web3VentureView2 天前
从“庞氏骗局”到“价值发现”:Web3 行业自我修正与新范式的曙光
大数据·金融·web3·去中心化·区块链
币圈菜头3 天前
GAEA × REVOX 合作 — 共建「情感 AI + Web3 应用」新生态
人工智能·web3·去中心化·区块链