ticket.gno
7.74 Kb · 269 lines
1package gnocial
2
3import (
4 "chain"
5 "math/overflow"
6 "strconv"
7 "strings"
8)
9
10// QuoteBuy is the exact ugnot a buyer must attach to mint amount tickets.
11// The total fee is always FeeCapBps of the curve. An anonymous quote pays
12// no trader-referrer slice, so that slice stays in the protocol fee.
13// The amount due does not change when the buyer is known.
14func QuoteBuy(handle string, amount int) (due, curve, creatorFee, protocolFee, referralFee int64) {
15 return QuoteBuyFor(handle, "", amount)
16}
17
18// QuoteBuyFor is QuoteBuy with the buyer's address. Empty or a member with
19// no referrer adds no trader-referrer slice. A bad address panics.
20func QuoteBuyFor(handle, buyer string, amount int) (due, curve, creatorFee, protocolFee, referralFee int64) {
21 curve, creatorFee, protocolFee, referralFee = quoteTotals(handle, buyer, amount, false)
22 due = overflow.Add64p(curve, overflow.Add64p(creatorFee, overflow.Add64p(protocolFee, referralFee)))
23 return
24}
25
26// QuoteSell is what a seller receives. The reserve drops by the full curve.
27// Proceeds are the curve minus FeeCapBps, whoever the seller is.
28func QuoteSell(handle string, amount int) (proceeds, curve, creatorFee, protocolFee, referralFee int64) {
29 return QuoteSellFor(handle, "", amount)
30}
31
32// QuoteSellFor is QuoteSell with the seller's address.
33func QuoteSellFor(handle, seller string, amount int) (proceeds, curve, creatorFee, protocolFee, referralFee int64) {
34 curve, creatorFee, protocolFee, referralFee = quoteTotals(handle, seller, amount, true)
35 proceeds = curve - creatorFee - protocolFee - referralFee
36 if proceeds <= 0 {
37 panic("proceeds")
38 }
39 return
40}
41
42// Buy mints amount tickets of handle. Attach exactly QuoteBuy's due in ugnot.
43// The caller's own referrer receives the trader slice. The due matches QuoteBuy.
44func Buy(cur realm, handle string, amount int) {
45 who := caller(cur)
46 if state.paused {
47 panic("paused")
48 }
49 mustMember(who)
50 curve, creatorFee, protocolFee, creatorRefAmt, traderRefAmt, p, creatorRef, traderRef := tradeQuote(handle, who.String(), amount, false)
51 due := overflow.Add64p(curve, bps(curve, FeeCapBps))
52 sent := takeUgnot(cur)
53 if sent != due {
54 panic("send exactly " + strconv.FormatInt(due, 10) + "ugnot")
55 }
56 b := bookOf(p.Addr)
57 b.Supply += amount
58 b.Reserve = overflow.Add64p(b.Reserve, curve)
59 state.totalReserve = overflow.Add64p(state.totalReserve, curve)
60 addShares(who, p.Addr, amount)
61 forwardFees(cur, p, creatorFee, protocolFee, creatorRef, creatorRefAmt, traderRef, traderRefAmt)
62 chain.Emit(
63 "ArenaBuy",
64 "buyer", who.String(),
65 "creator", p.Addr.String(),
66 "handle", p.Handle,
67 "amount", strconv.Itoa(amount),
68 "curve", strconv.FormatInt(curve, 10),
69 "due", strconv.FormatInt(due, 10),
70 )
71}
72
73// Sell burns amount tickets and pays the caller from the reserve, minus fees.
74// Sells stay available while the realm is paused.
75func Sell(cur realm, handle string, amount int) {
76 who := caller(cur)
77 mustNoPay()
78 mustMember(who)
79 curve, creatorFee, protocolFee, creatorRefAmt, traderRefAmt, p, creatorRef, traderRef := tradeQuote(handle, who.String(), amount, true)
80 if getShares(who, p.Addr) < amount {
81 panic("not enough tickets")
82 }
83 proceeds := curve - bps(curve, FeeCapBps)
84 if proceeds <= 0 {
85 panic("proceeds")
86 }
87 b := bookOf(p.Addr)
88 if b.Reserve < curve || state.totalReserve < curve {
89 panic("insolvent")
90 }
91 b.Supply -= amount
92 b.Reserve = overflow.Sub64p(b.Reserve, curve)
93 state.totalReserve = overflow.Sub64p(state.totalReserve, curve)
94 subShares(who, p.Addr, amount)
95 payout(cur, who, proceeds, "seller")
96 forwardFees(cur, p, creatorFee, protocolFee, creatorRef, creatorRefAmt, traderRef, traderRefAmt)
97 chain.Emit(
98 "ArenaSell",
99 "seller", who.String(),
100 "creator", p.Addr.String(),
101 "handle", p.Handle,
102 "amount", strconv.Itoa(amount),
103 "curve", strconv.FormatInt(curve, 10),
104 "proceeds", strconv.FormatInt(proceeds, 10),
105 )
106}
107
108func forwardFees(cur realm, creator *Profile, creatorFee, protocolFee int64, creatorRef address, creatorRefAmt int64, traderRef address, traderRefAmt int64) {
109 if protocolFee > 0 && !state.treasury.IsValid() {
110 panic("treasury not set")
111 }
112 payout(cur, creator.Addr, creatorFee, "creator")
113 payout(cur, state.treasury, protocolFee, "treasury")
114 if creatorRefAmt > 0 {
115 payout(cur, creatorRef, creatorRefAmt, "creator-referrer")
116 }
117 if traderRefAmt > 0 {
118 payout(cur, traderRef, traderRefAmt, "trader-referrer")
119 }
120}
121
122func addShares(holder, creator address, n int) {
123 if n < 1 {
124 panic("amount")
125 }
126 had := getShares(holder, creator)
127 setShares(holder, creator, had+n)
128 if had == 0 {
129 bookOf(creator).Holders++
130 }
131}
132
133func subShares(holder, creator address, n int) {
134 had := getShares(holder, creator)
135 if n < 1 || had < n {
136 panic("not enough tickets")
137 }
138 setShares(holder, creator, had-n)
139 if had == n {
140 b := bookOf(creator)
141 if b.Holders > 0 {
142 b.Holders--
143 }
144 }
145}
146
147func quoteTotals(handle, trader string, amount int, sell bool) (curve, creatorFee, protocolFee, referralFee int64) {
148 var creatorRefAmt, traderRefAmt int64
149 curve, creatorFee, protocolFee, creatorRefAmt, traderRefAmt, _, _, _ = tradeQuote(handle, trader, amount, sell)
150 referralFee = overflow.Add64p(creatorRefAmt, traderRefAmt)
151 return
152}
153
154// tradeQuote splits FeeCapBps of the curve. trader is the buyer or seller.
155// Empty means an anonymous quote: no trader-referrer slice.
156// protocolFee is the cap minus the floored slices, so it cannot go negative
157// while the configured slices fit inside the cap.
158func tradeQuote(handle, trader string, amount int, sell bool) (curve, creatorFee, protocolFee, creatorRefAmt, traderRefAmt int64, creator *Profile, creatorRef, traderRef address) {
159 creator = lookup(handle)
160 if creator == nil {
161 panic("unknown handle")
162 }
163 b := bookOf(creator.Addr)
164 start := b.Supply
165 if sell {
166 if amount < 1 || amount > MaxTrade {
167 panic("amount")
168 }
169 if amount > b.Supply {
170 panic("not enough tickets")
171 }
172 start = b.Supply - amount
173 }
174 curve = curveCost(start, amount)
175 points := creator.CreatorFee
176 if points < CreatorFeeMinBps || points > CreatorFeeMaxBps {
177 panic("fee")
178 }
179 creatorRef = creator.Referrer
180 traderRef = traderReferrer(trader)
181 total := bps(curve, FeeCapBps)
182 creatorFee = bps(curve, points)
183 if creatorRef.IsValid() {
184 creatorRefAmt = bps(curve, CreatorReferralBps)
185 }
186 if traderRef.IsValid() {
187 traderRefAmt = bps(curve, TraderReferralBps)
188 }
189 used := overflow.Add64p(creatorFee, overflow.Add64p(creatorRefAmt, traderRefAmt))
190 if used > total {
191 panic("fee")
192 }
193 protocolFee = total - used
194 return
195}
196
197func traderReferrer(raw string) address {
198 raw = strings.TrimSpace(raw)
199 if raw == "" {
200 return address("")
201 }
202 if len(raw) > 60 {
203 panic("bad address")
204 }
205 who := address(raw)
206 if !who.IsValid() {
207 panic("bad address")
208 }
209 p := profileByAddr(who)
210 if p == nil || !p.Referrer.IsValid() {
211 return address("")
212 }
213 return p.Referrer
214}
215
216func bps(amount, points int64) int64 {
217 if amount == 0 || points == 0 {
218 return 0
219 }
220 if amount < 0 || points < 0 {
221 panic("fee")
222 }
223 return overflow.Mul64p(amount, points) / 10_000
224}
225
226// curveCost is the ugnot reserve needed to move `amount` tickets starting at supply.
227// Ticket i (1-based) is priced i² * TicketUnit. The sum of squares is exact.
228func curveCost(supply, amount int) int64 {
229 if amount < 1 || amount > MaxTrade {
230 panic("amount")
231 }
232 if supply < 0 {
233 panic("supply")
234 }
235 from := int64(supply)
236 to := from + int64(amount)
237 if to > int64(MaxSupply) {
238 panic("supply cap")
239 }
240 return overflow.Mul64p(sumSq(to)-sumSq(from), TicketUnit)
241}
242
243func sumSq(n int64) int64 {
244 if n <= 0 {
245 return 0
246 }
247 if n > int64(MaxSupply) {
248 panic("supply cap")
249 }
250 // n*(n+1)*(2n+1)/6, dividing 2 and 3 out before multiplying.
251 a := n
252 b := n + 1
253 if a%2 == 0 {
254 a /= 2
255 } else {
256 b /= 2
257 }
258 c := 2*n + 1
259 if a%3 == 0 {
260 a /= 3
261 } else if b%3 == 0 {
262 b /= 3
263 } else if c%3 == 0 {
264 c /= 3
265 } else {
266 panic("sumSq")
267 }
268 return overflow.Mul64p(overflow.Mul64p(a, b), c)
269}