Auctions as Smart Contracts

Auctions as Smart Contracts

Part one of a series on the implementation of various auction formats as smart contracts. In this post, we'll be implementing an English auction in DAML, a smart contract language based on Haskell.

The word "auction" typically calls to mind bidders competing for an item by calling out ever-escalating prices, and an auctioneer urging them ever-higher. That is, however, but one form that an auction could take. At their heart, auctions are a multi-party mechanism for deciding how goods should be allocated, to whom, and for what price.

The structure and complexity of auctions varies greatly, from the simple version described above to the sprawling spectrum auctions that governments have started to use to allocate blocks of radio frequency across vast geographies.

Like other multi-party systems, auctions can be modeled as smart contracts, and doing so ensures that the rules of the auction are completely transparent and perfectly enforced.

In this series, we're going to implement a few different types of auctions as smart contracts. We'll start with an English auction, which involves a single item for sale, one seller, and multiple bidders (like a typical sale of rare art or an eBay listing). We'll be implementing this contract in DAML, but we'll explain the necessary concepts of the language as we go, so no DAML knowledge will be required to follow along.

We selected DAML because it's built on top of a robust foundation (Haskell: a pure, functional programming language), comes with great tooling, and is designed to make it easy to express complex multi-party relationships. To learn more about DAML, visit the documentation site and the developer education site which features interactive tutorials, videos, and online courses.

An English Auction Specification

We're going to model an auction where:

  • The auctioneer sets a reserve price or minimum opening bid
  • Bids must exceed the previous high bid by a certain amount
  • Bids are public
  • The auctioneer decides when the auction is over
  • At the close of the auction, the high bidder wins

There are other rules that we could model, but won't in this post, including ending the auction when a certain amount of time passes or lowering the price if the reserve price isn't met.

Modeling the Auction

Auction Data

In DAML, smart contracts are defined as templates that can be instantiated on the ledger, and that can subsequently be replaced by new instances of that contract. Each template must have a signatory, which is the party who can authorize the creation or replacement of the contract. Let's start with that:

template Auction
  with
    auctioneer : Party
  where
    signatory auctioneer

Our template is not particularly interesting yet. We can create an auction that consists solely of an auctioneer, and we've made that auctioneer the signatory. Next, let's add the item up for sale.

type Item = Text

template Auction
  with
    auctioneer : Party
    item : Item
  where
    signatory auctioneer

For simplicity, we've represented the item as a string of text here, which could be an identifier or description of the item. Next, let's add the other parameters governing the auction.

template Auction
  with
    auctioneer : Party
    item : Item
    reservePrice : Decimal -- The minimum bid
    increment : Decimal -- The minimum amount by which each bid must exceed the previous bid
  where
    signatory auctioneer

We've added the reserve price and the minimum bidding increment. We'll eventually be able to use those values to determine whether or not to accept incoming bids. But before we have bids, we need some bidders.

Bidders

Let's add some bidders to the auction.

template Auction
  with
    auctioneer : Party
    item : Item
    reservePrice : Decimal
    increment : Decimal
    bidders : Set Party -- The parties who can submit bids
  where
    signatory auctioneer
    observer bidders -- Bidders can see auctions that they are part of

We use the Set data structure to represent our collection of bidders instead of a list because each bidder should only appear in the collection once, and the order doesn't matter.

We've also given the bidders observer rights on the contract. In a normal English auction, most information is public: everyone knows the rules of the auction, the minimum price, who the other bidders are, and so on. Everyone who participates in the auction will be able to look up the contract and view its current data.

Note that we could have structured things differently: rather than embedding the set of bidders into this template, each bidder could be issued a contract that grants them the right to participate in a particular auction. In some cases, this may be critical to the design of the auction (e.g., if the set of bidders is going to change frequently or is very large, for example).

The last piece of data we need to capture is the state of the auction: who's the winning bidder and what is their bid?

template Auction
  with
    auctioneer : Party
    item : Item
    reservePrice : Decimal
    increment : Decimal
    bidders : Set Party
    winningBidder : Party -- We'll initially set this to the auctioneer
    winningBid : Decimal
  where
    signatory auctioneer
    observer bidders

