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

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}