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}