Search Apps Documentation Source Content File Folder Download Copy Actions Download

auction.gno

5.46 Kb · 181 lines
  1package 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
154	chain.Emit("AuctionEnded", "collection", collID, "tokenId", tokenID,
155		"winner", a.highestBidder.String(), "amount", strconv.FormatInt(a.highestBid, 10))
156}
157
158func mustGetAuction(collID, tokenID string) *Auction {
159	v, ok := auctions.Get(listingKey(collID, tokenID))
160	if !ok {
161		panic("no active auction for this token")
162	}
163	return v.(*Auction)
164}
165
166func getAuction(collID, tokenID string) (*Auction, bool) {
167	v, ok := auctions.Get(listingKey(collID, tokenID))
168	if !ok {
169		return nil, false
170	}
171	return v.(*Auction), true
172}
173
174// GetAuction returns the current state of an auction (read-only).
175func GetAuction(collID, tokenID string) (seller address, minBid, highestBid int64, highestBidder address, endUnix int64, active bool) {
176	a, ok := getAuction(collID, tokenID)
177	if !ok {
178		return zeroAddr, 0, 0, zeroAddr, 0, false
179	}
180	return a.seller, a.minBid, a.highestBid, a.highestBidder, a.endTime.Unix(), true
181}