Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}