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