allowlist.gno
4.24 Kb · 159 lines
1package guardians
2
3import (
4 "chain"
5 "chain/runtime"
6 "chain/runtime/unsafe"
7 "crypto/sha256"
8 "encoding/hex"
9 "strings"
10 "time"
11
12 "gno.land/p/nt/ufmt/v0"
13)
14
15const maxMerkleProofDepth = 32
16
17var (
18 allowlistRoot string
19 allowlistRootLocked bool
20)
21
22func AllowlistRoot() string { return allowlistRoot }
23
24func AllowlistRootLocked() bool { return allowlistRootLocked }
25
26// SetAllowlistRoot accepts a lowercase/uppercase 32-byte SHA-256 hash in hex.
27// The first sale unpause permanently locks the root for this deployment.
28func SetAllowlistRoot(cur realm, root string) {
29 assertAdmin(cur.Previous().Address())
30 if !salePaused {
31 panic("guardians: pause sale before setting allowlist root")
32 }
33 if allowlistRootLocked {
34 panic("guardians: allowlist root is locked")
35 }
36 decoded, ok := decodeMerkleHash(root)
37 if !ok || len(decoded) != sha256.Size {
38 panic("guardians: invalid allowlist root")
39 }
40 allowlistRoot = hex.EncodeToString(decoded)
41 chain.Emit("AllowlistRootConfigured", "root", allowlistRoot)
42}
43
44// AllowlistLeaf binds a claim to this chain, collection, phase, address,
45// individual allowance and configured unit price.
46func AllowlistLeaf(owner address, allowance int, unitPrice int64) string {
47 payload := runtime.ChainID() + "|" +
48 CollectionID() + "|" +
49 salePhaseAllowlist + "|" +
50 owner.String() + "|" +
51 ufmt.Sprintf("%d", allowance) + "|" +
52 ufmt.Sprintf("%d", unitPrice)
53 return hashBytes([]byte(payload))
54}
55
56// VerifyAllowlist expects comma-separated 64-character hex sibling hashes.
57// Pair hashing is sorted, so proofs do not need left/right direction flags.
58func VerifyAllowlist(owner address, allowance int, unitPrice int64, proof string) bool {
59 if allowlistRoot == "" || allowance < 1 || allowance > maxSupplyAsInt() {
60 return false
61 }
62
63 computed := AllowlistLeaf(owner, allowance, unitPrice)
64 siblings, ok := parseMerkleProof(proof)
65 if !ok {
66 return false
67 }
68 for _, sibling := range siblings {
69 computed = hashMerklePair(computed, sibling)
70 }
71 return computed == allowlistRoot
72}
73
74func MintAllowlist(cur realm, quantity, allowance int, unitPrice int64, proof string) {
75 if !cur.Previous().IsUserCall() {
76 panic("guardians: only direct user calls can mint")
77 }
78 if !allowlistSaleOpenAt(time.Now().Unix()) {
79 panic("guardians: allowlist sale is not active")
80 }
81 if unitPrice != saleConfig.allowlistPrice {
82 panic("guardians: allowlist price mismatch")
83 }
84 if allowance < 1 || allowance > saleConfig.allowlistLimit {
85 panic("guardians: invalid allowlist allowance")
86 }
87
88 caller := cur.Previous().Address()
89 if !VerifyAllowlist(caller, allowance, unitPrice, proof) {
90 panic("guardians: invalid allowlist proof")
91 }
92 assertSaleMint(quantity, AllowlistMinted(caller), allowance)
93 required := requiredPayment(unitPrice, quantity)
94 sent := unsafe.OriginSend()
95 assertExactPayment(sent, required)
96
97 mintAllocatedTokens(caller, quantity, 0)
98 allowlistMintedByWallet.Set(caller.String(), AllowlistMinted(caller)+quantity)
99 forwardPayment(cur, sent)
100
101 chain.Emit(
102 "AllowlistMint",
103 "minter", caller.String(),
104 "quantity", ufmt.Sprintf("%d", quantity),
105 "allowance", ufmt.Sprintf("%d", allowance),
106 "paid", ufmt.Sprintf("%d", required),
107 )
108}
109
110func parseMerkleProof(proof string) ([]string, bool) {
111 if proof == "" {
112 return []string{}, true
113 }
114 parts := strings.Split(proof, ",")
115 if len(parts) > maxMerkleProofDepth {
116 return nil, false
117 }
118 for i, part := range parts {
119 decoded, ok := decodeMerkleHash(part)
120 if !ok {
121 return nil, false
122 }
123 parts[i] = hex.EncodeToString(decoded)
124 }
125 return parts, true
126}
127
128func hashMerklePair(first, second string) string {
129 left, leftOK := decodeMerkleHash(first)
130 right, rightOK := decodeMerkleHash(second)
131 if !leftOK || !rightOK {
132 return ""
133 }
134 if second < first {
135 left, right = right, left
136 }
137 combined := make([]byte, 0, sha256.Size*2)
138 combined = append(combined, left...)
139 combined = append(combined, right...)
140 return hashBytes(combined)
141}
142
143func hashBytes(data []byte) string {
144 sum := sha256.Sum256(data)
145 return hex.EncodeToString(sum[:])
146}
147
148func decodeMerkleHash(value string) ([]byte, bool) {
149 if len(value) != sha256.Size*2 {
150 return nil, false
151 }
152 decoded, err := hex.DecodeString(value)
153 if err != nil || len(decoded) != sha256.Size {
154 return nil, false
155 }
156 return decoded, true
157}
158
159func maxSupplyAsInt() int { return int(maxSupply) }