One Template or Many?

Here, again, we're faced with some choices. We could capture bids in a separate template that refers to the auction contract (in a manner similar to a foreign key). To update a contract, the old copy of the contract is archived and a new one is created. So, every time there's a new winning bid, we'll end up creating a new instance of this auction contract and copying over all the data.

On the other hand, if we didn't embed the winner information in this contract, we'd have to perform some lookups/fetches to retrieve bid information. Which solution is more performant will depend a lot on the parameters of your auction:

  • how much data is being copied?
  • are there a large number of participants?
  • does that set need to change frequently?
  • how many lookups need to be performed?

In our case, we're going to assume a relatively small and fixed set of participants, as one might have in an auction for art.

Ensuring that Rules are Followed

Now that we've created a model for our data, we can begin to enforce our contract invariants. In other words, we can implement the rules of the auction.

We want to prevent auctions from being created with invalid data (and by "created" we mean initial creation and subsequent updates). For instance, we don't want auctions with negative reserve prices or negative bidding increments. We want auctions to have more than one bidder, and so on.

We can add an ensure clause to our template to check that we're creating a valid auction.

template Auction
  with
    auctioneer : Party
    item : Item
    reservePrice : Decimal
    increment : Decimal
    bidders : Set Party
    winningBidder : Party
    winningBid : Decimal
  where
    ensure
      and
        [ -- There must be at least 2 bidders:
          Set.size bidders > 1
          -- The auctioneer cannot bid:
        , auctioneer `Set.notMember` bidders
          -- The winning bidder must be one of the participants:
        , winningBidder == auctioneer || winningBidder `Set.member` bidders
          -- The reserve price cannot be negative:
        , reservePrice > 0.0
          -- All bids must be equal to or greater than the reserve price:
        , winningBidder == auctioneer || winningBid >= reservePrice
          -- The bidding increment must be positive:
        , increment > 0.0
        ]
    signatory auctioneer
    observer bidders

All of these checks must return True for an auction contract to be created.

One limitation of ensure clauses is that they can only perform a pure check of the data at hand at contract creation time. You cannot, in other words, look things up on the ledger (e.g., the values of other contracts). If you need to perform such lookups or impure checks, you'll need to move some of your validity logic out of the ensure clause and into the choices.

Placing Bids

Actions taken on smart contracts in DAML are called choices: choices allow you to modify contracts. Our bidders need the ability to place a bid. We'll define a choice on the auction contract that lets them do so.

    choice PlaceBid : ContractId Auction
      with
        bidder : Party
        bid : Decimal
      controller bidder
      do
        -- Check that this is a valid new bid:
        assertMsg "New bid must beat current bid by the increment amount" $
          bid >= winningBid + increment
        -- Create a new auction contract with the new bid info.
        -- This will only happen if the ensure checks also succeed.
        create this with
          winningBidder = bidder
          winningBid = bid

Let's go through this step by step. We've called our choice PlaceBid and given it a return type of ContractId Auction. Choices are, by default, consuming, meaning that they archive the existing contract when they are exercised. To update contract data, you archvie the contract and instantiate a new one with the new data. This choice will do just that, and will return the identifier of the new auction contract.

Our choice takes two arguments: a bidder and the price submitted by that bidder. The controller of the choice is party allowed to exercise this choice (in this case the bidder).

Before we can update the auction, we have to check whether the bid actually beats the previous winning bid by the required amount. This is similar to what we were doing with the ensure clause above, but this time we have access to the old auction data and the proposed update. Indeed, we have access to any data we can access on the ledger as well. In other words, the checks here can be impure, unlike the checks done by the ensure clause.

If that check succeeds, we create a new auction contract with the new bid information. Calling create also causes the ensure checks to be run on the new instance of the auction contract, so we know that those invariants will still hold. If any of the checks fail, the choice will not archive the current contract and the new contract won't be created.

Our auction template now looks like this:

