airdrop.gno
4.20 Kb · 123 lines
1package sales
2
3import (
4 "strconv"
5 "strings"
6
7 "chain"
8 "gno.land/p/samcrew/launchpad/currency/v1"
9 "gno.land/p/samcrew/launchpad/merkle/v1"
10 "gno.land/p/samcrew/launchpad/meta/v1"
11 sm "gno.land/p/samcrew/launchpad/safemath/v1"
12 "gno.land/r/samcrew/launchpad/config/v1"
13 "gno.land/r/samcrew/launchpad/tokens/v1"
14)
15
16const maxAirdropProofBytes = merkle.MaxDepth*64 + merkle.MaxDepth - 1
17
18// AirdropRecord keeps the whole funded allocation encumbered until recipients
19// claim. Claims never expire, and nothing sweeps or returns an unclaimed
20// remainder.
21type AirdropRecord struct {
22 Root string
23 Total int64
24 Claimed int64
25}
26
27func requireAirdropRoot(root string) {
28 h, err := merkle.ParseHash(root)
29 if err != nil || merkle.FormatHash(h) != root || h == (merkle.Hash{}) {
30 panic("launchpad/sales: invalid airdrop root")
31 }
32}
33
34// CreateAirdrop funds one immutable campaign for a token from its creator's
35// allowance. Its Merkle leaves are sha256(0x00 ||
36// "launchpad-airdrop-v1|<token-id>|<index>|<beneficiary>|<amount>").
37// The token ID in every leaf prevents a proof from authorizing a claim on a
38// different launch, even if two creators publish the same tree shape.
39func CreateAirdrop(cur realm, id, root string, total int64) {
40 creator := directCaller(cur)
41 currency.AssertNoCoins(0, cur)
42 info := tokens.GetInfo(id)
43 if info.Creator != creator || total <= 0 || airdropOf(id).Total != 0 {
44 panic("launchpad/sales: creator airdrop only")
45 }
46 requireAirdropRoot(root)
47 config.AssertLaneOpen(config.LaneAirdrop, info.CurrencyKey)
48 tokens.TransferFrom(cross(cur), id, creator, chain.PackageAddress(salesPath), total)
49 airdrops.Set(id, AirdropRecord{Root: root, Total: total})
50 setTokenOwed(id, sm.Add(tokenOwed(id), total))
51 if tokens.BalanceOf(id, chain.PackageAddress(salesPath)) < tokenOwed(id) {
52 panic("launchpad/sales: token custody shortfall")
53 }
54 chain.Emit("LaunchpadAirdropCreated", "id", id, "creator", creator.String(),
55 "root", root, "total", strconv.FormatInt(total, 10))
56}
57
58func GetAirdrop(id string) AirdropRecord {
59 r := airdropOf(id)
60 if r.Total == 0 {
61 panic("launchpad/sales: airdrop not found")
62 }
63 return r
64}
65
66func AirdropClaimed(id string, index int) bool {
67 if index < 0 {
68 panic("launchpad/sales: invalid airdrop index")
69 }
70 return airdropClaims.Has(indexKey(id, index))
71}
72
73// ClaimAirdrop remains available while the airdrop lane is paused. Anyone can
74// submit a proof; the destination is always the leaf's beneficiary. A leaf
75// whose beneficiary is not written as the chain writes a caller's address can
76// never be claimed: the token ledger would credit a balance no caller can
77// reach.
78func ClaimAirdrop(cur realm, id string, index int, beneficiary address, amount int64, proof string) {
79 if !cur.IsCurrent() {
80 panic("launchpad/sales: stale realm context")
81 }
82 currency.AssertNoCoins(0, cur)
83 if index < 0 || amount <= 0 || len(proof) > maxAirdropProofBytes ||
84 !meta.ValidAddress(beneficiary) ||
85 beneficiary == chain.PackageAddress(salesPath) {
86 panic("launchpad/sales: invalid airdrop claim")
87 }
88 r := GetAirdrop(id)
89 key := indexKey(id, index)
90 if airdropClaims.Has(key) {
91 panic("launchpad/sales: airdrop already claimed")
92 }
93 h, _ := merkle.ParseHash(r.Root)
94 p, err := merkle.ParseProof(proof)
95 if err != nil {
96 panic("launchpad/sales: invalid airdrop proof")
97 }
98 if proof != "" {
99 parts := strings.Split(proof, ",")
100 for i, sibling := range p {
101 if merkle.FormatHash(sibling) != parts[i] {
102 panic("launchpad/sales: invalid airdrop proof")
103 }
104 }
105 }
106 leaf := merkle.Leaf([]byte("launchpad-airdrop-v1|" + id + "|" +
107 strconv.Itoa(index) + "|" + beneficiary.String() + "|" +
108 strconv.FormatInt(amount, 10)))
109 if !merkle.Verify(h, leaf, p) || sm.Add(r.Claimed, amount) > r.Total {
110 panic("launchpad/sales: airdrop proof or allocation denied")
111 }
112 r.Claimed = sm.Add(r.Claimed, amount)
113 airdropClaims.Set(key, true)
114 airdrops.Set(id, r)
115 setTokenOwed(id, sm.Sub(tokenOwed(id), amount))
116 tokens.Transfer(cross(cur), id, beneficiary, amount)
117 if tokens.BalanceOf(id, chain.PackageAddress(salesPath)) < tokenOwed(id) {
118 panic("launchpad/sales: token custody shortfall")
119 }
120 chain.Emit("LaunchpadAirdropClaimed", "id", id,
121 "index", strconv.Itoa(index), "beneficiary", beneficiary.String(),
122 "amount", strconv.FormatInt(amount, 10))
123}