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

eligibility.gno

2.87 Kb · 96 lines
 1package sales
 2
 3import (
 4	"strconv"
 5	"strings"
 6
 7	"gno.land/p/samcrew/launchpad/merkle/v1"
 8	"gno.land/r/samcrew/launchpad/tokens/v1"
 9)
10
11const maxFairHoldingGateBytes = 256
12const maxFairProofBytes = merkle.MaxDepth*64 + merkle.MaxDepth - 1
13
14type fairHoldingGate struct {
15	TokenID string
16	Minimum int64
17}
18
19// Gates are at most three canonical, strictly token-ID-ordered records:
20// token:T<n>:<positive base units>. Collection gates remain unavailable until
21// the collection realm and its BalanceOf interface are implemented.
22func parseFairHoldingGates(spec string) []fairHoldingGate {
23	if spec == "" {
24		return nil
25	}
26	if len(spec) > maxFairHoldingGateBytes {
27		panic("launchpad/sales: invalid fair holding gates")
28	}
29	parts := strings.Split(spec, ";")
30	if len(parts) > 3 {
31		panic("launchpad/sales: invalid fair holding gates")
32	}
33	gates := make([]fairHoldingGate, len(parts))
34	previous := ""
35	for i, part := range parts {
36		fields := strings.Split(part, ":")
37		if len(fields) != 3 || fields[0] != "token" || fields[1] == "" ||
38			(previous != "" && fields[1] <= previous) {
39			panic("launchpad/sales: invalid fair holding gates")
40		}
41		minimum, err := strconv.ParseInt(fields[2], 10, 64)
42		if err != nil || minimum <= 0 || strconv.FormatInt(minimum, 10) != fields[2] {
43			panic("launchpad/sales: invalid fair holding gates")
44		}
45		tokens.GetInfo(fields[1])
46		gates[i] = fairHoldingGate{TokenID: fields[1], Minimum: minimum}
47		previous = fields[1]
48	}
49	return gates
50}
51
52func validateFairEligibility(root, gates string) {
53	if root != "" {
54		h, err := merkle.ParseHash(root)
55		if err != nil || merkle.FormatHash(h) != root || h == (merkle.Hash{}) {
56			panic("launchpad/sales: invalid fair allowlist root")
57		}
58	}
59	parseFairHoldingGates(gates)
60}
61
62// The leaf is sha256(0x00 || "launchpad-fair-v1|<token-id>|<buyer>").
63// The token ID prevents a valid proof from being reused for a different sale.
64func requireFairEligibility(id string, r FairSaleRecord, buyer address, proof string) {
65	if r.AllowlistRoot == "" {
66		if proof != "" {
67			panic("launchpad/sales: fair proof not expected")
68		}
69	} else {
70		if len(proof) > maxFairProofBytes {
71			panic("launchpad/sales: invalid fair allowlist proof")
72		}
73		parsed, err := merkle.ParseProof(proof)
74		if err != nil {
75			panic("launchpad/sales: invalid fair allowlist proof")
76		}
77		if proof != "" {
78			parts := strings.Split(proof, ",")
79			for i, sibling := range parsed {
80				if merkle.FormatHash(sibling) != parts[i] {
81					panic("launchpad/sales: invalid fair allowlist proof")
82				}
83			}
84		}
85		root, _ := merkle.ParseHash(r.AllowlistRoot)
86		leaf := merkle.Leaf([]byte("launchpad-fair-v1|" + id + "|" + buyer.String()))
87		if !merkle.Verify(root, leaf, parsed) {
88			panic("launchpad/sales: fair allowlist denied")
89		}
90	}
91	for _, gate := range parseFairHoldingGates(r.HoldingGates) {
92		if tokens.BalanceOf(gate.TokenID, buyer) < gate.Minimum {
93			panic("launchpad/sales: fair holding gate denied")
94		}
95	}
96}