以下简单的拍卖合约的总体思路是每个人都可以在投标期内发送他们的出价。 出价已经包含了资金/以太币,来将投标人与他们的投标绑定。 如果最高出价提高了(被其他出价者的出价超过),之前出价最高的出价者可以拿回她的钱。 在投标期结束后,受益人需要手动调用合约来接收他的钱 - 合约不能自己激活接收。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.7.0;
contract SimpleAuction {
// 拍卖的参数。
address payable public beneficiary;
// 时间是unix的绝对时间戳(自1970-01-01以来的秒数)
// 或以秒为单位的时间段。
uint public auctionEnd;
// 拍卖的当前状态
address public highestBidder;
uint public highestBid;
//可以取回的之前的出价
mapping(address => uint) pendingReturns;
// 拍卖结束后设为 true,将禁止所有的变更
bool ended;
// 变更触发的事件
event HighestBidIncreased(address bidder, uint amount);
event AuctionEnded(address winner, uint amount);
// 以下是所谓的 natspec 注释,可以通过三个斜杠来识别。
// 当用户被要求确认交易时将显示。
/// 以受益者地址 `_beneficiary` 的名义,
/// 创建一个简单的拍卖,拍卖时间为 `_biddingTime` 秒。
constructor(
uint _biddingTime,
address payable _beneficiary
) {
beneficiary = _beneficiary;
auctionEnd = block.timestamp + _biddingTime;
}
/// 对拍卖进行出价,具体的出价随交易一起发送。
/// 如果没有在拍卖中胜出,则返还出价。
function bid() public payable {
// 参数不是必要的。因为所有的信息已经包含在了交易中。
// 对于能接收以太币的函数,关键字 payable 是必须的。
// 如果拍卖已结束,撤销函数的调用。
require(block.timestamp <= auctionEnd,"Auction already ended.");
// 如果出价不够高,返还你的钱
require(msg.value > highestBid,"There already is a higher bid.");
if (highestBid != 0) {
// 返还出价时,简单地直接调用 highestBidder.send(highestBid) 函数,
// 是有安全风险的,因为它有可能执行一个非信任合约。
// 更为安全的做法是让接收方自己提取金钱。
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
emit HighestBidIncreased(msg.sender, msg.value);
}
/// 取回出价(当该出价已被超越)
function withdraw() public returns (bool) {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
// 这里很重要,首先要设零值。
// 因为,作为接收调用的一部分,
// 接收者可以在 `send` 返回之前,重新调用该函数。
pendingReturns[msg.sender] = 0;
if (!payable(msg.sender).send(amount)) {
// 这里不需抛出异常,只需重置未付款
pendingReturns[msg.sender] = amount;
return false;
}
}
return true;
}
/// 结束拍卖,并把最高的出价发送给受益人
function auctionEnded() public {
// 对于可与其他合约交互的函数(意味着它会调用其他函数或发送以太币),
// 一个好的指导方针是将其结构分为三个阶段:
// 1. 检查条件
// 2. 执行动作 (可能会改变条件)
// 3. 与其他合约交互
// 如果这些阶段相混合,其他的合约可能会回调当前合约并修改状态,
// 或者导致某些效果(比如支付以太币)多次生效。
// 如果合约内调用的函数包含了与外部合约的交互,
// 则它也会被认为是与外部合约有交互的。
// 1. 条件
require(block.timestamp >= auctionEnd, "Auction not yet ended.");
require(!ended, "auctionEnd has already been called.");
// 2. 生效
ended = true;
emit AuctionEnded(highestBidder, highestBid);
// 3. 交互
beneficiary.transfer(highestBid);
}
}
上面的公开拍卖接下来将被扩展为一个秘密竞拍。 秘密竞拍的好处是在投标结束前不会有时间压力。 在一个透明的计算平台上进行秘密竞拍听起来像是自相矛盾,但密码学可以实现它。
在 投标期间 ,投标人实际上并没有发送她的出价,而只是发送一个哈希版本的出价。 由于目前几乎不可能找到两个(足够长的)值,其哈希值是相等的,因此投标人可通过该方式提交报价。 在投标结束后,投标人必须公开他们的出价:他们不加密的发送他们的出价,合约检查出价的哈希值是否与投标期间提供的相同。
另一个挑战是如何使拍卖同时做到 绑定和秘密 : 唯一能阻止投标者在她赢得拍卖后不付款的方式是,让她将钱连同出价一起发出。 但由于资金转移在以太坊中不能被隐藏,因此任何人都可以看到转移的资金。
下面的合约通过接受任何大于最高出价的值来解决这个问题。 当然,因为这只能在披露阶段进行检查,有些出价可能是 无效 的, 并且,这是故意的(与高出价一起,它甚至提供了一个明确的标志来标识无效的出价): 投标人可以通过设置几个或高或低的无效出价来迷惑竞争对手。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract BlindAuction {
struct Bid {
bytes32 blindedBid;
uint deposit;
}
address payable public beneficiary;
uint public biddingEnd;
uint public revealEnd;
bool public ended;
mapping(address => Bid[]) public bids;
address public highestBidder;
uint public highestBid;
// 可以取回的之前的出价
mapping(address => uint) pendingReturns;
event AuctionEnded(address winner, uint highestBid);
/// 使用 modifier 可以更便捷的校验函数的入参。
/// `onlyBefore` 会被用于后面的 `bid` 函数:
/// 新的函数体是由 modifier 本身的函数体,并用原函数体替换 `_;` 语句来组成的。
modifier onlyBefore(uint _time) { require(block.timestamp < _time); _; }
modifier onlyAfter(uint _time) { require(block.timestamp > _time); _; }
constructor(
uint _biddingTime,
uint _revealTime,
address payable _beneficiary
) public {
beneficiary = _beneficiary;
biddingEnd = block.timestamp + _biddingTime;
revealEnd = biddingEnd + _revealTime;
}
/// 可以通过 `_blindedBid` = keccak256(value, fake, secret)
/// 设置一个秘密竞拍。
/// 只有在出价披露阶段被正确披露,已发送的以太币才会被退还。
/// 如果与出价一起发送的以太币至少为 “value” 且 “fake” 不为真,则出价有效。
/// 将 “fake” 设置为 true ,然后发送满足订金金额但又不与出价相同的金额是隐藏实际出价的方法。
/// 同一个地址可以放置多个出价。
function bid(bytes32 _blindedBid) public payable onlyBefore(biddingEnd) {
bids[msg.sender].push(Bid({
blindedBid: _blindedBid,
deposit: msg.value
}));
}
/// 披露你的秘密竞拍出价。
/// 对于所有正确披露的无效出价以及除最高出价以外的所有出价,你都将获得退款。
function reveal(
uint[] _values,
bool[] _fake,
bytes32[] _secret
) public onlyAfter(biddingEnd) onlyBefore(revealEnd) {
uint length = bids[msg.sender].length;
require(_values.length == length);
require(_fake.length == length);
require(_secret.length == length);
uint refund;
for (uint i = 0; i < length; i++) {
Bid storage bid = bids[msg.sender][i];
(uint value, bool fake, bytes32 secret) =
(_values[i], _fake[i], _secret[i]);
if (bid.blindedBid != keccak256(value, fake, secret)) {
// 出价未能正确披露
// 不返还订金
continue;
}
refund += bid.deposit;
if (!fake && bid.deposit >= value) {
if (placeBid(msg.sender, value))
refund -= value;
}
// 使发送者不可能再次认领同一笔订金
bid.blindedBid = bytes32(0);
}
msg.sender.transfer(refund);
}
// 这是一个 "internal" 函数, 意味着它只能在本合约(或继承合约)内被调用
function placeBid(address bidder, uint value) internal returns (bool success) {
if (value <= highestBid) {
return false;
}
if (highestBidder != address(0)) {
// 返还之前的最高出价
pendingReturns[highestBidder] += highestBid;
}
highestBid = value;
highestBidder = bidder;
return true;
}
/// 取回出价(当该出价已被超越)
function withdraw() public {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
// 这里很重要,首先要设零值。
// 因为,作为接收调用的一部分,
// 接收者可以在 `transfer` 返回之前重新调用该函数。(可查看上面关于‘条件 -> 影响 -> 交互’的标注)
pendingReturns[msg.sender] = 0;
msg.sender.transfer(amount);
}
}
/// 结束拍卖,并把最高的出价发送给受益人
function auctionEnd() public onlyAfter(revealEnd) {
require(!ended);
emit AuctionEnded(highestBidder, highestBid);
ended = true;
beneficiary.transfer(highestBid);
}
}
当前,远程购买商品需要多个需要彼此信任的各方。最简单的配置包括卖方和买方。买方希望从卖方那里收到一件商品,而卖方则希望从中获得金钱(或等值货币)作为回报。有问题的部分是这里的货物:无法确定物品是否到达买方。
有多种方法可以解决此问题,但是所有方法都无法实现。在下面的示例中,双方都必须将托管商品的价值加倍放入合同中。一旦发生这种情况,这笔钱将被锁定在合同内,直到买家确认他们已收到该物品为止。在那之后,买主被退还了价值(他们的保证金的一半),而卖主得到了价值的三倍(他们的保证金加上价值)。这背后的想法是,双方都有解决问题的动力,否则他们的钱将永远被锁定。
该合同当然不能解决问题,但是概述了如何在合同中使用类似于状态机的构造。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Purchase {
//押金
uint public value;
//买家
address payable public seller;
//卖家
address payable public buyer;
//创建、锁定、释放、非活动
enum State { Created, Locked, Release, Inactive }
State public state;
//定义一个bool 类型的 condition 修饰器
modifier condition(bool _condition) {
require(_condition);
_;
}
modifier onlyBuyer() {
require(msg.sender == buyer,"确认买家身份");
_;
}
modifier onlySeller() {
require(msg.sender == seller,"确认卖家身份");
_;
}
modifier inState(State _state) {
require(state == _state,"无效状态");
// _; 代表执行调用修饰器的函数体
_;
}
//非活动状态
event Aborted();
//购买确认
event PurchaseConfirmed();
//收货确认
event ItemReceived();
//卖方归还
event SellerRefunded();
//确保 `msg.value` 是一个偶数。
//如果它是一个奇数,则它将被截断。
//通过乘法检查它不是奇数。
constructor() payable {
seller = payable(msg.sender);
value = msg.value / 2;
require((2 * value) == msg.value, "价值必须相等");
}
///中止购买并回收以太币。
///只能在合约被锁定之前由卖家调用。
function abort() public onlySeller inState(State.Created) {
emit Aborted();
state = State.Inactive;
seller.transfer(address(this).balance);
}
/// 买家确认购买。
/// 交易必须包含 `2 * value` 个以太币。
/// 以太币会被锁定,直到 confirmReceived 被调用。
function confirmPurchase() public inState(State.Created)
condition(msg.value == (2 * value)) payable {
emit PurchaseConfirmed();
buyer = payable(msg.sender);
state = State.Locked;
}
/// 确认你(买家)已经收到商品。
/// 这会释放被锁定的以太币。
function confirmReceived() public onlyBuyer inState(State.Locked) {
emit ItemReceived();
// 重要的是首先改变状态,
// 否则,使用下面的“send”调用的合约可以在这里再次调用。
state = State.Release;
buyer.transfer(value);
}
/// 此功能退款给卖家,即
/// 归还卖家的锁定资金。
function refundSeller() public onlySeller inState(State.Release) {
emit SellerRefunded();
// 重要的是首先改变状态,
// 否则,使用下面的“send”调用的合约可以在这里再次调用。
state = State.Inactive;
// 将卖家压入的 2*value 与买家购买商品的 1*value 一起发送给卖家
seller.transfer(3 * value);
}
}