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

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}