1// Package fairmath prices whole-lot fair-sale commitments without holding2// funds. No sale is active until a separate realm accounts for custody and3// exit rights. All amounts are token or quote base units.4package fairmath56import sm "gno.land/p/samcrew/launchpad/safemath/v1"78const MaxWindowSeconds int64 = 365 * 24 * 60 * 60910// Schedule defines quote atoms per lot. A zero decrement is a fixed-price11// sale; otherwise the price falls by Decrement every IntervalSeconds, never12// 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 int6416 End int6417 StartPrice int6418 FloorPrice int6419 Decrement int6420 IntervalSeconds int6421}2223func (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}3132func (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.StartPrice39 }40 steps := (at - s.Start) / s.IntervalSeconds41 gap := sm.Sub(s.StartPrice, s.FloorPrice)42 stepsToFloor := sm.MulDivUp(gap, 1, s.Decrement)43 if steps >= stepsToFloor {44 return s.FloorPrice45 }46 return sm.Sub(s.StartPrice, sm.Mul(steps, s.Decrement))47}4849// 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 / lotSize56}5758type Order struct {59 Lots int6460 Tokens int6461 Deposit int6462}6364// Caps snapshots the sale's hard cap in whole lots and soft cap in quote65// atoms actually spent at the final clearing price. Entry deposits and later66// refunds do not count toward the soft cap.67type Caps struct {68 HardLots int6469 SoftQuote int6470}7172func (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}8283// AddLots rejects an order that would exceed the hard cap. Equality signals84// 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.HardLots94}9596// 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}106107type Settlement struct {108 Succeeded bool109 SoldTokens int64110 GrossQuote int64111 RefundQuote int64112 UnsoldTokens int64113}114115// 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}139140type BuyerClaim struct {141 Tokens int64142 SpentQuote int64143 RefundQuote int64144}145146// Claim computes one buyer's exact rights after Clear. Claims over the same147// 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}164Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.