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