跳到主要内容

构建密封拍卖

密封拍卖是机密智能合约的旗舰用例之一。公开 EVM 上需要两阶段 commit-reveal,且有已知问题。OXN 上可以做成单笔提交、金额到揭示前一直隐藏。

设计

  • 投标阶段。 用户提交出价。每份出价加密存储;任何人——拍卖方、其他竞拍者、观察者——在截止前都读不到。
  • 揭示阶段。 截止后合约从自己的加密状态里确定赢家。中标金额与赢家公开;其他出价保持机密。
  • 结算。 赢家付款;物品转移。

完整合约

contracts/SealedBidAuction.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract SealedBidAuction {
struct Bid {
uint256 amount;
bool exists;
}

address public immutable auctioneer;
uint256 public immutable bidDeadline;
uint256 public immutable revealDeadline;

// 出价加密存储;enclave 外无人可读
mapping(address => Bid) private bids;
address[] private bidders;

// 揭示后公开
address public winner;
uint256 public winningBid;
bool public revealed;

event AuctionCreated(address auctioneer, uint256 bidDeadline, uint256 revealDeadline);
event BidReceived(address indexed bidder); // 不 emit 金额
event AuctionResolved(address indexed winner, uint256 winningBid);

constructor(uint256 _bidDuration, uint256 _revealBuffer) {
auctioneer = msg.sender;
bidDeadline = block.timestamp + _bidDuration;
revealDeadline = bidDeadline + _revealBuffer;
emit AuctionCreated(auctioneer, bidDeadline, revealDeadline);
}

// 提交密封出价——金额在 calldata 与存储中加密
function submitBid() external payable {
require(block.timestamp < bidDeadline, "bidding closed");
require(!bids[msg.sender].exists, "already bid");

bids[msg.sender] = Bid({ amount: msg.value, exists: true });
bidders.push(msg.sender);

emit BidReceived(msg.sender); // 事件数据本就加密
}

// 揭示赢家。截止后任何人可调。
function reveal() external {
require(block.timestamp >= revealDeadline, "not yet");
require(!revealed, "already revealed");

uint256 highest = 0;
address highBidder = address(0);
for (uint256 i = 0; i < bidders.length; i++) {
uint256 amount = bids[bidders[i]].amount;
if (amount > highest) {
highest = amount;
highBidder = bidders[i];
}
}

winner = highBidder;
winningBid = highest;
revealed = true;

emit AuctionResolved(winner, winningBid);
}

// 输家揭示后可取回
function withdrawBid() external {
require(revealed, "not revealed yet");
require(msg.sender != winner, "winner cannot withdraw");
require(bids[msg.sender].exists, "no bid");

uint256 amount = bids[msg.sender].amount;
delete bids[msg.sender];
payable(msg.sender).transfer(amount);
}

// 拍卖方领取中标金额
function claim() external {
require(msg.sender == auctioneer, "not auctioneer");
require(revealed, "not revealed yet");

uint256 amount = winningBid;
winningBid = 0;
payable(auctioneer).transfer(amount);
}

// 竞拍者可读自己的出价(签名查询)
function myBid() external view returns (uint256) {
require(bids[msg.sender].exists, "no bid");
return bids[msg.sender].amount;
}
}

机密性达成什么

  • 竞拍者互相看不到对方出价 拍卖期间。公开 EVM 上大家都从 msg.value 看到金额——如果没有 OXN 的加密,本合约也会可见的。

等等——msg.value 在 OXN 上也是链上可见的。这是真问题。让我修订设计。

设计修订:用授权,不用 msg.value

因为外层交易的 msg.value 可见,把出价金额存进 msg.value 会泄漏。更干净的设计把出价改成对代币的授权,加密持有:

// 替代:ERC-20 授权形式的出价
mapping(address => uint256) private bidAmount;

function submitBid(uint256 amount) external {
require(block.timestamp < bidDeadline, "closed");
require(!bids[msg.sender].exists, "already bid");

// amount 在 calldata 里加密
// 通过授权拉取——合约持有至结算
token.transferFrom(msg.sender, address(this), amount);

bids[msg.sender] = Bid({ amount: amount, exists: true });
bidders.push(msg.sender);
emit BidReceived(msg.sender);
}

要让它工作,用户先给拍卖合约一个最大额授权,实际出价金额在加密 calldata 里,不在 msg.value 里。

测试

test/auction.integration.js
describe("SealedBidAuction [integration]", function () {
this.timeout(120_000);

let auction, alice, bob, carol;

before(async function () {
[alice, bob, carol] = await ethers.getSigners();
const Auction = await ethers.getContractFactory("SealedBidAuction");
auction = await Auction.deploy(3600, 300, { gasLimit: 3_000_000n }); // 1 小时投标,5 分钟揭示
await auction.waitForDeployment();
});

it("keeps bids private until reveal", async function () {
await auction.connect(alice).submitBid({ value: ethers.parseEther("1.0"), gasLimit: 500_000n });
await auction.connect(bob).submitBid({ value: ethers.parseEther("1.5"), gasLimit: 500_000n });
await auction.connect(carol).submitBid({ value: ethers.parseEther("1.2"), gasLimit: 500_000n });

// 揭示前,myBid() 只返回自己
expect(await auction.connect(alice).myBid()).to.equal(ethers.parseEther("1.0"));
expect(await auction.connect(bob).myBid()).to.equal(ethers.parseEther("1.5"));

// 赢家未知
expect(await auction.winner()).to.equal(ethers.ZeroAddress);
});

it("determines winner after reveal", async function () {
// 真实 OXN 上不能快进时间。
// 本测试要么等真实时间,要么设计成接受手动揭示触发。
});
});

部署与集成清单

  • 用合理的时长部署(避免测试用超小窗口)。
  • 给测试竞拍者充足余额。
  • 测试 myBid() 的签名查询路径。
  • 考虑边界情况:无人出价、平局、拍卖方未领取。

下一步