Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}