fairmath.gno
5.13 Kb · 163 lines
1// Package fairmath prices whole-lot fair-sale commitments without holding
2// funds. No sale is active until a separate realm accounts for custody and
3// exit rights. All amounts are token or quote base units.
4package fairmath
5
6import sm "gno.land/p/samcrew/launchpad/safemath/v1"
7
8const MaxWindowSeconds int64 = 365 * 24 * 60 * 60
9
10// Schedule defines quote atoms per lot. A zero decrement is a fixed-price
11// sale; otherwise the price falls by Decrement every IntervalSeconds, never
12// below FloorPrice. End is the scheduled close instant. An inventory or hard-
13// cap close must pass its recorded transaction time to PriceAt instead.
14type Schedule struct {
15 Start int64
16 End int64
17 StartPrice int64
18 FloorPrice int64
19 Decrement int64
20 IntervalSeconds int64
21}
22
23func (s Schedule) Validate() {
24 if s.Start < 0 || s.End <= s.Start || s.End-s.Start > MaxWindowSeconds ||
25 s.StartPrice <= 0 || s.FloorPrice <= 0 || s.FloorPrice > s.StartPrice ||
26 s.Decrement < 0 || s.IntervalSeconds <= 0 ||
27 (s.Decrement == 0 && s.FloorPrice != s.StartPrice) {
28 panic("launchpad/fairmath: invalid schedule")
29 }
30}
31
32func (s Schedule) PriceAt(at int64) int64 {
33 s.Validate()
34 if at < s.Start || at > s.End {
35 panic("launchpad/fairmath: time outside sale")
36 }
37 if s.Decrement == 0 {
38 return s.StartPrice
39 }
40 steps := (at - s.Start) / s.IntervalSeconds
41 gap := sm.Sub(s.StartPrice, s.FloorPrice)
42 stepsToFloor := sm.MulDivUp(gap, 1, s.Decrement)
43 if steps >= stepsToFloor {
44 return s.FloorPrice
45 }
46 return sm.Sub(s.StartPrice, sm.Mul(steps, s.Decrement))
47}
48
49// MaxLots requires the escrowed sale allocation to consist of whole lots.
50// No token dust is created by dividing the allocation between buyers.
51func MaxLots(allocation, lotSize int64) int64 {
52 if allocation <= 0 || lotSize <= 0 || allocation%lotSize != 0 {
53 panic("launchpad/fairmath: invalid lot allocation")
54 }
55 return allocation / lotSize
56}
57
58type Order struct {
59 Lots int64
60 Tokens int64
61 Deposit int64
62}
63
64// Caps snapshots the sale's hard cap in whole lots and soft cap in quote
65// atoms actually spent at the final clearing price. Entry deposits and later
66// refunds do not count toward the soft cap.
67type Caps struct {
68 HardLots int64
69 SoftQuote int64
70}
71
72func (c Caps) Validate(allocation, lotSize, highestPrice int64) {
73 maxLots := MaxLots(allocation, lotSize)
74 if c.HardLots <= 0 || c.HardLots > maxLots || c.SoftQuote <= 0 ||
75 highestPrice <= 0 {
76 panic("launchpad/fairmath: invalid caps")
77 }
78 if c.SoftQuote > sm.Mul(c.HardLots, highestPrice) {
79 panic("launchpad/fairmath: unreachable soft cap")
80 }
81}
82
83// AddLots rejects an order that would exceed the hard cap. Equality signals
84// an early close at the contribution transaction's block time.
85func (c Caps) AddLots(current, order int64) (int64, bool) {
86 if c.HardLots <= 0 || current < 0 || current > c.HardLots || order <= 0 {
87 panic("launchpad/fairmath: invalid cap state")
88 }
89 next := sm.Add(current, order)
90 if next > c.HardLots {
91 panic("launchpad/fairmath: hard cap exceeded")
92 }
93 return next, next == c.HardLots
94}
95
96// QuoteOrder gives the exact token and quote amounts for a whole-lot order.
97func QuoteOrder(lots, lotSize, entryPrice int64) Order {
98 if lots <= 0 || lotSize <= 0 || entryPrice <= 0 {
99 panic("launchpad/fairmath: invalid order")
100 }
101 return Order{
102 Lots: lots, Tokens: sm.Mul(lots, lotSize),
103 Deposit: sm.Mul(lots, entryPrice),
104 }
105}
106
107type Settlement struct {
108 Succeeded bool
109 SoldTokens int64
110 GrossQuote int64
111 RefundQuote int64
112 UnsoldTokens int64
113}
114
115// Clear partitions all whole-lot commitments at one pinned clearing price.
116// A missed soft cap returns all deposits and all tokens. On success,
117// deposits above the clearing-price gross remain buyer refund liabilities.
118func Clear(c Caps, allocation, lotSize, totalLots, totalDeposits, closePrice int64) Settlement {
119 maxLots := MaxLots(allocation, lotSize)
120 if c.HardLots <= 0 || c.HardLots > maxLots || c.SoftQuote <= 0 ||
121 totalLots < 0 || totalLots > c.HardLots || totalDeposits < 0 || closePrice <= 0 {
122 panic("launchpad/fairmath: invalid settlement")
123 }
124 sold := sm.Mul(totalLots, lotSize)
125 gross := sm.Mul(totalLots, closePrice)
126 if totalDeposits < gross {
127 panic("launchpad/fairmath: unfunded settlement")
128 }
129 if gross < c.SoftQuote {
130 return Settlement{RefundQuote: totalDeposits, UnsoldTokens: allocation}
131 }
132 return Settlement{
133 Succeeded: true,
134 SoldTokens: sold, GrossQuote: gross,
135 RefundQuote: sm.Sub(totalDeposits, gross),
136 UnsoldTokens: sm.Sub(allocation, sold),
137 }
138}
139
140type BuyerClaim struct {
141 Tokens int64
142 SpentQuote int64
143 RefundQuote int64
144}
145
146// Claim computes one buyer's exact rights after Clear. Claims over the same
147// orders telescope to its aggregate settlement, regardless of claim order.
148func Claim(success bool, lots, deposit, lotSize, closePrice int64) BuyerClaim {
149 if lots <= 0 || deposit <= 0 || lotSize <= 0 || closePrice <= 0 {
150 panic("launchpad/fairmath: invalid claim")
151 }
152 if !success {
153 return BuyerClaim{RefundQuote: deposit}
154 }
155 spent := sm.Mul(lots, closePrice)
156 if deposit < spent {
157 panic("launchpad/fairmath: unfunded claim")
158 }
159 return BuyerClaim{
160 Tokens: sm.Mul(lots, lotSize), SpentQuote: spent,
161 RefundQuote: sm.Sub(deposit, spent),
162 }
163}