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}