template Auction
  with
    auctioneer : Party
    item : Item
    reservePrice : Decimal
    increment : Decimal
    bidders : Set Party
    winningBidder : Party
    winningBid : Decimal
  where
    ensure
      and
        [ -- There must be at least 2 bidders:
          Set.size bidders > 1
          -- The auctioneer cannot bid:
        , auctioneer `Set.notMember` bidders
          -- The winning bidder must be one of the participants:
        , winningBidder == auctioneer || winningBidder `Set.member` bidders
          -- The reserve price cannot be negative:
        , reservePrice > 0.0
          -- All bids must be equal to or greater than the reserve price:
        , winningBidder == auctioneer || winningBid >= reservePrice
          -- The bidding increment must be positive:
        , increment > 0.0
        ]
    signatory auctioneer
    observer bidders
    choice PlaceBid : ContractId Auction
      with
        bidder : Party
        bid : Decimal
      controller bidder
      do
        -- Check that this is a valid new bid:
        assertMsg "New bid must beat current bid by the increment amount" $
          bid >= winningBid + increment
        -- Create a new auction contract with the new bid info.
        -- This will only happen if the ensure checks also succeed.
        create this with
          winningBidder = bidder
          winningBid = bid

Closing the Auction

At some point, the auction must end. Some auctions end after a certain amount of time has elapsed, or based on some other criteria. Here, we're going to end the auction when the auctioneer decides it's over.

The Result Template

There are different ways we could represent the auction ending. We've opted to create another template which will record the results of an auction. On the ledger, this will mean that a completed auction is archived and an auction result record is created in its place.

template AuctionResult
  with
    auctioneer : Party
    item : Item
    winningBidder : Party
    winningBid : Decimal
  where
    signatory auctioneer, winningBidder

This contract records the final result of an auction. You'll notice that we've got two signatories on this contract: the auctioneer and the winning bidder. This is signifcant because the signatory has the power to create and archive a contract. By having multiple signatories, we ensure that neither the auctioneer nor the winning bidder can, without the others' consent, make any change to this contract.

Recording the Result

To actually record the result, we need a choice that the auctioneer can exercise.

    choice CloseAuction : ContractId AuctionResult
      controller auctioneer
      do
      create AuctionResult
        with
          auctioneer
          item
          winningBidder
          winningBid

Delegating Authority

This introduces a problem: the signatories on the AuctionResult include both the auctioneer and the winning bidder. The auctioneer cannot bind the winning bidder (i.e., cannot create a contract where the winning bidder is the signatory) without that bidder's authorization.

When the controller of a choice exercises that choice, they are able to act with the authority of all of the signatories of the contract. If the winning bidder were a signatory on the auction contract, the auctioneer would be have the authority to create an auction result with the winning bidder as a signatory as well.

template Auction
  with
    auctioneer : Party
    item : Item
    reservePrice : Decimal
    increment : Decimal
    bidders : Set Party
    winningBidder : Party
    winningBid : Decimal
  where
    ensure
      and
        [ -- There must be at least 2 bidders:
          Set.size bidders > 1
          -- The auctioneer cannot bid:
        , auctioneer `Set.notMember` bidders
          -- The winning bidder must be one of the participants:
        , winningBidder == auctioneer || winningBidder `Set.member` bidders
          -- The reserve price cannot be negative:
        , reservePrice > 0.0
          -- All bids must be equal to or greater than the reserve price:
        , winningBidder == auctioneer || winningBid >= reservePrice
          -- The bidding increment must be positive:
        , increment > 0.0
        ]
    signatory auctioneer, winningBidder
    observer bidders
    choice PlaceBid : ContractId Auction
      with
        bidder : Party
        bid : Decimal
      controller bidder
      do
        -- Check that this is a valid new bid:
        assertMsg "New bid must beat current bid by the increment amount" $
          bid >= winningBid + increment
        -- Create a new auction contract with the new bid info.
        -- This will only happen if the ensure checks also succeed.
        create this with
          winningBidder = bidder
          winningBid = bid
    choice CloseAuction : ContractId AuctionResult
      controller auctioneer
      do
      create AuctionResult
        with
          auctioneer
          item
          winningBidder
          winningBid

Testing the Auction

DAML comes with a vscode plugin that allows smart contracts to be tested inside of the editor. We'll use that capability to test our auction.

