Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}