Skip to content
LogoLogo

English auction

Each bid must beat the current highest. Outbid bidders withdraw their refunds themselves.

english-auction.lithe
(* Highest bid wins. Outbid bidders withdraw their own refunds: sending them
   inside `bid` would let one bidder's failing receiver block everyone else. *)
 
import "std/address";
import "std/evm";
 
error NotSeller;
error AlreadyStarted;
error NotStarted;
error Ended;
error NotEnded;
error BidTooLow of record {
  bid : wei;
  highest : wei;
}
error AlreadySettled;
 
event Bid of record {
  indexed bidder : address;
  amount : wei;
}
 
event Settled of record {
  indexed winner : address;
  amount : wei;
}
 
storage is record {
  seller : address;
  endsAt : timestamp;
  highestBidder : address;
  highestBid : wei;
  settled : bool;
  refunds : map(address, wei);
}
 
external function start(const reserve : wei; const duration : uint)
  raises AlreadyStarted
is {
  assert(storage.seller = Address.zero, AlreadyStarted);
  storage.seller := Evm.sender();
  storage.highestBid := reserve;
  storage.endsAt := Evm.blockTimestamp() + duration;
}
 
external payable function bid()
  raises NotStarted | Ended | BidTooLow
is {
  assert(storage.seller <> Address.zero, NotStarted);
  assert(Evm.blockTimestamp() < storage.endsAt, Ended);
  assert(
    Evm.value() > storage.highestBid,
    BidTooLow { bid = Evm.value(); highest = storage.highestBid; }
  );
  if storage.highestBidder <> Address.zero then {
    storage.refunds[storage.highestBidder] :=
      storage.refunds[storage.highestBidder] + storage.highestBid;
  }
  storage.highestBidder := Evm.sender();
  storage.highestBid := Evm.value();
  emit Bid { bidder = Evm.sender(); amount = Evm.value(); };
}
 
external nonreentrant function withdraw() is {
  const amount : wei := storage.refunds[Evm.sender()];
  storage.refunds[Evm.sender()] := 0wei;
  defer Evm.transfer(Evm.sender(), amount);
}
 
external nonreentrant function settle()
  raises NotEnded | AlreadySettled
is {
  assert(Evm.blockTimestamp() >= storage.endsAt, NotEnded);
  assert(not storage.settled, AlreadySettled);
  storage.settled := true;
  emit Settled { winner = storage.highestBidder; amount = storage.highestBid; };
  if storage.highestBidder <> Address.zero then {
    defer Evm.transfer(storage.seller, storage.highestBid);
  }
}

Refunds are pulled, not pushed. If bid sent the previous bidder's refund itself, a bidder whose receiver always reverts could block every later bid.