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

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}