englishauction.gno
6.20 Kb · 231 lines
1// Package englishauction is an idiomatic gno.land port of the classic Solidity
2// English (ascending-bid) auction. Anyone can Start an auction for a named item
3// with a duration in blocks; bidders call Bid with a strictly increasing amount.
4// The previous highest bid becomes refundable (claim it with Withdraw). After
5// the auction's end height anyone may call End, awarding the item to the highest
6// bidder. Auctions and pending refunds live in ordered avl trees so Render can
7// iterate deterministically.
8package englishauction
9
10import (
11 "strconv"
12
13 "chain"
14 "chain/runtime"
15 "chain/runtime/unsafe"
16
17 "gno.land/p/nt/avl/v0"
18)
19
20// Auction is a single ascending-bid auction.
21type Auction struct {
22 ID int64
23 Seller address
24 Item string
25 EndHeight int64
26 HighestBid uint64
27 HighestBidder address
28 Ended bool
29}
30
31var (
32 auctions avl.Tree // key(id) -> *Auction
33 pending avl.Tree // "<id>:<addr>" -> uint64 refundable amount
34 nextID int64 = 1
35)
36
37// key formats an id into a zero-padded, lexicographically-sortable string.
38func key(id int64) string {
39 s := strconv.FormatInt(id, 10)
40 for len(s) < 12 {
41 s = "0" + s
42 }
43 return s
44}
45
46// pendKey formats the refund key for a given auction id and bidder.
47func pendKey(id int64, bidder address) string {
48 return key(id) + ":" + bidder.String()
49}
50
51// Start opens a new auction for `item` running for `durationBlocks` blocks from
52// the current height. The caller becomes the seller. Returns the auction id.
53func Start(cur realm, item string, durationBlocks int64) int64 {
54 if item == "" {
55 panic("englishauction: item must not be empty")
56 }
57 if durationBlocks <= 0 {
58 panic("englishauction: duration must be > 0 blocks")
59 }
60 seller := unsafe.PreviousRealm().Address()
61 id := nextID
62 nextID++
63
64 a := &Auction{
65 ID: id,
66 Seller: seller,
67 Item: item,
68 EndHeight: runtime.ChainHeight() + durationBlocks,
69 }
70 auctions.Set(key(id), a)
71
72 chain.Emit("Start",
73 "id", strconv.FormatInt(id, 10),
74 "seller", seller.String(),
75 "item", item,
76 "endHeight", strconv.FormatInt(a.EndHeight, 10),
77 )
78 return id
79}
80
81// Bid places a bid of `amount` on auction `id`. It must strictly exceed the
82// current highest bid. The displaced highest bid becomes refundable to its
83// bidder via Withdraw.
84func Bid(cur realm, id int64, amount uint64) {
85 a := mustGet(id)
86 if a.Ended || runtime.ChainHeight() >= a.EndHeight {
87 panic("englishauction: auction has ended")
88 }
89 if amount <= a.HighestBid {
90 panic("englishauction: bid must strictly exceed current highest")
91 }
92 bidder := unsafe.PreviousRealm().Address()
93 if bidder == a.Seller {
94 panic("englishauction: seller cannot bid")
95 }
96
97 // The prior top bid becomes refundable (accumulate in case of repeats).
98 if a.HighestBid > 0 {
99 pk := pendKey(id, a.HighestBidder)
100 pending.Set(pk, pendingOf(id, a.HighestBidder)+a.HighestBid)
101 }
102
103 a.HighestBid = amount
104 a.HighestBidder = bidder
105
106 chain.Emit("Bid",
107 "id", strconv.FormatInt(id, 10),
108 "bidder", bidder.String(),
109 "amount", strconv.FormatUint(amount, 10),
110 )
111}
112
113// Withdraw refunds the caller's accumulated displaced bids for auction `id`.
114// Returns the amount refunded (0 if nothing pending).
115func Withdraw(cur realm, id int64) uint64 {
116 mustGet(id) // ensure auction exists
117 caller := unsafe.PreviousRealm().Address()
118 amount := pendingOf(id, caller)
119 if amount == 0 {
120 return 0
121 }
122 pending.Remove(pendKey(id, caller))
123 chain.Emit("Withdraw",
124 "id", strconv.FormatInt(id, 10),
125 "bidder", caller.String(),
126 "amount", strconv.FormatUint(amount, 10),
127 )
128 return amount
129}
130
131// End closes auction `id` once its end height is reached, awarding the item to
132// the highest bidder (if any). Anyone may call it.
133func End(cur realm, id int64) {
134 a := mustGet(id)
135 if a.Ended {
136 panic("englishauction: auction already ended")
137 }
138 if runtime.ChainHeight() < a.EndHeight {
139 panic("englishauction: auction not yet over")
140 }
141 a.Ended = true
142
143 chain.Emit("End",
144 "id", strconv.FormatInt(id, 10),
145 "winner", a.HighestBidder.String(),
146 "amount", strconv.FormatUint(a.HighestBid, 10),
147 )
148}
149
150// --- read-only helpers (safe from tests and Render) ---
151
152// mustGet returns the auction for `id`, panicking if it does not exist.
153func mustGet(id int64) *Auction {
154 v := auctions.Get(key(id))
155 if v == nil {
156 panic("englishauction: no such auction")
157 }
158 return v.(*Auction)
159}
160
161// pendingOf returns the refundable amount owed to `bidder` for auction `id`.
162func pendingOf(id int64, bidder address) uint64 {
163 v := pending.Get(pendKey(id, bidder))
164 if v == nil {
165 return 0
166 }
167 return v.(uint64)
168}
169
170// blocksLeft returns how many blocks remain before `a` can be ended (0 if the
171// end height has been reached).
172func blocksLeft(a *Auction, height int64) int64 {
173 if height >= a.EndHeight {
174 return 0
175 }
176 return a.EndHeight - height
177}
178
179// hasWinner reports whether the auction received at least one bid.
180func hasWinner(a *Auction) bool {
181 return a.HighestBid > 0
182}
183
184// Render lists every auction with item, top bid + bidder, time left, and winner.
185func Render(path string) string {
186 out := "# English Auction\n\n"
187 out += "Ascending-bid auctions. Start one, outbid others, and End it after "
188 out += "its block deadline to award the item to the top bidder.\n\n"
189
190 if auctions.Size() == 0 {
191 out += "_No auctions yet. Call `Start` to open one._\n"
192 return out
193 }
194
195 height := runtime.ChainHeight()
196 out += "_Current block height: " + strconv.FormatInt(height, 10) + "_\n\n"
197 out += "| ID | Item | Highest Bid | Bidder | Status | Winner |\n"
198 out += "|---:|------|------------:|--------|--------|--------|\n"
199
200 auctions.Iterate("", "", func(_ string, v interface{}) bool {
201 a := v.(*Auction)
202
203 bidStr := "—"
204 bidderStr := "—"
205 if hasWinner(a) {
206 bidStr = strconv.FormatUint(a.HighestBid, 10)
207 bidderStr = "`" + a.HighestBidder.String() + "`"
208 }
209
210 status := ""
211 winner := "—"
212 switch {
213 case a.Ended:
214 status = "ended"
215 if hasWinner(a) {
216 winner = "`" + a.HighestBidder.String() + "`"
217 } else {
218 winner = "no bids"
219 }
220 case height >= a.EndHeight:
221 status = "awaiting End"
222 default:
223 status = strconv.FormatInt(blocksLeft(a, height), 10) + " blocks left"
224 }
225
226 out += "| " + strconv.FormatInt(a.ID, 10) + " | " + a.Item + " | " +
227 bidStr + " | " + bidderStr + " | " + status + " | " + winner + " |\n"
228 return false
229 })
230 return out
231}