1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
|
title: How to Build a Decentralized Auction
date: June 24, 2020
---
<p><em>
this post assumes familiarity with the concept of smart contracts and
uses Ethereum and Solidity for concrete examples
</em></p>
<h3>Multiunit auctions</h3>
<p>
Multiunit auctions are a generic financial primitive that could be useful in
various decentralized financial systems. I'd like to present an alternative to
the Dutch auction that may be more suitable in certain situations.
</p>
<h3>Designing an auction smart contract</h3>
<p>
Let's first define the basic behavior we want from our auction. Then we'll be
able to see which steps would break if implemented naively, and how to address
those problems. Let's frame it in the context of a currency auction, where
currency <strong>A</strong> is being sold for currency <strong>B</strong>:
<ol>
<li>
The auctioneer decides he wants to sell a pot of <em>x</em> units of
<strong>A</strong>.
</li>
<li>
Users submit bids that say "I want to buy <em>y</em> units of
<strong>A</strong> for <em>z</em> units of <strong>B</strong>."
</li>
<li>
We go through the bids from highest <em>z</em>/<em>y</em> ratio to
lowest, filling them as we go. We stop if the sum of paid out
<em>y</em>s reaches the size of the pot <em>x</em>.
<a href='#note-1' id='ref-1'><sup>1</sup></a>
</li>
</ol>
A higher <em>z</em>/<em>y</em> ratio means a higher price (in
<strong>B</strong>) is being offered by the user, so step 3 ensures that the
winners of the auction are the highest bidders, as we'd expect.
</p>
<p>
To make this all a little more concrete, let's define an interface we might
expect from an auction smart contract:
<pre>
interface IAuction {
function start(uint pot);
function bid(uint sellAmount, uint buyAmount) returns (uint bidId);
function end();
function fill(uint bidId);
}
</pre>
</p>
<p>
The above is very similar to
<a href='https://solidity.readthedocs.io/en/v0.5.11/solidity-by-example.html#blind-auction'>
an example from the Solidity tutorial
</a>
showing how to build a single unit auction. The big difference is that when
making a bid, we specify how much we're willing to pay <em>and</em> how many
units of the auctioned asset we're bidding
on.<a href='#note-2' id='ref-2'><sup>2</sup></a>
</p>
<p>
The problem, as hinted at earlier, is that we can't just sort the list of bids
and go through it, bid by bid, when implementing step 3 of our protocol. Sorting
is an <em>O</em>(<em>n</em>log<em>n</em>) operation, and even the linear scan
would greatly limit the allowable number of participants in an auction given
block gas limits.
</p>
<h3>Sorting</h3>
<p>
The solution to this problem, as is often the case in computer science, comes in
the shape of a tree.
</p>
<p>
We'll have <code>bid</code> store bids in a balanced binary search tree. The
in-order traversal of the resulting tree gives us a sorted list of all bids.
We're essentially having each auction participant pay the necessary gas to sort
just their bid into the data structure.
</p>
<p>
Here's a scaffold of what <code>bid</code> could look like:
<pre>
contract Auction {
// Node in a red-black tree
struct Node {
uint sellAmount;
uint buyAmount;
uint left;
uint right;
uint parent;
bool red;
}
Node[] tree;
uint root;
function greaterThan(Node n1, Node n2) {
return n1.sellAmount * n2.buyAmount >= n1.buyAmount * n2.sellAmount;
}
function <b>bid</b>(uint sellAmount, uint buyAmount) payable returns (uint bidId) {
bidId = tree.length;
tree.push(Node(sellAmount, buyAmount, 0, 0, 0, false));
... // <b>do RBT insertion with greaterThan as the comparison function</b>
}
...
}
</pre>
</p>
<p>
When all bids have been submitted and sorted into the tree, we need to be able
to fill the bids of those users that submitted the highest bids. When a user
comes in and says "Hey, look at my bid, I paid <em>z</em> amount of
<strong>B</strong> so now I deserve <em>y</em> amount of <strong>A</strong>!",
we need to be able to verify that their bid actually fit into the pot and can be
filled.
</p>
<p>
At this point, when implementing <code>fill</code> naively, we would still need
a linear traversal to determine whether a given <code>bidId</code> should be
filled or not. To finish off this auction design, we'll augment the BST such
that we can implement an <em>O</em>(log<em>n</em>) <code>fill</code>.
</p>
<h3>Filling</h3>
<p>
We'll augment each node of the tree to contain the total amount of
<code>buyToken</code> in the node's subtree. This will allow us to quickly
calculate the amount of <code>buyToken</code> in all nodes with a better price
than a given bid.
</p>
<p>
This augmentation is possible because updating this total is efficient when
updating the tree. We only need to modify the insert and rotation operations:
<pre>
function insert(Bid bid, uint bidId) {
uint current = tree.root;
uint parent = current;
while (current != 0) {
parent = current;
<b>// Increase totals on the path from root to newly inserted node
tree.[parent].total = tree[parent].total + bid.buyAmount;</b>
if (greaterThan(bid, tree.bids[current])) {
current = tree.bids[current].left;
} else {
current = tree.bids[current].right;
}
}
// ...
}
<b>// Called at the end of each rotation operation</b>
function recalculateTotalsAfterRotation(uint n, uint pivot) {
tree[pivot].total = tree[n].total;
tree[n].total = tree[tree.bids[n].left].total +
tree[tree[n].right].total +
tree[n].buyAmount
}
</pre>
<p>
This in our data structure gives us a logarithmic algorithm for checking how
much of the token would be bought by bidders at higher exchange rates. We walk
up from our node to the root. Any time we step up from a right child, we'll add
the total from our sibling's subtree and parent node's value (since, by the BST
property, those are the nodes with greater rates than our starting node).
<pre>
function totalFromHigherExchangeRates(uint bidId) view returns (uint) {
uint total = tree[tree[bidId].left].total;
uint parent = tree[bidId].parent;
uint currentNode = bidId;
while (parent != 0) {
<b>if (tree[parent].right == currentNode) {
total += tree[tree[parent].left].total;
total += tree[parent].buyAmount;
}</b>
currentNode = parent;
parent = tree[parent].parent;
}
return total;
}
</pre>
</p>
<p>
<code>fill</code> can now use <code>totalFromHigherExchangeRates</code> to
determine whether or not a given bid should be filled. The per-bid
computational complexity (and thus gas cost) is logarithmic with respect to the
total number of bids.
</p>
<h3>Origin</h3>
<p>
I originally came up with this auction design while working on
<a href="https://celo.org">Celo's</a> on-chain stability mechanism. The idea was
to use periodic currency auctions to expand or contract the supply of a token to
match demand, thus stabilizing its price.
</p>
<p>
Later we came up with an even more clever stabilization mechanism, one that can
function continuously. It's based on <a href="https://uniswap.io/">Uniswap</a>
and you can read more about it in
<a href="https://medium.com/celohq/zooming-in-on-the-celo-expansion-contraction-mechanism-446ca7abe4f">
this Medium post
</a>.
<p id='note-1'>
<a href='#ref-1'>1.</a> The last successful bid might only be partially filled.
This is a detail that needs to be addressed, but it's easy to do so. For
simplicity of the rest of the text, I'll avoid discussing it.
</p>
<p id='note-2'>
<a href='#ref-2'>2.</a> For simplicity, we'll talk about a two phase, open bid
auction. As with the single unit auction, this could be modified to a hidden bid
auction by adding a commit step.
</p>
|