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.

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) }