前言
维克里拍卖(第二价格密封拍卖),作为一种"密封出价、价高者得、支付第二高价"的真话诱导机制,通过智能合约的透明执行与策略简化特性,完美解决了传统拍卖中的竞价博弈难题。本指南将完整呈现其链上实现:从开发阶段构建Commit-Reveal保密机制、第二价格自动计算与保留价判定逻辑,到测试阶段验证密封出价、限时揭示流程、流拍处理及防恶意攻击边界条件,最终完成合约多网络部署与链上交互验证。通过系统性的工程实践,为公平高效、激励相容的链上资产竞拍提供可直接复用的技术方案。
概念
维克里拍卖(又称第二价格密封拍卖)是一种出价最高者获胜,但按第二高价支付的拍卖机制。由诺贝尔经济学奖得主威廉·维克里于1961年提出,其本质是:
- "说真话"的机制设计 :在私有价值模型下,如实报价(报出真实心理价位)是每个竞拍者的最优策略
- 激励相容性:不存在竞价操纵动机,简化了参与者决策
特性
| 特征维度 | 具体表现 |
|---|---|
| 报价策略 | 报真实估值为最优策略,无需复杂博弈 |
| 信息需求 | 无需知晓他人估值,降低信息收集成本 |
| 公平性 | 对出价高者更友好,支付价格≤其报价 |
| 效率性 | 资源总能配置给估值最高的参与者 |
| 透明度 | 仅公布成交价,保护赢家隐私 |
| 潜在风险 | 存在"假名攻击"(shill bidding)漏洞 |
理论优势:相比首价拍卖(需猜测他人出价),策略更简单且社会福利最大化
拍卖流程
-
阶段1:拍卖创建(Preparation)
-
阶段2:密封出价(Bidding)
-
阶段3:揭示价格(Reveal)
-
阶段4:结果结算(Settlement)
总结:和第一价格密封拍卖流程一样都属于密封拍卖的类型
智能合约开发、测试、部署
智能合约
1. NFT智能合约
// 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";
/**
* @title VickreyAuction(完全修复版)
* @dev 第二价格密封拍卖合约(Vickrey拍卖)
* @notice 修复了 bidder 地址存储问题,确保 winner 正确设置
*/
contract VickreyAuction is Ownable, ReentrancyGuard, Pausable, IERC721Receiver {
struct Auction {
address seller;
address nftContract;
uint256 tokenId;
uint256 reservePrice;
uint256 commitDeadline;
uint256 revealDeadline;
address winner;
uint256 winningBid;
uint256 secondHighestBid;
bool ended;
bool claimed;
uint256 revealedBidCount;
}
struct BidCommitment {
bytes32 commitment;
bool hasCommitted;
bool hasRevealed;
uint256 amount;
uint256 deposit;
}
// ✅ 修复:存储揭示的出价和投标者
struct RevealedBid {
address bidder;
uint256 amount;
}
mapping(uint256 => Auction) public auctions;
mapping(uint256 => mapping(address => BidCommitment)) public commitments;
mapping(uint256 => RevealedBid[]) private revealedBids; // ✅ 修改这里
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,
uint256 secondHighestBid,
uint256 finalPrice
);
event NFTClaimed(uint256 indexed auctionId, address indexed winner);
event FundsWithdrawn(uint256 indexed auctionId, address indexed seller, uint256 amount);
event DepositRefunded(uint256 indexed auctionId, address indexed bidder, uint256 amount);
constructor() Ownable(msg.sender) {}
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");
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,
winner: address(0),
winningBid: 0,
secondHighestBid: 0,
ended: false,
claimed: false,
revealedBidCount: 0
});
emit AuctionCreated(
auctionId,
msg.sender,
nftContract,
tokenId,
reservePrice,
currentTime + commitDuration,
currentTime + commitDuration + revealDuration
);
return auctionId;
}
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: 0,
deposit: msg.value
});
emit BidCommitted(auctionId, msg.sender, commitment);
}
function revealBid(
uint256 auctionId,
uint256 amount,
uint256 secret
) external whenNotPaused nonReentrant {
require(commitments[auctionId][msg.sender].hasCommitted, "Must commit first");
_processReveal(auctionId, msg.sender, amount, secret);
}
function forceRevealBid(
uint256 auctionId,
address bidder,
uint256 amount,
uint256 secret
) external onlyOwner nonReentrant {
require(!auctions[auctionId].ended, "Auction already ended");
require(commitments[auctionId][bidder].hasCommitted, "No commitment found");
require(!commitments[auctionId][bidder].hasRevealed, "Already revealed");
_processReveal(auctionId, bidder, amount, secret);
}
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");
require(amount > 0, "Invalid bid amount");
require(bid.deposit >= amount, "Insufficient deposit");
bid.hasRevealed = true;
bid.amount = amount;
Auction storage auction = auctions[auctionId];
auction.revealedBidCount++;
// ✅ 修复:存储投标者地址和金额
revealedBids[auctionId].push(RevealedBid({
bidder: bidder,
amount: amount
}));
emit BidRevealed(auctionId, bidder, amount);
}
function endAuction(uint256 auctionId) external nonReentrant {
require(!auctions[auctionId].ended, "Auction already ended");
_endAuction(auctionId);
}
function forceEndAuction(uint256 auctionId) external onlyOwner nonReentrant {
require(!auctions[auctionId].ended, "Auction already ended");
_endAuction(auctionId);
}
function _endAuction(uint256 auctionId) internal {
Auction storage auction = auctions[auctionId];
auction.ended = true;
RevealedBid[] storage bids = revealedBids[auctionId];
uint256 bidCount = bids.length;
if (bidCount == 0) {
emit AuctionEnded(auctionId, address(0), 0, 0, 0);
return;
}
uint256 highestBid = 0;
uint256 secondHighestBid = 0;
address highestBidder = address(0);
// ✅ 修复:直接从结构体读取bidder和amount
for (uint256 i = 0; i < bidCount; i++) {
address bidder = bids[i].bidder;
uint256 currentBid = bids[i].amount;
if (currentBid > highestBid) {
secondHighestBid = highestBid;
highestBid = currentBid;
highestBidder = bidder;
} else if (currentBid > secondHighestBid) {
secondHighestBid = currentBid;
}
}
auction.winningBid = highestBid;
auction.secondHighestBid = secondHighestBid;
uint256 finalPrice = secondHighestBid > auction.reservePrice ?
secondHighestBid : auction.reservePrice;
if (highestBid >= auction.reservePrice) {
auction.winner = highestBidder; // ✅ 正确设置winner
emit AuctionEnded(auctionId, highestBidder, highestBid, secondHighestBid, finalPrice);
} else {
emit AuctionEnded(auctionId, address(0), highestBid, secondHighestBid, 0);
}
}
// ✅ 移除损坏的函数
// function _findBidderByAmount(uint256 /*auctionId*/, uint256 /*amount*/) internal pure returns (address) {
// return address(0);
// }
function claimNFT(uint256 auctionId) external nonReentrant {
Auction storage auction = auctions[auctionId];
require(auction.ended, "Auction not ended");
require(auction.winner != address(0), "No winner");
require(msg.sender == auction.winner, "Not winner");
require(!auction.claimed, "NFT already claimed");
auction.claimed = true;
IERC721(auction.nftContract).safeTransferFrom(
address(this),
auction.winner,
auction.tokenId
);
emit NFTClaimed(auctionId, auction.winner);
}
function withdrawFunds(uint256 auctionId) external nonReentrant {
Auction storage auction = auctions[auctionId];
require(auction.ended, "Auction not ended");
require(auction.winner != address(0), "No winner");
require(msg.sender == auction.seller, "Only seller");
uint256 finalPrice = auction.secondHighestBid > auction.reservePrice ?
auction.secondHighestBid : auction.reservePrice;
require(finalPrice > 0, "No funds to withdraw");
auction.secondHighestBid = 0;
(bool success, ) = payable(auction.seller).call{value: finalPrice}("");
require(success, "Transfer failed");
emit FundsWithdrawn(auctionId, auction.seller, finalPrice);
}
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 found");
bool shouldRefund = !bid.hasRevealed ||
(bid.hasRevealed && msg.sender != auction.winner) ||
auction.winner == address(0);
require(shouldRefund, "Cannot refund");
uint256 refundAmount = bid.deposit;
bid.deposit = 0;
(bool success, ) = payable(msg.sender).call{value: refundAmount}("");
require(success, "Refund failed");
emit DepositRefunded(auctionId, msg.sender, refundAmount);
}
function getAuction(uint256 auctionId) external view returns (Auction memory) {
return auctions[auctionId];
}
function getCommitment(uint256 auctionId, address bidder)
external
view
returns (BidCommitment memory)
{
return commitments[auctionId][bidder];
}
// ✅ 修复:返回 RevealedBid[] memory
function getRevealedBids(uint256 auctionId)
external
view
returns (RevealedBid[] memory)
{
return revealedBids[auctionId];
}
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external pure override returns (bytes4) {
return this.onERC721Received.selector;
}
function _setWinnerForTest(
uint256 auctionId,
address winner,
uint256 winningBid,
uint256 secondHighestBid
) external onlyOwner {
Auction storage auction = auctions[auctionId];
auction.winner = winner;
auction.winningBid = winningBid;
auction.secondHighestBid = secondHighestBid;
auction.ended = true;
}
}
编译指令
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 VickreyAuction=await deploy("VickreyAuction",{
from:firstAccount,
args: [],//参数
log: true,
})
console.log('维克里拍卖合约',VickreyAuction.address)
};
module.exports.tags = ["all", "VickreyAuction"];
部署指令
npx hardhat deploy --tags xxx.xxx //(例如:nft,VickreyAuction)
智能合约测试
维克里拍卖智能合约测试脚本
const {ethers,getNamedAccounts,deployments} = require("hardhat");
const { assert,expect } = require("chai");
describe("VickreyAuction",function(){
let VickreyAuction;//合约
let nft;//合约
let addr1;
let addr2;
let addr3;
let firstAccount//第一个账户
let secondAccount//第二个账户
let mekadate='ipfs://QmQT8VpmWQVhUhoDCEK1mdHXaFaJ3KawkRxHm96GUhrXLB';
beforeEach(async function(){
await deployments.fixture(["nft","VickreyAuction"]);
[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 VickreyAuctionDeployment = await deployments.get("VickreyAuction");//已经部署的合约交互
VickreyAuction = await ethers.getContractAt("VickreyAuction",VickreyAuctionDeployment.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(VickreyAuction.target, 0);
console.log("✓ NFT已授权给拍卖合约");
// 创建拍卖:保留价1 ETH,commit和reveal各360秒
const reservePrice = ethers.parseEther("1");
const createAuctionTx = await VickreyAuction.createAuction(
nft.target,
0,
reservePrice,
360, // commit持续360秒
360 // reveal持续360秒
);
await createAuctionTx.wait();
const auctionId = 0;
const auction = await VickreyAuction.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 = VickreyAuction.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 = VickreyAuction.connect(addr3);
await auctionWithAddr3.commitBid(auctionId, commitment3, { value: ethers.parseEther("3.5") });
console.log("✓ addr3已提交承诺,出价:", ethers.formatEther(bidAmount3), "ETH");
// 验证承诺已存储
const commitmentData2 = await VickreyAuction.getCommitment(auctionId, addr2.address);
const commitmentData3 = await VickreyAuction.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 VickreyAuction.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 VickreyAuction.endAuction(auctionId);
console.log("✓ 拍卖已手动结束");
// 验证拍卖状态和结果
const endedAuction = await VickreyAuction.getAuction(auctionId);
// console.log(endedAuction)
console.log(" 拍卖状态:", endedAuction.ended);
console.log(" 最高出价者:", endedAuction.winner);
console.log(" 最高出价:", ethers.formatEther(endedAuction.winningBid));
console.log(" 第二高出价:", ethers.formatEther(endedAuction.secondHighestBid));
assert.equal(endedAuction.ended, true);
assert.equal(endedAuction.winner, addr3.address);
assert.equal(endedAuction.winningBid, bidAmount3);
assert.equal(endedAuction.secondHighestBid, bidAmount2);
console.log("✓ 拍卖结果验证: addr3以", ethers.formatEther(bidAmount2), "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 = VickreyAuction.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 //(例如:VickreyAuction.js)
总结
至此,维克里拍卖智能合约全流程实现已完成。该机制以"密封出价、价高者得、支付第二高价"为核心,通过Commit-Reveal模式、限时揭示机制与第二价格自动计算保障链上竞价公平性与激励相容。开发基于OpenZeppelin构建安全架构,测试运用Hardhat完成多阶段流程验证,最终部署为链上资产竞拍提供策略简化、可信执行的技术方案。