Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}