sale.gno
7.60 Kb · 277 lines
1package guardians_pilot_v1
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 saleDenom = "ugnot"
19)
20
21type saleConfiguration struct {
22 allowlistStart int64
23 allowlistEnd int64
24 publicStart int64
25 publicEnd int64
26 allowlistPrice int64
27 publicPrice int64
28 allowlistLimit int
29 publicLimit int
30 treasury address
31}
32
33var (
34 saleConfig saleConfiguration
35 saleConfigured bool
36 salePaused = true
37 allowlistMintedByWallet avl.Tree
38 publicMintedByWallet avl.Tree
39)
40
41// ConfigureSale stores timestamps as Unix seconds and prices in ugnot.
42// Configuration is only mutable while the sale is paused.
43func ConfigureSale(
44 cur realm,
45 allowlistStart, allowlistEnd int64,
46 publicStart, publicEnd int64,
47 allowlistPrice, publicPrice int64,
48 allowlistLimit, publicLimit int,
49 treasury address,
50) {
51 assertAdmin(cur.Previous().Address())
52 if !salePaused {
53 panic("guardians: pause sale before configuration")
54 }
55 if !validSaleSchedule(allowlistStart, allowlistEnd, publicStart, publicEnd) {
56 panic("guardians: invalid sale schedule")
57 }
58 if !validAllowlistPrice(allowlistPrice) || !validSalePrice(publicPrice) {
59 panic("guardians: invalid sale price")
60 }
61 if !validWalletLimit(allowlistLimit) || !validWalletLimit(publicLimit) {
62 panic("guardians: invalid wallet limit")
63 }
64 if allowlistPrice != 0 || publicPrice != 1000000 || allowlistLimit != 1 || publicLimit != 3 {
65 panic("guardians: pilot requires free 1 + paid 3 at 1 GNOT")
66 }
67 if treasury != address("g1ht2848ahr20tqq7086thg5h9vrw4836suzard8") {
68 panic("guardians: unexpected pilot treasury")
69 }
70 if !treasury.IsValid() {
71 panic("guardians: invalid sale treasury")
72 }
73
74 saleConfig = saleConfiguration{
75 allowlistStart: allowlistStart,
76 allowlistEnd: allowlistEnd,
77 publicStart: publicStart,
78 publicEnd: publicEnd,
79 allowlistPrice: allowlistPrice,
80 publicPrice: publicPrice,
81 allowlistLimit: allowlistLimit,
82 publicLimit: publicLimit,
83 treasury: treasury,
84 }
85 saleConfigured = true
86
87 chain.Emit(
88 "SaleConfigured",
89 "allowlistStart", ufmt.Sprintf("%d", allowlistStart),
90 "allowlistEnd", ufmt.Sprintf("%d", allowlistEnd),
91 "publicStart", ufmt.Sprintf("%d", publicStart),
92 "publicEnd", ufmt.Sprintf("%d", publicEnd),
93 "treasury", treasury.String(),
94 )
95}
96
97func SetSalePaused(cur realm, paused bool) {
98 assertAdmin(cur.Previous().Address())
99 if !paused && !saleConfigured {
100 panic("guardians: configure sale before unpausing")
101 }
102 if !paused && allowlistRoot == "" {
103 panic("guardians: configure allowlist root before unpausing")
104 }
105 if !paused && revealState != revealStateCommitted {
106 panic("guardians: commit reveal configuration before unpausing")
107 }
108 if !paused {
109 allowlistRootLocked = true
110 }
111 salePaused = paused
112 chain.Emit("SalePauseChanged", "paused", ufmt.Sprintf("%t", paused))
113}
114
115func SalePhase() string { return salePhaseAt(time.Now().Unix()) }
116
117// Activate once, after metadata and proofs are ready. Both purchase modes share
118// the same on-chain 24-hour window beginning in this activation transaction.
119func ActivatePilotSale(cur realm) {
120 assertAdmin(cur.Previous().Address())
121 if saleConfigured {
122 panic("guardians: pilot sale already configured")
123 }
124 start := time.Now().Unix()
125 end := start + 86400
126 ConfigureSale(cur, start, end, start, end, 0, 1000000, 1, 3, address("g1ht2848ahr20tqq7086thg5h9vrw4836suzard8"))
127 SetSalePaused(cur, false)
128}
129
130func SalePhaseAt(unixTime int64) string { return salePhaseAt(unixTime) }
131
132func salePhaseAt(unixTime int64) string {
133 if salePaused || !saleConfigured {
134 return salePhaseClosed
135 }
136 if unixTime >= saleConfig.publicStart && unixTime < saleConfig.publicEnd {
137 return salePhaseOpen
138 }
139 return salePhaseClosed
140}
141
142func SalePaused() bool { return salePaused }
143
144func SaleConfigured() bool { return saleConfigured }
145
146func SaleTreasury() address { return saleConfig.treasury }
147
148func AllowlistStart() int64 { return saleConfig.allowlistStart }
149
150func AllowlistEnd() int64 { return saleConfig.allowlistEnd }
151
152func PublicStart() int64 { return saleConfig.publicStart }
153
154func PublicEnd() int64 { return saleConfig.publicEnd }
155
156func AllowlistPrice() int64 { return saleConfig.allowlistPrice }
157
158func PublicPrice() int64 { return saleConfig.publicPrice }
159
160func AllowlistWalletLimit() int { return saleConfig.allowlistLimit }
161
162func PublicWalletLimit() int { return saleConfig.publicLimit }
163
164func AllowlistMinted(owner address) int { return mintedCount(&allowlistMintedByWallet, owner) }
165
166func PublicMinted(owner address) int { return mintedCount(&publicMintedByWallet, owner) }
167
168func NextSaleTimestamp() int64 {
169 now := time.Now().Unix()
170 if !saleConfigured {
171 return 0
172 }
173 if now < saleConfig.allowlistStart {
174 return saleConfig.allowlistStart
175 }
176 if now < saleConfig.allowlistEnd {
177 return saleConfig.allowlistEnd
178 }
179 if now < saleConfig.publicStart {
180 return saleConfig.publicStart
181 }
182 if now < saleConfig.publicEnd {
183 return saleConfig.publicEnd
184 }
185 return 0
186}
187
188// MintPublic accepts only direct Adena/EOA calls. This pairing is critical:
189// OriginSend describes actual receipt by this realm only for a direct user call.
190func MintPublic(cur realm, quantity int) {
191 if !cur.Previous().IsUserCall() {
192 panic("guardians: only direct user calls can mint")
193 }
194 if SalePhase() != salePhaseOpen {
195 panic("guardians: public sale is not active")
196 }
197
198 caller := cur.Previous().Address()
199 assertSaleMint(quantity, PublicMinted(caller), saleConfig.publicLimit)
200 required := requiredPayment(saleConfig.publicPrice, quantity)
201 sent := unsafe.OriginSend()
202 assertExactPayment(sent, required)
203
204 mintTokens(caller, quantity)
205 publicMintedByWallet.Set(caller.String(), PublicMinted(caller)+quantity)
206 forwardPayment(cur, sent)
207
208 chain.Emit(
209 "PublicMint",
210 "minter", caller.String(),
211 "quantity", ufmt.Sprintf("%d", quantity),
212 "paid", ufmt.Sprintf("%d", required),
213 )
214}
215
216func assertSaleMint(quantity, alreadyMinted, walletLimit int) {
217 assertMintQuantity(quantity)
218 if alreadyMinted+quantity > walletLimit {
219 panic("guardians: wallet mint limit exceeded")
220 }
221}
222
223func requiredPayment(unitPrice int64, quantity int) int64 {
224 if unitPrice < 0 || !validMintQuantity(quantity) {
225 panic("guardians: invalid payment calculation")
226 }
227 return unitPrice * int64(quantity)
228}
229
230func assertExactPayment(sent chain.Coins, required int64) {
231 if required == 0 {
232 if len(sent) != 0 {
233 panic("guardians: free mint must not include payment")
234 }
235 return
236 }
237 if !isExactPayment(sent, required) {
238 panic(ufmt.Sprintf("guardians: exact payment required: %dugnot", required))
239 }
240}
241
242func isExactPayment(sent chain.Coins, required int64) bool {
243 return len(sent) == 1 && sent[0].Denom == saleDenom && sent[0].Amount == required
244}
245
246func forwardPayment(cur realm, sent chain.Coins) {
247 if len(sent) == 0 {
248 return
249 }
250 b := banker.NewBanker(banker.BankerTypeRealmSend, cur)
251 b.SendCoins(cur.Address(), saleConfig.treasury, sent)
252}
253
254func mintedCount(tree *avl.Tree, owner address) int {
255 v := tree.Get(owner.String())
256 if v == nil {
257 return 0
258 }
259 return v.(int)
260}
261
262func validSaleSchedule(allowlistStart, allowlistEnd, publicStart, publicEnd int64) bool {
263 return allowlistStart > 0 && allowlistStart == publicStart &&
264 allowlistEnd == publicEnd && publicEnd- publicStart == 86400
265}
266
267func validSalePrice(price int64) bool {
268 return price > 0 && price <= 9223372036854775807/int64(maxMintBatch)
269}
270
271func validAllowlistPrice(price int64) bool {
272 return price >= 0 && price <= 9223372036854775807/int64(maxMintBatch)
273}
274
275func validWalletLimit(limit int) bool {
276 return limit > 0 && int64(limit) <= maxSupply
277}