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.

sale.gno

8.44 Kb · 298 lines
  1package guardians
  2
  3import (
  4	"chain"
  5	"chain/banker"
  6	"chain/runtime/unsafe"
  7	"time"
  8
  9	"gno.land/p/nt/avl/v0"
 10	"gno.land/p/nt/ufmt/v0"
 11)
 12
 13const (
 14	salePhaseClosed    = "CLOSED"
 15	salePhaseAllowlist = "ALLOWLIST"
 16	salePhasePublic    = "PUBLIC"
 17	salePhaseOpen      = "OPEN"
 18	salePhaseSoldOut   = "SOLD_OUT"
 19	saleDenom          = "ugnot"
 20)
 21
 22type saleConfiguration struct {
 23	allowlistStart int64
 24	allowlistEnd   int64
 25	publicStart    int64
 26	publicEnd      int64
 27	allowlistPrice int64
 28	publicPrice    int64
 29	allowlistLimit int
 30	publicLimit    int
 31	treasury       address
 32}
 33
 34var (
 35	saleConfig              saleConfiguration
 36	saleConfigured          bool
 37	salePaused              = true
 38	allowlistMintedByWallet avl.Tree
 39	publicMintedByWallet    avl.Tree
 40)
 41
 42// ConfigureSale stores timestamps as Unix seconds and prices in ugnot.
 43// Configuration is only mutable while the sale is paused. An end timestamp
 44// of zero means no announced deadline, not an already-ended sale.
 45func ConfigureSale(
 46	cur realm,
 47	allowlistStart, allowlistEnd int64,
 48	publicStart, publicEnd int64,
 49	allowlistPrice, publicPrice int64,
 50	allowlistLimit, publicLimit int,
 51	treasury address,
 52) {
 53	assertAdmin(cur.Previous().Address())
 54	if !salePaused {
 55		panic("guardians: pause sale before configuration")
 56	}
 57	if !validSaleSchedule(allowlistStart, allowlistEnd, publicStart, publicEnd) {
 58		panic("guardians: invalid sale schedule")
 59	}
 60	if !validAllowlistPrice(allowlistPrice) || !validSalePrice(publicPrice) {
 61		panic("guardians: invalid sale price")
 62	}
 63	if !validWalletLimit(allowlistLimit) || !validWalletLimit(publicLimit) {
 64		panic("guardians: invalid wallet limit")
 65	}
 66	if !treasury.IsValid() {
 67		panic("guardians: invalid sale treasury")
 68	}
 69
 70	saleConfig = saleConfiguration{
 71		allowlistStart: allowlistStart,
 72		allowlistEnd:   allowlistEnd,
 73		publicStart:    publicStart,
 74		publicEnd:      publicEnd,
 75		allowlistPrice: allowlistPrice,
 76		publicPrice:    publicPrice,
 77		allowlistLimit: allowlistLimit,
 78		publicLimit:    publicLimit,
 79		treasury:       treasury,
 80	}
 81	saleConfigured = true
 82
 83	chain.Emit(
 84		"SaleConfigured",
 85		"allowlistStart", ufmt.Sprintf("%d", allowlistStart),
 86		"allowlistEnd", ufmt.Sprintf("%d", allowlistEnd),
 87		"publicStart", ufmt.Sprintf("%d", publicStart),
 88		"publicEnd", ufmt.Sprintf("%d", publicEnd),
 89		"treasury", treasury.String(),
 90	)
 91}
 92
 93func SetSalePaused(cur realm, paused bool) {
 94	assertAdmin(cur.Previous().Address())
 95	if !paused && !saleConfigured {
 96		panic("guardians: configure sale before unpausing")
 97	}
 98	if !paused && allowlistRoot == "" {
 99		panic("guardians: configure allowlist root before unpausing")
100	}
101	if !paused && revealState != revealStateCommitted {
102		panic("guardians: commit reveal configuration before unpausing")
103	}
104	if !paused {
105		allowlistRootLocked = true
106	}
107	salePaused = paused
108	chain.Emit("SalePauseChanged", "paused", ufmt.Sprintf("%t", paused))
109}
110
111func SalePhase() string { return salePhaseAt(time.Now().Unix()) }
112
113func SalePhaseAt(unixTime int64) string { return salePhaseAt(unixTime) }
114
115func salePhaseAt(unixTime int64) string {
116	if token.TotalSupply() >= maxSupply {
117		return salePhaseSoldOut
118	}
119	if salePaused || !saleConfigured {
120		return salePhaseClosed
121	}
122	allowlistOpen := allowlistSaleOpenAt(unixTime)
123	publicOpen := publicSaleOpenAt(unixTime)
124	if allowlistOpen && publicOpen {
125		return salePhaseOpen
126	}
127	if allowlistOpen {
128		return salePhaseAllowlist
129	}
130	if publicOpen {
131		return salePhasePublic
132	}
133	return salePhaseClosed
134}
135
136func publicSaleOpenAt(unixTime int64) bool {
137	return saleConfigured && !salePaused && unixTime >= saleConfig.publicStart &&
138		(saleConfig.publicEnd == 0 || unixTime < saleConfig.publicEnd)
139}
140
141func allowlistSaleOpenAt(unixTime int64) bool {
142	return saleConfigured && !salePaused && unixTime >= saleConfig.allowlistStart &&
143		(saleConfig.allowlistEnd == 0 || unixTime < saleConfig.allowlistEnd) &&
144		(saleConfig.publicEnd == 0 || unixTime < saleConfig.publicEnd)
145}
146
147// SetPublicEnd announces a deadline without pausing or changing price, wallet
148// limits, roots, or counters. A completed sale cannot be reopened this way.
149// Set end to the current block time to close immediately, or a future time
150// to announce an end countdown. Zero is reserved for initial configuration.
151func SetPublicEnd(cur realm, end int64) {
152	assertAdmin(cur.Previous().Address())
153	if !saleConfigured || revealState != revealStateCommitted {
154		panic("guardians: sale is not configurable")
155	}
156	now := time.Now().Unix()
157	if saleConfig.publicEnd > 0 && now >= saleConfig.publicEnd {
158		panic("guardians: closed sale cannot be reopened")
159	}
160	if end < now || end <= saleConfig.publicStart {
161		panic("guardians: invalid public end timestamp")
162	}
163	saleConfig.publicEnd = end
164	chain.Emit("PublicEndConfigured", "publicEnd", ufmt.Sprintf("%d", end))
165}
166
167func SalePaused() bool { return salePaused }
168
169func SaleConfigured() bool { return saleConfigured }
170
171func SaleTreasury() address { return saleConfig.treasury }
172
173func AllowlistStart() int64 { return saleConfig.allowlistStart }
174
175func AllowlistEnd() int64 { return saleConfig.allowlistEnd }
176
177func PublicStart() int64 { return saleConfig.publicStart }
178
179func PublicEnd() int64 { return saleConfig.publicEnd }
180
181func AllowlistPrice() int64 { return saleConfig.allowlistPrice }
182
183func PublicPrice() int64 { return saleConfig.publicPrice }
184
185func AllowlistWalletLimit() int { return saleConfig.allowlistLimit }
186
187func PublicWalletLimit() int { return saleConfig.publicLimit }
188
189func AllowlistMinted(owner address) int { return mintedCount(&allowlistMintedByWallet, owner) }
190
191func PublicMinted(owner address) int { return mintedCount(&publicMintedByWallet, owner) }
192
193func NextSaleTimestamp() int64 {
194	now := time.Now().Unix()
195	if !saleConfigured {
196		return 0
197	}
198	next := int64(0)
199	for _, timestamp := range []int64{saleConfig.allowlistStart, saleConfig.allowlistEnd, saleConfig.publicStart, saleConfig.publicEnd} {
200		if timestamp > now && (next == 0 || timestamp < next) {
201			next = timestamp
202		}
203	}
204	return next
205}
206
207// MintPublic accepts only direct Adena/EOA calls. This pairing is critical:
208// OriginSend describes actual receipt by this realm only for a direct user call.
209func MintPublic(cur realm, quantity int) {
210	if !cur.Previous().IsUserCall() {
211		panic("guardians: only direct user calls can mint")
212	}
213	if !publicSaleOpenAt(time.Now().Unix()) {
214		panic("guardians: public sale is not active")
215	}
216
217	caller := cur.Previous().Address()
218	assertSaleMint(quantity, PublicMinted(caller), saleConfig.publicLimit)
219	required := requiredPayment(saleConfig.publicPrice, quantity)
220	sent := unsafe.OriginSend()
221	assertExactPayment(sent, required)
222
223	mintTokens(caller, quantity)
224	publicMintedByWallet.Set(caller.String(), PublicMinted(caller)+quantity)
225	forwardPayment(cur, sent)
226
227	chain.Emit(
228		"PublicMint",
229		"minter", caller.String(),
230		"quantity", ufmt.Sprintf("%d", quantity),
231		"paid", ufmt.Sprintf("%d", required),
232	)
233}
234
235func assertSaleMint(quantity, alreadyMinted, walletLimit int) {
236	assertMintQuantity(quantity)
237	if alreadyMinted+quantity > walletLimit {
238		panic("guardians: wallet mint limit exceeded")
239	}
240}
241
242func requiredPayment(unitPrice int64, quantity int) int64 {
243	if unitPrice < 0 || !validMintQuantity(quantity) {
244		panic("guardians: invalid payment calculation")
245	}
246	return unitPrice * int64(quantity)
247}
248
249func assertExactPayment(sent chain.Coins, required int64) {
250	if required == 0 {
251		if len(sent) != 0 {
252			panic("guardians: free mint must not include payment")
253		}
254		return
255	}
256	if !isExactPayment(sent, required) {
257		panic(ufmt.Sprintf("guardians: exact payment required: %dugnot", required))
258	}
259}
260
261func isExactPayment(sent chain.Coins, required int64) bool {
262	return len(sent) == 1 && sent[0].Denom == saleDenom && sent[0].Amount == required
263}
264
265func forwardPayment(cur realm, sent chain.Coins) {
266	if len(sent) == 0 {
267		return
268	}
269	b := banker.NewBanker(banker.BankerTypeRealmSend, cur)
270	b.SendCoins(cur.Address(), saleConfig.treasury, sent)
271}
272
273func mintedCount(tree *avl.Tree, owner address) int {
274	v := tree.Get(owner.String())
275	if v == nil {
276		return 0
277	}
278	return v.(int)
279}
280
281func validSaleSchedule(allowlistStart, allowlistEnd, publicStart, publicEnd int64) bool {
282	return allowlistStart > 0 &&
283		publicStart >= allowlistStart &&
284		(allowlistEnd == 0 || allowlistStart < allowlistEnd) &&
285		(publicEnd == 0 || publicStart < publicEnd)
286}
287
288func validSalePrice(price int64) bool {
289	return price > 0 && price <= 9223372036854775807/int64(maxMintBatch)
290}
291
292func validAllowlistPrice(price int64) bool {
293	return price >= 0 && price <= 9223372036854775807/int64(maxMintBatch)
294}
295
296func validWalletLimit(limit int) bool {
297	return limit > 0 && int64(limit) <= maxSupply
298}