First, we need to create the parties who will participate in our auction:

test : Script ()
test = script do
  -- Create parties
  auctioneer <- allocateParty "Amari"
  blair <- allocateParty "Blair"
  corey <- allocateParty "Corey"
  dakota <- allocateParty "Dakota"
  eden <- allocateParty "Eden"

Next, let's create an item for sale:

  -- Create an item that will go up for auction
  let item = "Salvator Mundi (Leonardo)"

We now have everything we need to create the auction. We'll store the auction's contract identifier so that we can use it to exercise choices on the auction:

  -- Create the auction
  contract0 <- submit auctioneer do
    createCmd Auction with
      auctioneer
      bidders = Set.fromList [blair, corey, dakota, eden]
      item
      increment = 1000.0
      reservePrice = 10000.0
      winningBidder = auctioneer
      winningBid = 0.0

Let's try submitting a bid:

  contract1 <- submit blair $ exerciseCmd contract0 PlaceBid
    with
        bidder = blair
        bid = 15000.0
  pure ()

Clicking on "script results" in vscode should give you output that looks like this:
DAML1_script1

This output shows us that the initial contract was archived, and that a new contract with Blair as the winning bidder has taken its place, with the winning bid amount updated. Off to the right, the output shows the current signatories and observers on the contract.

Note that we bound the result of Blair's PlaceBid to contract1. This is because exercising the PlaceBid choice has archived the old contract and we now have a new contract identifier that must be used for subsequent choices.

Next, let's make sure our rules are being enforced:

    submitMustFail corey $ exerciseCmd contract1 PlaceBid
    with 
      bidder = dakota
      bid = 21000.0

The problem here is that Corey doesn't have the authority to submit a bid on behalf of Dakota. And, indeed it does fail. If you change the submitMustFail to submit you'll get the following error:

Script execution failed on commit at English:95:3:
  0: exercise of PlaceBid in English:Auction at DA.Internal.Template.Functions:218:3
     failed due to a missing authorization from 'Dakota'

Here's a more complete test script that tests various properties and eventually closes the auction:

test : Script ()
test = script do
  -- Create parties
  auctioneer <- allocateParty "Amari"
  blair <- allocateParty "Blair"
  corey <- allocateParty "Corey"
  dakota <- allocateParty "Dakota"
  eden <- allocateParty "Eden"
  frankie <- allocateParty "Frankie" -- Frankie is not a bidder
    -- Create an item that will go up for auction
  let item = "Salvator Mundi (Leonardo)"
    -- Create the auction
  contract0 <- submit auctioneer do
    createCmd Auction with
      auctioneer
      bidders = Set.fromList [blair, corey, dakota, eden]
      item
      increment = 1000.0
      reservePrice = 10000.0
      winningBidder = auctioneer
      winningBid = 0.0

  -- Initial bid must be >= reserve
  submitMustFail eden do
    exerciseCmd contract0 PlaceBid with
      bidder = eden
      bid = 999.0

  contract1 <- submit blair $ exerciseCmd contract0 PlaceBid
    with
      bidder = blair
      bid = 15000.0

  -- Bid must be placed by someone authorized to
  submitMustFail corey $ exerciseCmd contract1 PlaceBid
    with 
      bidder = dakota
      bid = 21000.0

  -- Bid must be placed by participant
  submitMustFail frankie $ exerciseCmd contract1 PlaceBid
    with 
      bidder = frankie
      bid = 25000.0

  -- Bid must beat current bid by at least increment
  submitMustFail eden $ exerciseCmd contract1 PlaceBid
    with
      bidder = eden
      bid = 15001.0

  contract2 <- submit dakota $ exerciseCmd contract1 PlaceBid
    with
      bidder = dakota
      bid = 16000.0

  submit auctioneer $
    exerciseCmd contract2 CloseAuction
  pure ()

Running this script will give you the following output on the auction contract:
DAML1_script2

And we have an auction result:
DAML1_script3

Bonus: Using Contract Keys

Keeping track of all those contract identifiers can be a bit cumbersome. Luckily, we can give our auction a key that we can then use to look it up even after choices have been exercised and the contract identifier has changed.

