Skip to content
LogoLogo

Dutch auction

The price falls linearly from a start price to a floor. The first buyer to pay the current price wins, and any overpayment is refunded in the same transaction.

dutch-auction.lithe
(* The price falls linearly from a start price to a floor. The first buyer at or
   above the current price wins, and any overpayment is returned. *)
 
import "std/address";
import "std/evm";
 
error AlreadyStarted;
error Sold;
error TooLow of record {
  paid : wei;
  price : wei;
}
 
event Bought of record {
  indexed buyer : address;
  price : wei;
}
 
storage is record {
  seller : address;
  startPrice : wei;
  floor : wei;
  startedAt : timestamp;
  duration : uint;
  sold : bool;
}
 
external function start(const startPrice : wei; const floor : wei; const duration : uint)
  raises AlreadyStarted
is {
  assert(storage.seller = Address.zero, AlreadyStarted);
  storage.seller := Evm.sender();
  storage.startPrice := startPrice;
  storage.floor := floor;
  storage.startedAt := Evm.blockTimestamp();
  storage.duration := duration;
}
 
view function currentPrice() : wei is {
  const elapsed : uint := Evm.blockTimestamp() - storage.startedAt;
  var price : wei := storage.floor;
  if elapsed < storage.duration then {
    const spread : uint := uint(storage.startPrice - storage.floor);
    price := storage.startPrice - wei(spread * elapsed / storage.duration);
  }
} with price
 
external view function price() : wei is
  currentPrice()
 
external payable nonreentrant function buy()
  raises Sold | TooLow
is {
  assert(not storage.sold, Sold);
  const price : wei := currentPrice();
  assert(Evm.value() >= price, TooLow { paid = Evm.value(); price = price; });
  storage.sold := true;
  emit Bought { buyer = Evm.sender(); price = price; };
  defer Evm.transfer(storage.seller, price);
  if Evm.value() > price then {
    defer Evm.transfer(Evm.sender(), Evm.value() - price);
  }
}

Both transfers are deferred, and they run in order after sold is set: first the seller is paid, then the buyer is refunded.