前言组合拍卖,作为一种"打包竞标、全有或全无、链下计算链上验证"的资源配置机制,通过智能合约的自动化执行与信任最小化特性,有效解决了多物品互补性拍卖中的计算复杂性与激励相容难题。本指南将完整呈现其链上实现:从开发阶段构建物品组合定义、密封投标承诺机制、揭示验证逻辑与批量结算流程,到测试阶段验证多
组合拍卖,作为一种"打包竞标、全有或全无、链下计算链上验证"的资源配置机制,通过智能合约的自动化执行与信任最小化特性,有效解决了多物品互补性拍卖中的计算复杂性与激励相容难题。本指南将完整呈现其链上实现:从开发阶段构建物品组合定义、密封投标承诺机制、揭示验证逻辑与批量结算流程,到测试阶段验证多物品组合投标、限时揭示窗口、未完全揭示处理及Gas优化边界条件,最终完成合约与ERC1155标准集成及多网络部署验证。通过系统性的工程实践,为高效透明、激励相容的链上数字资产组合竞拍提供可直接复用的技术方案。
概念
组合拍卖(Combinatorial Auction) 是一种允许竞标者对多个物品的组合(Bundle) 进行打包竞拍的拍卖形式。与传统单物品拍卖不同,其理论基础源于博弈论和机制设计,旨在解决多物品间存在互补性或替代性时的资源配置问题
特性
- 打包竞标:竞标者可对任意物品组合提交单一报价
- 激励相容:如实报价是竞标者的最优策略(机制设计保证)
- 全有或全无:中标即获得整个组合,避免部分分配导致的价值损失
- 密封竞价:通常采用两阶段机制(承诺+揭示),保护竞标信息
拍卖流程
阶段1:密封投标
阶段2:揭示投标
阶段3:胜者计算
阶段4:结算分配
| 应用领域 | 核心场景 | 典型案例 | 解决问题/价值 |
|---|---|---|---|
| 通信与频谱 | 频段和区域牌照组合竞拍 | 美国FCC频谱拍卖(2008年73期,Verizon 47亿美元购C Block)<br>加拿大4G频谱拍卖 | 暴露风险、阈值问题,提升拍卖收入 |
| 能源与云计算 | 储能容量共享、移动云服务打包 | 移动云资源拍卖(用户竞标app组合)<br>跨链MEV拍卖 | 双向报价、协同价值最大化 |
| 工业与制造业 | 废旧物资批量处置、专利组合打包 | 聚拍网"竞价-组合拍卖"<br>北方昆物光电十项专利整体拍卖 | 降低交易成本30-50%,整合技术资产 |
| Web3与数字资产 | NFT批量交易、元宇宙虚拟地产打包 | The Sandbox/Axie Infinity道具组合<br>智能合约实现(密封投标+链下计算) | 原子性、Gas优化、可编程支付规则 |
| 交通与物流 | 机场时段分配、供应链多批次运输 | 机场跑道时段组合调度<br>路线资源打包分配 | 处理时段互补性,提升资源利用率 |
| 新兴交叉领域 | 专利池授权、司法拍卖"带押过户"、网络资源组合 | 不动产跨行贷款+产权同步转移<br>带宽+存储动态服务打包 | 降低碎片化成本,实现跨域资源同步 |
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.22;
import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import {ERC1155Burnable} from "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract GameToken is ERC1155, Ownable, ERC1155Burnable {
constructor(address initialOwner)
ERC1155("ipfs://QmcN49MKt4MbSXSGckAcpvFqtea43uuPD2tvmuER1mG67s")
Ownable(initialOwner)
{}
function setURI(string memory newuri) public onlyOwner {
_setURI(newuri);
}
function mint(address account, uint256 id, uint256 amount, bytes memory data)
public
onlyOwner
{
_mint(account, id, amount, data);
}
function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data)
public
onlyOwner
{
_mintBatch(to, ids, amounts, data);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; // 添加这行
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
contract CombinatorialAuction is ReentrancyGuard, Ownable, Pausable , ERC1155Holder {
using ECDSA for bytes32;
using MessageHashUtils for bytes32;
// 拍卖状态枚举
enum AuctionState { NOT_STARTED, BIDDING, REVEALING, ENDED }
// 物品结构
struct Item {
uint256 id;
uint256 amount; // ERC1155数量
address tokenContract;
uint256 tokenId;
}
// 密封投标结构
struct SealedBid {
bytes32 commitment; // 投标哈希
uint256 deposit; // 保证金
bool revealed; // 是否已揭示
}
// 揭示后的投标
struct Bid {
address bidder;
uint256[] itemIds; // 投标的物品组合
uint256 bidValue; // 投标金额
bytes signature; // 竞标者签名
}
// 拍卖状态变量
AuctionState public state;
uint256 public biddingEndTime;
uint256 public revealEndTime;
uint256 public constant REVEAL_PERIOD = 1 days;
// 物品管理
Item[] public items;
mapping(uint256 => uint256) public itemIdToIndex;
// 投标管理
mapping(address => SealedBid) public sealedBids;
mapping(address => Bid) public revealedBids;
address[] public biddersList;
// 拍卖结果
address[] public winners;
mapping(address => uint256[]) public winnerItemIds;
mapping(address => uint256) public winnerPayments;
// 事件
event AuctionStarted(uint256 biddingDeadline);
event BidSubmitted(address indexed bidder, bytes32 commitment);
event BidRevealed(address indexed bidder, uint256[] itemIds, uint256 value);
event AuctionEnded(address[] winners, uint256 totalRevenue);
constructor(
address[] memory _tokenContracts,
uint256[] memory _tokenIds,
uint256[] memory _amounts,
uint256 _biddingPeriod
) Ownable(msg.sender) {
require(
_tokenContracts.length == _tokenIds.length &&
_tokenIds.length == _amounts.length,
"Arrays length mismatch"
);
for (uint i = 0; i < _tokenContracts.length; i++) {
items.push(Item({
id: i,
amount: _amounts[i],
tokenContract: _tokenContracts[i],
tokenId: _tokenIds[i]
}));
itemIdToIndex[i] = i;
}
biddingEndTime = block.timestamp + _biddingPeriod;
revealEndTime = biddingEndTime + REVEAL_PERIOD;
}
// 提交密封投标(使用哈希承诺)
function submitSealedBid(bytes32 _commitment) external payable nonReentrant whenNotPaused {
require(state == AuctionState.NOT_STARTED || state == AuctionState.BIDDING, "Not in bidding phase");
require(block.timestamp < biddingEndTime, "Bidding period ended");
require(sealedBids[msg.sender].commitment == bytes32(0), "Bid already submitted");
require(msg.value > 0, "Deposit required");
sealedBids[msg.sender] = SealedBid({
commitment: _commitment,
deposit: msg.value,
revealed: false
});
biddersList.push(msg.sender);
state = AuctionState.BIDDING;
emit BidSubmitted(msg.sender, _commitment);
}
// 揭示投标(必须与密封哈希匹配)
function revealBid(
uint256[] memory _itemIds,
uint256 _bidValue,
bytes memory _signature,
bytes32 _secret
) external nonReentrant whenNotPaused {
require(state == AuctionState.BIDDING || state == AuctionState.REVEALING, "Not in reveal phase");
require(block.timestamp >= biddingEndTime && block.timestamp < revealEndTime, "Not reveal period");
SealedBid storage sealedBid = sealedBids[msg.sender];
require(!sealedBid.revealed, "Bid already revealed");
// 验证投标哈希
bytes32 computedHash = keccak256(abi.encodePacked(
msg.sender,
_itemIds,
_bidValue,
_signature,
_secret
));
require(computedHash == sealedBid.commitment, "Invalid bid reveal");
// 验证签名
bytes32 messageHash = keccak256(abi.encodePacked(_itemIds, _bidValue));
address recovered = messageHash.toEthSignedMessageHash().recover(_signature);
require(recovered == msg.sender, "Invalid signature");
// 存储揭示的投标
revealedBids[msg.sender] = Bid({
bidder: msg.sender,
itemIds: _itemIds,
bidValue: _bidValue,
signature: _signature
});
sealedBid.revealed = true;
state = AuctionState.REVEALING;
emit BidRevealed(msg.sender, _itemIds, _bidValue);
}
// 结束拍卖并计算结果(仅所有者调用,通常在链下计算后提交结果)
function finalizeAuction(
address[] memory _winners,
uint256[][] memory _winnerItems,
uint256[] memory _payments
) external onlyOwner nonReentrant {
require(state == AuctionState.REVEALING || state == AuctionState.ENDED, "Auction not ready");
require(block.timestamp >= revealEndTime, "Reveal period not ended");
// 验证输入长度
require(_winners.length == _winnerItems.length, "Winners data mismatch");
require(_winners.length == _payments.length, "Payments mismatch");
winners = _winners;
// 验证并存储结果
for (uint i = 0; i < _winners.length; i++) {
require(revealedBids[_winners[i]].bidder != address(0), "Winner not a bidder");
winnerItemIds[_winners[i]] = _winnerItems[i];
winnerPayments[_winners[i]] = _payments[i];
}
state = AuctionState.ENDED;
emit AuctionEnded(_winners, getTotalRevenue());
// 开始结算
_settleAuction();
}
// 内部结算函数(已修复)
function _settleAuction() internal {
// 转移NFT给获胜者
for (uint i = 0; i < winners.length; i++) {
address winner = winners[i];
uint256[] memory itemIds = winnerItemIds[winner];
uint256 payment = winnerPayments[winner];
// 验证支付金额与保证金
require(address(this).balance >= payment, "Insufficient contract balance");
// 退还多余保证金(如果有)
SealedBid storage sealedBid = sealedBids[winner];
if (sealedBid.deposit > payment) {
uint256 refundAmount = sealedBid.deposit - payment;
// ✅ 已修复:使用 call 替代 transfer
(bool success, ) = payable(winner).call{value: refundAmount}("");
require(success, "Refund transfer failed");
}
// 转移物品
for (uint j = 0; j < itemIds.length; j++) {
Item storage item = items[itemIdToIndex[itemIds[j]]];
IERC1155(item.tokenContract).safeTransferFrom(
address(this),
winner,
item.tokenId,
item.amount,
""
);
}
}
// 将剩余资金转给所有者
uint256 remainingBalance = address(this).balance;
if (remainingBalance > 0) {
// ✅ 已修复:使用 call 替代 transfer
(bool success, ) = payable(owner()).call{value: remainingBalance}("");
require(success, "Owner withdrawal failed");
}
}
// 获取总收益
function getTotalRevenue() public view returns (uint256) {
uint256 total = 0;
for (uint i = 0; i < winners.length; i++) {
total += winnerPayments[winners[i]];
}
return total;
}
// 获取拍卖物品数量
function getItemsCount() external view returns (uint256) {
return items.length;
}
// 获取投标人数量
function getBiddersCount() external view returns (uint256) {
return biddersList.length;
}
// 紧急撤回未揭示的投标(已修复)
function emergencyWithdraw() external onlyOwner {
require(block.timestamp > revealEndTime + 7 days, "Too early");
for (uint i = 0; i < biddersList.length; i++) {
address bidder = biddersList[i];
if (!sealedBids[bidder].revealed && sealedBids[bidder].deposit > 0) {
uint256 depositAmount = sealedBids[bidder].deposit;
// ✅ 已修复:使用 call 替代 transfer
(bool success, ) = payable(bidder).call{value: depositAmount}("");
if (success) {
sealedBids[bidder].deposit = 0;
}
}
}
}
}
npx hardhat compile
module.exports = async function ({getNamedAccounts,deployments}) {
const firstAccount= (await getNamedAccounts()).firstAccount;
const {deploy,log} = deployments;
const GameToken=await deploy("GameToken",{
from:firstAccount,
args: [firstAccount],//参数
log: true,
})
console.log("gametoken合约",GameToken.address)
}
module.exports.tags = ["all","gametoken"];
module.exports=async ({getNamedAccounts,deployments})=>{
const {deploy,log} = deployments;
const {firstAccount,secondAccount} = await getNamedAccounts();
console.log("firstAccount",firstAccount)
const GameToken = await deploy("GameToken", {
from: firstAccount,
args: [firstAccount],//参数
log: true,
});
console.log("GameToken 合约地址:", GameToken.address);
const gameTokenContract = await ethers.getContractAt("GameToken", GameToken.address);
const _tokenIds=[1,2,3]
const _tokenAmounts=[10,10,10]
for (let i = 0; i < _tokenIds.length; i++) {
const balance = await gameTokenContract.balanceOf(firstAccount, _tokenIds[i]);
if (balance.toString() === "0") {
await gameTokenContract.mint(firstAccount, _tokenIds[i], _tokenAmounts[i], "0x");
log(`已铸造 tokenId ${_tokenIds[i]} 数量 ${_tokenAmounts[i]}`);
}
}
// 部署组合拍卖合约
const _tokenContracts = Array(3).fill(GameToken.address); // 自动填充相同地址
const _biddingPeriod=60*60//1小时
const CombinatorialAuction=await deploy("CombinatorialAuction",{
from:firstAccount,
args: [_tokenContracts,_tokenIds,_tokenAmounts,_biddingPeriod],//参数
log: true,
})
// 授权拍卖合约转移代币
const isApproved = await gameTokenContract.isApprovedForAll(
firstAccount,
CombinatorialAuction.address
);
if (!isApproved) {
await gameTokenContract.setApprovalForAll(CombinatorialAuction.address, true);
log("已授权拍卖合约转移代币");
}
console.log('组合拍卖合约',CombinatorialAuction.address)
};
module.exports.tags = ["all", "CombinatorialAuction"];
npx hardhat deploy --tags xxx,xxxx//(例如:gametoken,CombinatorialAuction)
# 注释:严格按照执行顺序,必须保证ERC1155多资产代币合约先部署
const { expect } = require("chai");
const { ethers, deployments } = require("hardhat");
const { time } = require("@nomicfoundation/hardhat-toolbox/network-helpers");
describe("CombinatorialAuction(组合拍卖) 完整流程测试", function () {
// 测试账户
let owner, bidder1, bidder2, bidder3;
let gameToken, auction;
// 拍卖参数
const TOKEN_IDS = [1, 2, 3];
const TOKEN_AMOUNTS = [10, 10, 10];
const BIDDING_PERIOD = 3600; // 1小时
beforeEach(async function () {
// 获取测试账户
[owner, bidder1, bidder2, bidder3] = await ethers.getSigners();
// 部署合约(使用您的部署脚本)
await deployments.fixture(["gametoken","CombinatorialAuction"]);
// 获取合约实例
gameToken = await ethers.getContract("GameToken");
auction = await ethers.getContract("CombinatorialAuction");
// 铸造代币给owner
for (let i = 0; i < TOKEN_IDS.length; i++) {
await gameToken.mint(owner.address, TOKEN_IDS[i], TOKEN_AMOUNTS[i], "0x");
}
// 授权拍卖合约
await gameToken.setApprovalForAll(auction.target, true);
// 验证初始状态
expect(await auction.state()).to.equal(0); // NOT_STARTED
});
it("应完成完整的组合拍卖流程", async function () {
console.log("\n=== 开始组合拍卖流程测试 ===");
// 1. 准备投标数据
console.log("\n1. 准备投标数据...");
// 投标者1:竞标物品0和1,出价 2 ETH
const bidder1Items = [0, 1];
const bidder1Value = ethers.parseEther("2");
// 投标者2:竞标物品1和2,出价 2.5 ETH
const bidder2Items = [1, 2];
const bidder2Value = ethers.parseEther("2.5");
// 投标者3:竞标所有物品,出价 4 ETH
const bidder3Items = [0, 1, 2];
const bidder3Value = ethers.parseEther("4");
// 2. 生成并提交密封投标
console.log("\n2. 提交密封投标...");
// 投标者1
const secret1 = ethers.id("secret1");
const messageHash1 = ethers.solidityPackedKeccak256(
["uint256[]", "uint256"],
[bidder1Items, bidder1Value]
);
const signature1 = await bidder1.signMessage(ethers.getBytes(messageHash1));
const commitment1 = ethers.solidityPackedKeccak256(
["address", "uint256[]", "uint256", "bytes", "bytes32"],
[bidder1.address, bidder1Items, bidder1Value, signature1, secret1]
);
await auction.connect(bidder1).submitSealedBid(commitment1, {
value: ethers.parseEther("2.5") // 支付保证金
});
console.log(` 投标者1提交密封投标,保证金: 2.5 ETH`);
// 投标者2
const secret2 = ethers.id("secret2");
const messageHash2 = ethers.solidityPackedKeccak256(
["uint256[]", "uint256"],
[bidder2Items, bidder2Value]
);
const signature2 = await bidder2.signMessage(ethers.getBytes(messageHash2));
const commitment2 = ethers.solidityPackedKeccak256(
["address", "uint256[]", "uint256", "bytes", "bytes32"],
[bidder2.address, bidder2Items, bidder2Value, signature2, secret2]
);
await auction.connect(bidder2).submitSealedBid(commitment2, {
value: ethers.parseEther("3")
});
console.log(` 投标者2提交密封投标,保证金: 3 ETH`);
// 投标者3
const secret3 = ethers.id("secret3");
const messageHash3 = ethers.solidityPackedKeccak256(
["uint256[]", "uint256"],
[bidder3Items, bidder3Value]
);
const signature3 = await bidder3.signMessage(ethers.getBytes(messageHash3));
const commitment3 = ethers.solidityPackedKeccak256(
["address", "uint256[]", "uint256", "bytes", "bytes32"],
[bidder3.address, bidder3Items, bidder3Value, signature3, secret3]
);
await auction.connect(bidder3).submitSealedBid(commitment3, {
value: ethers.parseEther("4.5")
});
console.log(` 投标者3提交密封投标,保证金: 4.5 ETH`);
// 验证投标提交成功
expect(await auction.getBiddersCount()).to.equal(3);
expect(await auction.state()).to.equal(1); // BIDDING
// 3. 推进时间到投标期结束
console.log("\n3. 等待投标期结束...");
await time.increase(BIDDING_PERIOD + 60);
// expect(await auction.state()).to.equal(1); // 仍为BIDDING状态
// 4. 揭示投标
console.log("\n4. 揭示投标...");
// 投标者1揭示
await auction.connect(bidder1).revealBid(
bidder1Items,
bidder1Value,
signature1,
secret1
);
console.log(` 投标者1揭示: 物品${bidder1Items},出价 ${ethers.formatEther(bidder1Value)} ETH`);
// 投标者2揭示
await auction.connect(bidder2).revealBid(
bidder2Items,
bidder2Value,
signature2,
secret2
);
console.log(` 投标者2揭示: 物品${bidder2Items},出价 ${ethers.formatEther(bidder2Value)} ETH`);
// 投标者3揭示
await auction.connect(bidder3).revealBid(
bidder3Items,
bidder3Value,
signature3,
secret3
);
console.log(` 投标者3揭示: 物品${bidder3Items},出价 ${ethers.formatEther(bidder3Value)} ETH`);
// expect(await auction.state()).to.equal(2); // REVEALING
// 5. 推进时间到揭示期结束
console.log("\n5. 等待揭示期结束...");
await time.increase(86400 + 60); // 1天
// 6. 所有者结束拍卖并结算
console.log("\n6. 结束拍卖并计算结果...");
// 假设链下计算结果:投标者3赢得所有物品(出价最高)
const winners = [bidder3.address];
const winnerItems = [[0, 1, 2]]; // 赢得所有物品
const payments = [bidder3Value]; // 支付出价金额
// 在结束拍卖前,需要将NFT转入拍卖合约
// for (let i = 0; i < TOKEN_IDS.length; i++) {
// await gameToken.safeBatchTransferFrom(
// owner.address,
// auction.target,
// Number(TOKEN_IDS[i]),
// Number(TOKEN_AMOUNTS[i]),
// "0x"
// );
// }
const tokenIds = TOKEN_IDS.map(id => parseInt(id));
const tokenAmounts = TOKEN_AMOUNTS.map(amount => parseInt(amount));
await gameToken.safeBatchTransferFrom(
owner.address,
auction.target,
tokenIds,
tokenAmounts,
"0x"
);
// 验证转移成功
for (let i = 0; i < TOKEN_IDS.length; i++) {
const balance = await gameToken.balanceOf(auction.target, TOKEN_IDS[i]);
console.log(` 拍卖合约持有 tokenId ${TOKEN_IDS[i]}: ${balance} 个`);
}
await auction.connect(owner).finalizeAuction(winners, winnerItems, payments);
console.log(` 拍卖结束,获胜者: ${bidder3.address}`);
console.log(` 总收入: ${ethers.formatEther(await auction.getTotalRevenue())} ETH`);
expect(await auction.state()).to.equal(3); // ENDED
// 7. 验证结果
console.log("\n7. 验证拍卖结果...");
// 验证代币已转移给获胜者
for (let i = 0; i < TOKEN_IDS.length; i++) {
const winnerBalance = await gameToken.balanceOf(bidder3.address, TOKEN_IDS[i]);
expect(winnerBalance).to.equal(TOKEN_AMOUNTS[i]);
console.log(` 投标者3获得 tokenId ${TOKEN_IDS[i]}: ${winnerBalance} 个`);
}
// 验证未获胜者没有获得代币
for (let i = 0; i < TOKEN_IDS.length; i++) {
const bidder1Balance = await gameToken.balanceOf(bidder1.address, TOKEN_IDS[i]);
expect(bidder1Balance).to.equal(0);
const bidder2Balance = await gameToken.balanceOf(bidder2.address, TOKEN_IDS[i]);
expect(bidder2Balance).to.equal(0);
}
// 验证资金处理
const contractBalance = await ethers.provider.getBalance(auction.target);
console.log(` 合约剩余余额: ${ethers.formatEther(contractBalance)} ETH`);
// 验证投标者1和2的保证金已退还
const bidder1BalanceAfter = await ethers.provider.getBalance(bidder1.address);
const bidder2BalanceAfter = await ethers.provider.getBalance(bidder2.address);
console.log(` 投标者1/2保证金已退还`);
// 验证所有者收到获胜者支付
const ownerBalanceIncrease = await ethers.provider.getBalance(owner.address);
console.log(` 所有者收到支付: ${ethers.formatEther(payments[0])} ETH`);
console.log("\n=== 测试完成 ===");
});
});
npx hardhat test ./test/xxx.js;//(例如:CombinatorialAuction.js)
至此,组合拍卖智能合约全流程实现已完成。该机制以"打包竞标、全有或全无、链下计算链上验证"为核心,通过密封投标-揭示模式、链下最优分配计算与批量资产结算保障链上多物品竞拍的效率与激励相容。开发基于OpenZeppelin构建ERC1155资产标准与安全架构,测试运用Hardhat完成组合投标、限时揭示及复杂分配边界验证,最终部署为数字资产批量交易提供策略灵活、可信执行的技术方案。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!