ticket.gno
5.28 Kb · 206 lines
1package arena
2
3import (
4 "chain"
5 "math/overflow"
6 "strconv"
7)
8
9// QuoteBuy is the exact ugnot a buyer must attach to mint amount tickets.
10// due = curve + creator fee + protocol fee + referral fee.
11// curve is what stays in the reserve. Fees are paid on top and forwarded.
12func QuoteBuy(handle string, amount int) (due, curve, creatorFee, protocolFee, referralFee int64) {
13 p := lookup(handle)
14 if p == nil {
15 panic("unknown handle")
16 }
17 curve = curveCost(bookOf(p.Addr).Supply, amount)
18 creatorFee, protocolFee, referralFee = splitFees(curve, p.Referrer.IsValid())
19 due = overflow.Add64p(curve, overflow.Add64p(creatorFee, overflow.Add64p(protocolFee, referralFee)))
20 return
21}
22
23// QuoteSell is what a seller receives, and the fees taken out of the curve refund.
24// The reserve drops by the full curve amount, so later sells stay solvent.
25func QuoteSell(handle string, amount int) (proceeds, curve, creatorFee, protocolFee, referralFee int64) {
26 p := lookup(handle)
27 if p == nil {
28 panic("unknown handle")
29 }
30 b := bookOf(p.Addr)
31 if amount < 1 || amount > MaxTrade {
32 panic("amount")
33 }
34 if amount > b.Supply {
35 panic("not enough tickets")
36 }
37 curve = curveCost(b.Supply-amount, amount)
38 creatorFee, protocolFee, referralFee = splitFees(curve, p.Referrer.IsValid())
39 proceeds = curve - creatorFee - protocolFee - referralFee
40 if proceeds <= 0 {
41 panic("proceeds")
42 }
43 return
44}
45
46// Buy mints amount tickets of handle. Attach exactly QuoteBuy's due in ugnot.
47func Buy(cur realm, handle string, amount int) {
48 who := caller(cur)
49 if state.paused {
50 panic("paused")
51 }
52 mustMember(who)
53 due, curve, creatorFee, protocolFee, referralFee := QuoteBuy(handle, amount)
54 sent := takeUgnot(cur)
55 if sent != due {
56 panic("send exactly " + strconv.FormatInt(due, 10) + "ugnot")
57 }
58 p := lookup(handle)
59 b := bookOf(p.Addr)
60 b.Supply += amount
61 b.Reserve = overflow.Add64p(b.Reserve, curve)
62 state.totalReserve = overflow.Add64p(state.totalReserve, curve)
63 addShares(who, p.Addr, amount)
64 forwardFees(cur, p, creatorFee, protocolFee, referralFee)
65 chain.Emit(
66 "ArenaBuy",
67 "buyer", who.String(),
68 "creator", p.Addr.String(),
69 "handle", p.Handle,
70 "amount", strconv.Itoa(amount),
71 "curve", strconv.FormatInt(curve, 10),
72 "due", strconv.FormatInt(due, 10),
73 )
74}
75
76// Sell burns amount tickets and pays the caller from the reserve, minus fees.
77// Sells stay available while the realm is paused.
78func Sell(cur realm, handle string, amount int) {
79 who := caller(cur)
80 mustNoPay()
81 mustMember(who)
82 proceeds, curve, creatorFee, protocolFee, referralFee := QuoteSell(handle, amount)
83 if getShares(who, lookup(handle).Addr) < amount {
84 panic("not enough tickets")
85 }
86 p := lookup(handle)
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, referralFee)
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, referralFee 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 referralFee > 0 {
115 payout(cur, creator.Referrer, referralFee, "referrer")
116 }
117}
118
119func addShares(holder, creator address, n int) {
120 if n < 1 {
121 panic("amount")
122 }
123 had := getShares(holder, creator)
124 setShares(holder, creator, had+n)
125 if had == 0 {
126 bookOf(creator).Holders++
127 }
128}
129
130func subShares(holder, creator address, n int) {
131 had := getShares(holder, creator)
132 if n < 1 || had < n {
133 panic("not enough tickets")
134 }
135 setShares(holder, creator, had-n)
136 if had == n {
137 b := bookOf(creator)
138 if b.Holders > 0 {
139 b.Holders--
140 }
141 }
142}
143
144func splitFees(curve int64, referred bool) (creator, protocol, referral int64) {
145 creator = bps(curve, CreatorFeeBps)
146 protocol = bps(curve, ProtocolFeeBps)
147 if referred {
148 referral = bps(curve, ReferralFeeBps)
149 }
150 return
151}
152
153func bps(amount, points int64) int64 {
154 if amount == 0 || points == 0 {
155 return 0
156 }
157 if amount < 0 || points < 0 {
158 panic("fee")
159 }
160 return overflow.Mul64p(amount, points) / 10_000
161}
162
163// curveCost is the ugnot reserve needed to move `amount` tickets starting at supply.
164// Ticket i (1-based) is priced i² * TicketUnit. The sum of squares is exact.
165func curveCost(supply, amount int) int64 {
166 if amount < 1 || amount > MaxTrade {
167 panic("amount")
168 }
169 if supply < 0 {
170 panic("supply")
171 }
172 from := int64(supply)
173 to := from + int64(amount)
174 if to > int64(MaxSupply) {
175 panic("supply cap")
176 }
177 return overflow.Mul64p(sumSq(to)-sumSq(from), TicketUnit)
178}
179
180func sumSq(n int64) int64 {
181 if n <= 0 {
182 return 0
183 }
184 if n > int64(MaxSupply) {
185 panic("supply cap")
186 }
187 // n*(n+1)*(2n+1)/6, dividing 2 and 3 out before multiplying.
188 a := n
189 b := n + 1
190 if a%2 == 0 {
191 a /= 2
192 } else {
193 b /= 2
194 }
195 c := 2*n + 1
196 if a%3 == 0 {
197 a /= 3
198 } else if b%3 == 0 {
199 b /= 3
200 } else if c%3 == 0 {
201 c /= 3
202 } else {
203 panic("sumSq")
204 }
205 return overflow.Mul64p(overflow.Mul64p(a, b), c)
206}