前言第一价格密封拍卖,作为一种"密封出价、价高者得、支付最高价"的经典竞价机制,天然契合智能合约的透明执行特性。本指南将完整呈现其链上实现:从开发阶段构建Commit-Reveal保密机制与保留价判定逻辑,到测试阶段验证拍卖流程、流拍处理及所有者特权,最终完成合约部署与链上验证。通过系统性的工程
第一价格密封拍卖,作为一种"密封出价、价高者得、支付最高价"的经典竞价机制,天然契合智能合约的透明执行特性。本指南将完整呈现其链上实现:从开发阶段构建Commit-Reveal保密机制与保留价判定逻辑,到测试阶段验证拍卖流程、流拍处理及所有者特权,最终完成合约部署与链上验证。通过系统性的工程实践,为公平可信的链上资产竞拍提供可直接复用的技术方案。
概念
第一价格密封拍卖(First-price sealed-bid auction)是密封拍卖的一种形式,竞买人以独立密封方式提交报价,最高出价者按其所报价格成交付款
特征
- 密封性:竞买人无法获知其他参与者的数量及出价信息,所有报价严格保密;
- 一次性出价:每位竞买人只有一次报价机会,提交后不可更改;
- "价高者得"原则 :拍卖师开启密封报价后,出价最高者赢得拍卖品;
- 支付规则:获胜者需支付自己的出价金额(即最高出价),而非次高价;
- 同步竞价:所有竞买人在规定时间内同时提交报价,采用独立密封方式;
- 适用场景:多用于工程项目招标、大宗货物、土地房产等不动产交易以及资源开采权出让;
拍卖流程
第一阶段:准备与登记
第二阶段:密封投标
第三阶段:开标与成交
第四阶段:支付与交割
// 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);
}
}
#### 2. 第一价格密封拍卖智能合约
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
/**
@dev 新增功能:所有者特权 - 可随时强制结束拍卖 & 强制验证 reveal */ contract FirstPriceSealedAuction is Ownable, ReentrancyGuard, Pausable, IERC721Receiver { struct Auction { address seller; address nftContract; uint256 tokenId; uint256 reservePrice; // 保留价 uint256 commitDeadline; // 承诺截止时间 uint256 revealDeadline; // 揭示截止时间 address highestBidder; uint256 highestBid; bool ended; bool claimed; }
struct BidCommitment { bytes32 commitment; // 出价哈希 bool hasCommitted; bool hasRevealed; uint256 amount; }
// 拍卖ID => 拍卖信息 mapping(uint256 => Auction) public auctions; // 拍卖ID => 竞标者地址 => 承诺信息 mapping(uint256 => mapping(address => BidCommitment)) public commitments; // 记录已结束的拍卖数量 uint256 public auctionCounter;
// 事件定义 event AuctionCreated( uint256 indexed auctionId, address indexed seller, address indexed nftContract, uint256 tokenId, uint256 reservePrice, uint256 commitDeadline, uint256 revealDeadline );
event BidCommitted( uint256 indexed auctionId, address indexed bidder, bytes32 commitment );
event BidRevealed( uint256 indexed auctionId, address indexed bidder, uint256 amount );
event AuctionEnded( uint256 indexed auctionId, address indexed winner, uint256 winningBid );
event NFTClaimed(uint256 indexed auctionId, address indexed winner); event FundsWithdrawn(uint256 indexed auctionId, address indexed seller, uint256 amount);
/**
/**
@dev 创建新的拍卖 */ function createAuction( address nftContract, uint256 tokenId, uint256 reservePrice, uint256 commitDuration, uint256 revealDuration ) external whenNotPaused nonReentrant returns (uint256) { require(nftContract != address(0), "Invalid NFT contract"); require(commitDuration > 0 && revealDuration > 0, "Invalid duration");
// 转移NFT到合约 IERC721(nftContract).safeTransferFrom(msg.sender, address(this), tokenId);
uint256 auctionId = auctionCounter++; uint256 currentTime = block.timestamp;
auctions[auctionId] = Auction({ seller: msg.sender, nftContract: nftContract, tokenId: tokenId, reservePrice: reservePrice, commitDeadline: currentTime + commitDuration, revealDeadline: currentTime + commitDuration + revealDuration, highestBidder: address(0), highestBid: 0, ended: false, claimed: false });
emit AuctionCreated( auctionId, msg.sender, nftContract, tokenId, reservePrice, currentTime + commitDuration, currentTime + commitDuration + revealDuration );
return auctionId; }
/**
@dev 提交出价承诺(哈希) */ function commitBid(uint256 auctionId, bytes32 commitment) external payable whenNotPaused nonReentrant { Auction storage auction = auctions[auctionId]; require(block.timestamp < auction.commitDeadline, "Commit period ended"); require(msg.value > 0, "Must send deposit"); require(!commitments[auctionId][msg.sender].hasCommitted, "Already committed");
commitments[auctionId][msg.sender] = BidCommitment({ commitment: commitment, hasCommitted: true, hasRevealed: false, amount: msg.value });
emit BidCommitted(auctionId, msg.sender, commitment); }
/**
@dev 内部函数:处理 reveal 逻辑 */ function _processReveal( uint256 auctionId, address bidder, uint256 amount, uint256 secret ) internal { BidCommitment storage bid = commitments[auctionId][bidder]; require(bid.hasCommitted, "No commitment found"); require(!bid.hasRevealed, "Already revealed");
// 验证哈希匹配 bytes32 calculatedCommitment = keccak256(abi.encode(amount, secret)); require(calculatedCommitment == bid.commitment, "Invalid commitment");
bid.hasRevealed = true; bid.amount = amount;
// 检查保证金是否足够 require(bid.amount >= amount, "Insufficient deposit");
// 记录最高出价 Auction storage auction = auctions[auctionId]; if (amount > auction.highestBid) { auction.highestBid = amount; auction.highestBidder = bidder; }
emit BidRevealed(auctionId, bidder, amount); }
/**
@dev 揭示实际出价(需等待 reveal 阶段) */ function revealBid( uint256 auctionId, uint256 amount, uint256 secret ) external virtual whenNotPaused nonReentrant { Auction storage auction = auctions[auctionId]; require( block.timestamp >= auction.commitDeadline && block.timestamp < auction.revealDeadline, "Not reveal period" );
_processReveal(auctionId, msg.sender, amount, secret); }
/**
/**
@dev 内部函数:结束拍卖 */ function _endAuction(uint256 auctionId) internal { Auction storage auction = auctions[auctionId]; require(!auction.ended, "Auction already ended");
auction.ended = true;
// 如果最高出价低于保留价,则流拍 if (auction.highestBid < auction.reservePrice || auction.highestBidder == address(0)) { emit AuctionEnded(auctionId, address(0), 0); } else { emit AuctionEnded(auctionId, auction.highestBidder, auction.highestBid); } }
/**
@dev 正常结束拍卖(需等待揭示期结束) */ function endAuction(uint256 auctionId) external nonReentrant { Auction storage auction = auctions[auctionId]; require(block.timestamp >= auction.revealDeadline, "Reveal period not ended");
_endAuction(auctionId); }
/**
/**
@dev 胜标者领取NFT */ function claimNFT(uint256 auctionId) external nonReentrant { Auction storage auction = auctions[auctionId]; require(auction.ended, "Auction not ended"); require(auction.highestBid >= auction.reservePrice, "Auction failed"); require(msg.sender == auction.highestBidder, "Not winner"); require(!auction.claimed, "NFT already claimed");
auction.claimed = true;
IERC721(auction.nftContract).safeTransferFrom( address(this), auction.highestBidder, auction.tokenId );
emit NFTClaimed(auctionId, auction.highestBidder); }
/**
@dev 卖家提取资金 */ function withdrawFunds(uint256 auctionId) external nonReentrant { Auction storage auction = auctions[auctionId]; require(auction.ended, "Auction not ended"); require(auction.highestBid >= auction.reservePrice, "Auction failed"); require(msg.sender == auction.seller, "Only seller");
uint256 amount = auction.highestBid; auction.highestBid = 0;
(bool success, ) = payable(auction.seller).call{value: amount}(""); require(success, "Transfer failed");
emit FundsWithdrawn(auctionId, auction.seller, amount); }
/**
@dev 退还失败竞标者的保证金 */ function refundDeposit(uint256 auctionId) external nonReentrant { Auction storage auction = auctions[auctionId]; require(auction.ended, "Auction not ended");
BidCommitment storage bid = commitments[auctionId][msg.sender]; require(bid.hasCommitted, "No commitment");
bool shouldRefund = !bid.hasRevealed || (bid.hasRevealed && msg.sender != auction.highestBidder) || auction.highestBid < auction.reservePrice;
require(shouldRefund, "Cannot refund");
uint256 refundAmount = bid.amount; bid.amount = 0;
(bool success, ) = payable(msg.sender).call{value: refundAmount}(""); require(success, "Refund failed"); }
/**
/**
/**
/**
/**
#### 编译指令
npx hardhat compile
## 智能合约部署
#### 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"];
#### 2. 第一价格密封拍卖脚本
module.exports=async ({getNamedAccounts,deployments})=>{ const {deploy,log} = deployments; const {firstAccount,secondAccount} = await getNamedAccounts(); console.log("firstAccount",firstAccount) const FirstPriceSealedAuction=await deploy("FirstPriceSealedAuction",{ from:firstAccount, args: [],//参数 log: true, }) console.log('第一价格密封拍卖合约',FirstPriceSealedAuction.address) }; module.exports.tags = ["all", "FirstPriceSealedAuction"];
#### 部署指令
npx hardhat deploy --tags xxx,xxx //(例如:nft,FirstPriceSealedAuction)
## 智能合约测试
#### 第一价格密封拍卖拍卖流程测试脚本
const {ethers,getNamedAccounts,deployments} = require("hardhat"); const { assert,expect } = require("chai"); describe("FirstPriceSealedAuction",function(){ let FirstPriceSealedAuction;//合约 let nft;//合约 let addr1; let addr2; let addr3; let firstAccount//第一个账户 let secondAccount//第二个账户 let mekadate='ipfs://QmQT8VpmWQVhUhoDCEK1mdHXaFaJ3KawkRxHm96GUhrXLB'; beforeEach(async function(){ await deployments.fixture(["nft","FirstPriceSealedAuction"]); [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 FirstPriceSealedAuctionDeployment = await deployments.get("FirstPriceSealedAuction");//已经部署的合约交互
FirstPriceSealedAuction = await ethers.getContractAt("FirstPriceSealedAuction",FirstPriceSealedAuctionDeployment.address);//已经部署的合约交互
})
describe("第一价格密封拍卖",function(){
it("完整流程:创建、出价、揭示、结束、领取", async () => {
// ==================== 准备阶段 ====================
console.log("\n=== 准备阶段 ===");
// 铸造NFT给firstAccount
await nft.safeMint(firstAccount, mekadate);
console.log("✓ NFT已铸造,所有者:", await nft.ownerOf(0));
// firstAccount授权拍卖合约操作NFT
const nftWithSeller = nft.connect(await ethers.getSigner(firstAccount));
await nftWithSeller.approve(FirstPriceSealedAuction.target, 0);
console.log("✓ NFT已授权给拍卖合约");
// 创建拍卖:保留价1 ETH,commit和reveal各360秒
const reservePrice = ethers.parseEther("1");
const createAuctionTx = await FirstPriceSealedAuction.createAuction(
nft.target,
0,
reservePrice,
360, // commit持续360秒
360 // reveal持续360秒
);
await createAuctionTx.wait();
const auctionId = 0;
const auction = await FirstPriceSealedAuction.getAuction(auctionId);
console.log("✓ 拍卖已创建,ID:", auctionId);
console.log(" 保留价:", ethers.formatEther(reservePrice), "ETH");
console.log(" commit截止:", new Date(Number(auction.commitDeadline) * 1000).toLocaleString());
console.log(" reveal截止:", new Date(Number(auction.revealDeadline) * 1000).toLocaleString());
// ==================== Commit阶段 ====================
console.log("\n=== Commit阶段 ===");
// 准备出价:addr2出2 ETH,addr3出3 ETH
const bidAmount2 = ethers.parseEther("2");
const bidAmount3 = ethers.parseEther("3");
const secret2 = 12345;
const secret3 = 54321;
// 计算承诺哈希
const commitment2 = ethers.keccak256(
ethers.AbiCoder.defaultAbiCoder().encode(
["uint256", "uint256"],
[bidAmount2, secret2]
)
);
const commitment3 = ethers.keccak256(
ethers.AbiCoder.defaultAbiCoder().encode(
["uint256", "uint256"],
[bidAmount3, secret3]
)
);
// addr2提交承诺(发送2.5 ETH作为保证金)
const auctionWithAddr2 = FirstPriceSealedAuction.connect(addr2);
await auctionWithAddr2.commitBid(auctionId, commitment2, { value: ethers.parseEther("2.5") });
console.log("✓ addr2已提交承诺,出价:", ethers.formatEther(bidAmount2), "ETH");
// addr3提交承诺(发送3.5 ETH作为保证金)
const auctionWithAddr3 = FirstPriceSealedAuction.connect(addr3);
await auctionWithAddr3.commitBid(auctionId, commitment3, { value: ethers.parseEther("3.5") });
console.log("✓ addr3已提交承诺,出价:", ethers.formatEther(bidAmount3), "ETH");
// 验证承诺已存储
const commitmentData2 = await FirstPriceSealedAuction.getCommitment(auctionId, addr2.address);
const commitmentData3 = await FirstPriceSealedAuction.getCommitment(auctionId, addr3.address);
assert.equal(commitmentData2.hasCommitted, true);
assert.equal(commitmentData3.hasCommitted, true);
// 快进时间到commit阶段结束
await ethers.provider.send("evm_increaseTime", [400]); // 超过360秒
await ethers.provider.send("evm_mine");
console.log("⏰ 时间已推进到commit阶段结束");
// ==================== Reveal阶段 ====================
console.log("\n=== Reveal阶段 ===");
// 验证当前时间已在reveal阶段
const currentTime = await ethers.provider.getBlock('latest').then(b => b.timestamp);
const auctionAfterCommit = await FirstPriceSealedAuction.getAuction(auctionId);
assert.isAtLeast(currentTime, Number(auctionAfterCommit.commitDeadline));
// addr2揭示出价
await auctionWithAddr2.revealBid(auctionId, bidAmount2, secret2);
console.log("✓ addr2已揭示出价");
// addr3揭示出价
await auctionWithAddr3.revealBid(auctionId, bidAmount3, secret3);
console.log("✓ addr3已揭示出价");
// 快进时间到reveal阶段结束
await ethers.provider.send("evm_increaseTime", [400]);
await ethers.provider.send("evm_mine");
console.log("⏰ 时间已推进到reveal阶段结束");
// ==================== 结束拍卖 ====================
console.log("\n=== 结束拍卖 ===");
// 结束拍卖
await FirstPriceSealedAuction.endAuction(auctionId);
console.log("✓ 拍卖已手动结束");
// 验证拍卖状态和结果
const endedAuction = await FirstPriceSealedAuction.getAuction(auctionId);
assert.equal(endedAuction.ended, true);
assert.equal(endedAuction.highestBidder, addr3.address);
assert.equal(endedAuction.highestBid, bidAmount3);
console.log("✓ 拍卖结果验证: addr3以", ethers.formatEther(bidAmount3), "ETH获胜");
// ==================== 领取NFT ====================
console.log("\n=== 领取NFT ===");
// 验证auction.claimed状态
assert.equal(endedAuction.claimed, false);
// addr3领取NFT
await auctionWithAddr3.claimNFT(auctionId);
console.log("✓ 获胜者addr3已领取NFT");
// 验证NFT所有权已转移
const nftOwner = await nft.ownerOf(0);
assert.equal(nftOwner, addr3.address);
console.log("✓ NFT所有权已转移给addr3");
// ==================== 提取资金 ====================
console.log("\n=== 提取资金 ===");
// 获取卖家提取前的余额
const sellerBefore = await ethers.provider.getBalance(firstAccount);
// 卖家提取拍卖资金
const auctionWithSeller = FirstPriceSealedAuction.connect(await ethers.getSigner(firstAccount));
const withdrawTx = await auctionWithSeller.withdrawFunds(auctionId);
await withdrawTx.wait();
console.log("✓ 卖家已提取拍卖资金");
// 验证卖家余额增加(考虑gas费用)
const sellerAfter = await ethers.provider.getBalance(firstAccount);
const balanceIncrease = sellerAfter - sellerBefore;
console.log(" 卖家余额增加:", ethers.formatEther(balanceIncrease), "ETH");
expect(balanceIncrease).to.be.closeTo(bidAmount3, ethers.parseEther("0.01")); // 考虑gas费用
// ==================== 退还保证金 ====================
console.log("\n=== 退还保证金 ===");
// addr2(失败者)取回保证金
const addr2BalanceBefore = await ethers.provider.getBalance(addr2.address);
await auctionWithAddr2.refundDeposit(auctionId);
const addr2BalanceAfter = await ethers.provider.getBalance(addr2.address);
const addr2Refund = addr2BalanceAfter - addr2BalanceBefore;
console.log("✓ 失败者addr2已取回保证金:", ethers.formatEther(addr2Refund), "ETH");
expect(addr2Refund).to.be.closeTo(ethers.parseEther("2.5"), ethers.parseEther("0.01"));
// 验证获胜者addr3不能取回保证金
try {
await auctionWithAddr3.refundDeposit(auctionId);
assert.fail("获胜者不应该能取回保证金");
} catch (error) {
console.log("✓ 获胜者无法取回保证金(预期行为)");
}
console.log("\n🎉 完整拍卖流程测试通过!");
});
})
})
#### 测试指令
npx hardhat test ./test/xxx.js //(例如:FirstPriceSealedAuction.js)
# 总结
至此,第一价格密封拍卖智能合约全流程实现已完成。该机制以"密封出价、价高者得"为核心,通过Commit-Reveal模式与保留价判定保障链上竞价公平性。开发基于OpenZeppelin构建安全架构,测试运用Hardhat完成多场景验证,最终部署为NFT竞拍提供透明可信的技术方案。 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!