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 choice
s.
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:
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:
And we have an auction result:
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.
Comments ()