auction.gno
5.55 Kb · 182 lines
1package gnogle_nftmarket
2
3import (
4 "chain"
5 "strconv"
6 "time"
7
8 "gno.land/p/g18wk4a80cr7dqa25vfka2yug5n3pd50udled6y3/grc721"
9 "gno.land/p/nt/avl/v0"
10 "gno.land/p/nt/ufmt/v0"
11)
12
13// Auction is a timed English auction. While it runs, the NFT is escrowed in
14// the realm and the current highest bid is held by the realm (the previous
15// highest bidder is refunded whenever a higher bid arrives).
16type Auction struct {
17 collID string
18 tokenID string
19 seller address
20 minBid int64 // minimum first bid, in ugnot
21 highestBid int64 // 0 when there are no bids yet
22 highestBidder address // zero address when there are no bids yet
23 endTime time.Time // auction closes at this block time
24 settled bool
25}
26
27var auctions avl.Tree // "collID/tokenID" -> *Auction
28
29// CreateAuction escrows the caller's token and opens an auction that runs for
30// durationSecs seconds. minBid is the minimum acceptable first bid (ugnot).
31func CreateAuction(cur realm, collID, tokenID string, minBid, durationSecs int64) {
32 assertUserCall(cur)
33 if minBid < 0 {
34 panic("minBid cannot be negative")
35 }
36 if durationSecs <= 0 {
37 panic("duration must be positive")
38 }
39 coll := mustGetCollection(collID)
40 seller := cur.Previous().Address()
41 tid := grc721.TokenID(tokenID)
42
43 if _, exists := auctions.Get(listingKey(collID, tokenID)); exists {
44 panic("token is already in an auction")
45 }
46 if _, listed := getListing(collID, tokenID); listed {
47 panic("token is listed for fixed-price sale; cancel that listing first")
48 }
49
50 owner, err := coll.nft.OwnerOf(tid)
51 if err != nil {
52 panic(err)
53 }
54 if owner != seller {
55 panic("only the token owner can auction it")
56 }
57
58 // escrow the NFT in the realm for the duration of the auction
59 if err := coll.nft.TransferFrom(seller, seller, cur.Address(), tid); err != nil {
60 panic(err)
61 }
62
63 auctions.Set(listingKey(collID, tokenID), &Auction{
64 collID: collID,
65 tokenID: tokenID,
66 seller: seller,
67 minBid: minBid,
68 highestBid: 0,
69 highestBidder: zeroAddr,
70 endTime: time.Now().Add(time.Duration(durationSecs) * time.Second),
71 settled: false,
72 })
73
74 chain.Emit("AuctionCreated", "collection", collID, "tokenId", tokenID,
75 "seller", seller.String(), "minBid", strconv.FormatInt(minBid, 10))
76}
77
78// Bid places a bid on a running auction. Attach GNOT strictly greater than the
79// current highest bid (and at least minBid). The previous highest bidder is
80// refunded automatically.
81func Bid(cur realm, collID, tokenID string) {
82 assertUserCall(cur)
83 a := mustGetAuction(collID, tokenID)
84 bidder := cur.Previous().Address()
85
86 if a.settled {
87 panic("auction already settled")
88 }
89 if !time.Now().Before(a.endTime) {
90 panic("auction has ended")
91 }
92 if bidder == a.seller {
93 panic("seller cannot bid on their own auction")
94 }
95
96 amount := receivedUgnot()
97 if amount < a.minBid {
98 panic(ufmt.Sprintf("bid must be at least %d ugnot", a.minBid))
99 }
100 if amount <= a.highestBid {
101 panic(ufmt.Sprintf("bid must exceed the current highest bid of %d ugnot", a.highestBid))
102 }
103
104 // refund the previous highest bidder, if any
105 if a.highestBid > 0 && a.highestBidder != zeroAddr {
106 payout(cur, a.highestBidder, a.highestBid)
107 }
108 a.highestBid = amount
109 a.highestBidder = bidder
110
111 chain.Emit("Bid", "collection", collID, "tokenId", tokenID,
112 "bidder", bidder.String(), "amount", strconv.FormatInt(amount, 10))
113}
114
115// EndAuction settles an auction once its end time has passed. Anyone may call
116// it. With a winning bid the NFT goes to the winner and the proceeds are split
117// seller / royalty / fee; with no bids the NFT is returned to the seller.
118func EndAuction(cur realm, collID, tokenID string) {
119 assertUserCall(cur)
120 a := mustGetAuction(collID, tokenID)
121 if a.settled {
122 panic("auction already settled")
123 }
124 if time.Now().Before(a.endTime) {
125 panic("auction has not ended yet")
126 }
127
128 coll := mustGetCollection(collID)
129 tid := grc721.TokenID(tokenID)
130 a.settled = true
131 auctions.Remove(listingKey(collID, tokenID))
132
133 // no bids: return the NFT to the seller
134 if a.highestBid == 0 {
135 if err := coll.nft.TransferFrom(cur.Address(), cur.Address(), a.seller, tid); err != nil {
136 panic(err)
137 }
138 chain.Emit("AuctionEnded", "collection", collID, "tokenId", tokenID, "winner", "", "amount", "0")
139 return
140 }
141
142 // winning bid: NFT to winner, split the proceeds
143 if err := coll.nft.TransferFrom(cur.Address(), cur.Address(), a.highestBidder, tid); err != nil {
144 panic(err)
145 }
146 royalty := a.highestBid * coll.royaltyBps / bpsDenominator
147 fee := a.highestBid * feeBps / bpsDenominator
148 sellerAmt := a.highestBid - royalty - fee
149
150 feePot += fee
151 payout(cur, coll.creator, royalty)
152 payout(cur, a.seller, sellerAmt)
153 recordSale(collID, tokenID, a.seller, a.highestBidder, a.highestBid, "auction")
154
155 chain.Emit("AuctionEnded", "collection", collID, "tokenId", tokenID,
156 "winner", a.highestBidder.String(), "amount", strconv.FormatInt(a.highestBid, 10))
157}
158
159func mustGetAuction(collID, tokenID string) *Auction {
160 v, ok := auctions.Get(listingKey(collID, tokenID))
161 if !ok {
162 panic("no active auction for this token")
163 }
164 return v.(*Auction)
165}
166
167func getAuction(collID, tokenID string) (*Auction, bool) {
168 v, ok := auctions.Get(listingKey(collID, tokenID))
169 if !ok {
170 return nil, false
171 }
172 return v.(*Auction), true
173}
174
175// GetAuction returns the current state of an auction (read-only).
176func GetAuction(collID, tokenID string) (seller address, minBid, highestBid int64, highestBidder address, endUnix int64, active bool) {
177 a, ok := getAuction(collID, tokenID)
178 if !ok {
179 return zeroAddr, 0, 0, zeroAddr, 0, false
180 }
181 return a.seller, a.minBid, a.highestBid, a.highestBidder, a.endTime.Unix(), true
182}