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