We have to create a unique key using the data in the contract and specify a party that is the maintainer of that key.

    key (auctioneer, item) : (Party, Item)
    maintainer key._1

The restriction that this will enforce is that each the auctioneer can only auction off a given item once simultaneously, which seems rather sensible.

Our auction template looks like this now:

template Auction
  with
    auctioneer : Party
    item : Item
    reservePrice : Decimal
    increment : Decimal
    bidders : Set Party
    winningBidder : Party
    winningBid : Decimal
  where
    ensure
      and
        [ -- There must be at least 2 bidders:
          Set.size bidders > 1
          -- The auctioneer cannot bid:
        , auctioneer `Set.notMember` bidders
          -- The winning bidder must be one of the participants:
        , winningBidder == auctioneer || winningBidder `Set.member` bidders
          -- The reserve price cannot be negative:
        , reservePrice > 0.0
          -- All bids must be equal to or greater than the reserve price:
        , winningBidder == auctioneer || winningBid >= reservePrice
          -- The bidding increment must be positive:
        , increment > 0.0
        ]
    signatory auctioneer, winningBidder
    observer bidders
    key (auctioneer, item) : (Party, Item)
    maintainer key._1
    choice PlaceBid : ContractId Auction
      with
        bidder : Party
        bid : Decimal
      controller bidder
      do
        -- Check that this is a valid new bid:
        assertMsg "New bid must beat current bid by the increment amount" $
          bid >= winningBid + increment
        -- Create a new auction contract with the new bid info.
        -- This will only happen if the ensure checks also succeed.
        create this with
          winningBidder = bidder
          winningBid = bid
    choice CloseAuction : ContractId AuctionResult
      controller auctioneer
      do
      create AuctionResult
        with
          auctioneer
          item
          winningBidder
          winningBid

Now we can change our scripts so that they don't need the explicit contract identifier:

  let key = (auctioneer, item)

  submit blair $ exerciseByKeyCmd @Auction key PlaceBid
    with
      bidder = blair
      bid = 15000.0

Our test script now looks like this:

test : Script ()
test = script do
  -- Create parties
  auctioneer <- allocateParty "Amari"
  blair <- allocateParty "Blair"
  corey <- allocateParty "Corey"
  dakota <- allocateParty "Dakota"
  eden <- allocateParty "Eden"
  frankie <- allocateParty "Frankie" -- Frankie is not a bidder
    -- Create an item that will go up for auction
  let item = "Salvator Mundi (Leonardo)"
    -- Create the auction
  submit auctioneer do
    createCmd Auction with
      auctioneer
      bidders = Set.fromList [blair, corey, dakota, eden]
      item
      increment = 1000.0
      reservePrice = 10000.0
      winningBidder = auctioneer
      winningBid = 0.0

  let key = (auctioneer, item)

  -- Initial bid must be >= reserve
  submitMustFail eden do
    exerciseByKeyCmd @Auction key PlaceBid with
      bidder = eden
      bid = 999.0

  submit blair $ exerciseByKeyCmd @Auction key PlaceBid
    with
      bidder = blair
      bid = 15000.0

  -- Bid must be placed by someone authorized to
  submitMustFail corey $ exerciseByKeyCmd @Auction key PlaceBid
    with 
      bidder = dakota
      bid = 21000.0

  -- Bid must be placed by participant
  submitMustFail frankie $ exerciseByKeyCmd @Auction key PlaceBid
    with 
      bidder = frankie
      bid = 25000.0

  -- Bid must beat current bid by at least increment
  submitMustFail eden $ exerciseByKeyCmd @Auction key PlaceBid
    with
      bidder = eden
      bid = 15001.0

  submit dakota $ exerciseByKeyCmd @Auction key PlaceBid
    with
      bidder = dakota
      bid = 16000.0

  submit auctioneer $
    exerciseByKeyCmd @Auction key CloseAuction

  pure ()

Bonus 2: A User Interface

DAML comes with libraries to support typescript-react frontend development, and a codegen tool that creates typescript representations of your templates and choices. Using those tools, you can build a frontend that interacts with your smart contract.

A UI showing a live auction.