offers.gno
7.89 Kb · 216 lines
1package market
2
3import (
4 "chain"
5 "strconv"
6 "strings"
7 "time"
8
9 "gno.land/p/nt/avl/v0"
10 "gno.land/p/nt/seqid/v0"
11 "gno.land/p/samcrew/launchpad/merkle/v1"
12 sm "gno.land/p/samcrew/launchpad/safemath/v1"
13 "gno.land/r/samcrew/launchpad/nft/v1"
14)
15
16const (
17 OfferToken = "token" // one named token
18 OfferCollection = "collection" // any token of the collection
19 OfferTrait = "trait" // any token proven to carry a trait
20 StatusAccepted = "accepted"
21 // The token's own listing, closed because the token was sold to an offer.
22 StatusSuperseded = "superseded"
23 maxTraitBytes = 100
24)
25
26// Offer is a bid whose full price sits in this realm until it is accepted,
27// cancelled by its buyer or returned after expiry. An offer exists while it
28// is open and no longer.
29type Offer struct {
30 ID string // "O<n>"
31 Kind string
32 Collection string
33 Number int64 // token offers only
34 Trait string // trait offers only: "<type>=<value>"
35 Buyer address
36 Price int64
37 Currency string
38 CreatedAt int64
39 ExpiresAt int64
40 FeeBPS int64 // protocol fee, pinned when the buyer funded the offer
41}
42
43var (
44 lastOffer seqid.ID
45 offers avl.Tree // offer key -> *Offer, in creation order
46 open avl.Tree // "<collection>/<offer key>" -> *Offer
47 byBuyer avl.Tree // "<buyer>/<offer key>" -> *Offer
48)
49
50// findOffer returns an open offer and its key, or nil: a closed one is gone.
51func findOffer(id string) (*Offer, string) {
52 if key, ok := orderKey('O', id); ok {
53 if o := offers.Get(key); o != nil {
54 return o.(*Offer), key
55 }
56 }
57 return nil, ""
58}
59
60func mustOffer(id string) (*Offer, string) {
61 o, key := findOffer(id)
62 if o == nil {
63 panic("launchpad/market: unknown offer")
64 }
65 return o, key
66}
67
68// TraitLeaf is the Merkle leaf that proves a token carries a trait, as
69// committed by the collection's creator in the NFT ledger.
70func TraitLeaf(collection string, number int64, trait string) merkle.Hash {
71 return merkle.Leaf([]byte("launchpad-trait-v1|" + collection + "|" +
72 strconv.FormatInt(number, 10) + "|" + trait))
73}
74
75// MakeOffer escrows price and returns the offer ID. kind selects what the
76// offer is for: number is set for a token offer, trait for a trait offer, and
77// both are zero and empty otherwise. A trait offer needs a collection whose
78// creator committed its traits. The protocol fee in force is pinned on the
79// offer; it comes out of what the seller receives, never on top of the price,
80// and maxFeeBPS is the highest the buyer lets a seller be charged.
81func MakeOffer(cur realm, kind, collection string, number int64, trait string,
82 price, expiresAt int64, currencyKey string, maxFeeBPS int64) string {
83 buyer := directCaller(cur)
84 fee := pinned(collection, price, expiresAt, currencyKey, maxFeeBPS)
85 ok := false
86 switch kind {
87 case OfferToken:
88 ok = trait == "" && nft.StatusOf(collection, number) == nft.StatusActive &&
89 nft.OwnerOf(collection, number) != buyer
90 case OfferCollection:
91 ok = trait == "" && number == 0
92 case OfferTrait:
93 ok = number == 0 && len(trait) <= maxTraitBytes && strings.Count(trait, "=") == 1 &&
94 !strings.HasPrefix(trait, "=") && !strings.HasSuffix(trait, "=") &&
95 !strings.Contains(trait, "|") && nft.TraitsRoot(collection) != ""
96 }
97 if !ok {
98 panic("launchpad/market: invalid offer")
99 }
100 before := take(cur, currencyKey, price)
101 escrow[currencyKey] = sm.Add(escrow[currencyKey], price)
102 id, key := nextOrder(&lastOffer, "O")
103 o := &Offer{ID: id, Kind: kind, Collection: collection, Number: number,
104 Trait: trait, Buyer: buyer, Price: price, Currency: currencyKey,
105 CreatedAt: time.Now().Unix(), ExpiresAt: expiresAt, FeeBPS: fee}
106 offers.Set(key, o)
107 open.Set(collection+"/"+key, o)
108 byBuyer.Set(buyer.String()+"/"+key, o)
109 settled(cur, currencyKey, before)
110 chain.Emit("OfferMade", "offer", id, "kind", kind, "collection", collection,
111 "number", strconv.FormatInt(number, 10), "trait", trait,
112 "buyer", buyer.String(), "currency", currencyKey,
113 "price", strconv.FormatInt(price, 10), "fee_bps", strconv.FormatInt(fee, 10),
114 "expires_at", strconv.FormatInt(expiresAt, 10))
115 return id
116}
117
118// release deletes an offer, takes its price out of escrow and says why in its
119// closing event; the caller then pays the price out. Every exit goes through
120// here, and an offer leaves escrow once, by its own key: with `settled`, an
121// exit pays out exactly the price of the one offer it closed, so a fault in
122// one offer's exit can never spend another offer's escrow.
123func release(o *Offer, key, status string) {
124 if v := offers.Get(key); v == nil || v.(*Offer) != o {
125 panic("launchpad/market: offer already closed")
126 }
127 offers.Remove(key)
128 open.Remove(o.Collection + "/" + key)
129 byBuyer.Remove(o.Buyer.String() + "/" + key)
130 escrow[o.Currency] = sm.Sub(escrow[o.Currency], o.Price)
131 chain.Emit("OfferClosed", "offer", o.ID, "status", status)
132}
133
134// refund returns an open offer's escrow to its buyer. Refunds never consult
135// the config realm: no pause or policy change can hold a buyer's funds.
136func refund(cur realm, o *Offer, key, status string) {
137 before := untouched(cur, o.Currency)
138 release(o, key, status)
139 pay(cur, o.Currency, o.Buyer, o.Price)
140 settled(cur, o.Currency, before)
141}
142
143// CancelOffer takes back the caller's own open offer, expired or not.
144func CancelOffer(cur realm, id string) {
145 who := directCaller(cur)
146 o, key := mustOffer(id)
147 if who != o.Buyer {
148 panic("launchpad/market: buyer only")
149 }
150 refund(cur, o, key, StatusCancelled)
151}
152
153// ExpireOffer returns to its buyer the escrow of an offer that expired more
154// than graceToClose ago. Anyone may call it: the price goes to the buyer, and
155// the chain pays the caller the storage deposit of the bytes the offer
156// occupied, so for a week after expiry only the buyer, with CancelOffer,
157// takes its own offer back.
158func ExpireOffer(cur realm, id string) {
159 directCaller(cur)
160 o, key := mustOffer(id)
161 if time.Now().Unix() < o.ExpiresAt+graceToClose {
162 panic("launchpad/market: only its buyer closes this offer yet")
163 }
164 refund(cur, o, key, StatusExpired)
165}
166
167// AcceptOffer sells the caller's token number to an open offer. proof is the
168// comma-separated Merkle path of the token's trait for a trait offer, and
169// empty otherwise. currencyKey and price are the terms the seller read: an
170// offer on other terms, such as an ID the seller never saw, buys nothing. The
171// caller must first approve this realm in the NFT ledger.
172func AcceptOffer(cur realm, id string, number int64, proof, currencyKey string, price int64) {
173 seller := directCaller(cur)
174 o, key := mustOffer(id)
175 if o.Currency != currencyKey || o.Price != price {
176 panic("launchpad/market: order terms differ from those signed")
177 }
178 before := untouched(cur, o.Currency)
179 p := policy(o.Currency)
180 ok := time.Now().Unix() < o.ExpiresAt && seller != o.Buyer &&
181 sellable(o.Collection, number, seller)
182 switch {
183 case !ok:
184 case o.Kind == OfferToken:
185 ok = number == o.Number && proof == ""
186 case o.Kind == OfferCollection:
187 ok = proof == ""
188 default:
189 root, _ := merkle.ParseHash(nft.TraitsRoot(o.Collection))
190 path, err := merkle.ParseProof(proof)
191 ok = err == nil && merkle.Verify(root, TraitLeaf(o.Collection, number, o.Trait), path)
192 }
193 if !ok {
194 panic("launchpad/market: offer unavailable for this token")
195 }
196 release(o, key, StatusAccepted)
197 if l := listingOf(o.Collection, number); l != nil {
198 closeListing(l, StatusSuperseded)
199 }
200 nft.TransferFrom(cross(cur), o.Collection, seller, o.Buyer, number)
201 settle(cur, o.Currency, o.Collection, o.Price, o.FeeBPS, seller, p.treasury)
202 settled(cur, o.Currency, before)
203 chain.Emit("OfferAccepted", "offer", id, "collection", o.Collection,
204 "number", strconv.FormatInt(number, 10), "buyer", o.Buyer.String(),
205 "seller", seller.String(), "currency", o.Currency,
206 "price", strconv.FormatInt(o.Price, 10))
207}
208
209// GetOffer returns an open offer; a closed or unknown ID aborts.
210func GetOffer(id string) Offer {
211 o, _ := mustOffer(id)
212 return *o
213}
214
215// EscrowOf is the total of open offers in a currency.
216func EscrowOf(currencyKey string) int64 { return escrow[currencyKey] }