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

  • 木西
  • 发布于 13小时前
  • 阅读 27

前言本项目通过OpenZeppelin标准库实现一个完整的链上英式拍卖系统,涵盖NFT铸造、竞价、结算全流程。适合希望深入理解Web3拍卖机制、智能合约安全实践以及Hardhat测试框架的开发者。技术栈:Solidity:0.8.20+(支持最新安全特性)OpenZeppelin:

前言

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

// 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)

    
    // 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"];

**部署命令**:

部署到本地Hardhat网络

npx hardhat deploy

部署到Sepolia测试网

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)
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
木西
木西
0x5D5C...2dD7
江湖只有他的大名,没有他的介绍。