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

merkledrop.gno

5.12 Kb · 164 lines
  1// Package merkledrop is an idiomatic gno.land port of the classic Solidity
  2// MerkleDistributor airdrop (Uniswap's contract, OpenZeppelin's MerkleProof).
  3//
  4// A fixed merkleRoot commits to a set of (address, amount) allocations. A
  5// recipient proves membership by supplying the sibling hashes on the path from
  6// their leaf up to the root. Each address may claim exactly once.
  7//
  8// Leaf and node hashing use crypto/sha256 (available in the gno stdlib), the
  9// same primitive Solidity distributors use via keccak — here sha256 keeps the
 10// off-chain tree builder trivially reproducible. Internal nodes hash the two
 11// children in sorted order (the OpenZeppelin "commutative" scheme), so proofs
 12// carry no left/right flags.
 13//
 14//	leaf   = sha256(addr.String() + "|" + amount)
 15//	node   = sha256(min(a,b) || max(a,b))
 16//
 17// Balances are pure accounting (uint64); no real coin moves — there is no
 18// msg.value on gno, so this models the allocation ledger only.
 19package merkledrop
 20
 21import (
 22	"bytes"
 23	"crypto/sha256"
 24	"encoding/hex"
 25	"strconv"
 26	"strings"
 27
 28	"chain"
 29	"chain/runtime/unsafe"
 30
 31	"gno.land/p/nt/avl/v0"
 32)
 33
 34// merkleRoot commits to the airdrop allocation set. Generated off-chain with
 35// the exact scheme above over the four example recipients in README.md.
 36const merkleRoot = "0d00b73577019dc92924f0ae001d3e1839d51fb358adda2ad1510c39eb82b13f"
 37
 38// claimed maps claimer address string -> claimed amount (uint64).
 39var claimed avl.Tree
 40
 41// totalClaimed is the running sum of all claimed allocations.
 42var totalClaimed uint64
 43
 44// leaf computes the tree leaf for an (address, amount) allocation.
 45func leaf(claimer address, amount uint64) []byte {
 46	data := claimer.String() + "|" + strconv.FormatUint(amount, 10)
 47	h := sha256.Sum256([]byte(data))
 48	return h[:]
 49}
 50
 51// hashPair hashes two nodes in sorted order (commutative), so the proof does
 52// not need to encode child positions.
 53func hashPair(a, b []byte) []byte {
 54	var d []byte
 55	if bytes.Compare(a, b) <= 0 {
 56		d = append(append(d, a...), b...)
 57	} else {
 58		d = append(append(d, b...), a...)
 59	}
 60	h := sha256.Sum256(d)
 61	return h[:]
 62}
 63
 64// parseProof splits a comma-separated list of hex hashes into raw 32-byte
 65// nodes, panicking on any malformed element.
 66func parseProof(proof string) [][]byte {
 67	proof = strings.TrimSpace(proof)
 68	if proof == "" {
 69		return nil
 70	}
 71	parts := strings.Split(proof, ",")
 72	out := make([][]byte, 0, len(parts))
 73	for _, p := range parts {
 74		p = strings.TrimSpace(p)
 75		if p == "" {
 76			continue
 77		}
 78		raw, err := hex.DecodeString(p)
 79		if err != nil {
 80			panic("merkledrop: bad proof hash: " + p)
 81		}
 82		if len(raw) != sha256.Size {
 83			panic("merkledrop: proof hash must be 32 bytes: " + p)
 84		}
 85		out = append(out, raw)
 86	}
 87	return out
 88}
 89
 90// computeRoot walks the proof from the given leaf up to a candidate root and
 91// returns its hex encoding.
 92func computeRoot(leafHash []byte, proof [][]byte) string {
 93	node := leafHash
 94	for _, sib := range proof {
 95		node = hashPair(node, sib)
 96	}
 97	return hex.EncodeToString(node)
 98}
 99
100// Verify reports whether (claimer, amount, proof) resolves to merkleRoot. Pure
101// and read-only — safe to call from tests and Render.
102func Verify(claimer address, amount uint64, proof string) bool {
103	got := computeRoot(leaf(claimer, amount), parseProof(proof))
104	return got == merkleRoot
105}
106
107// AmountClaimed returns how much the address has already claimed (0 if none).
108func AmountClaimed(claimer address) uint64 {
109	if !claimed.Has(claimer.String()) {
110		return 0
111	}
112	return claimed.Get(claimer.String()).(uint64)
113}
114
115// HasClaimed reports whether the address already claimed.
116func HasClaimed(claimer address) bool {
117	return claimed.Has(claimer.String())
118}
119
120// Claim proves the caller is entitled to `amount` via `proof` (comma-separated
121// hex sibling hashes) and marks the allocation claimed. Panics on a bad proof
122// or a double claim. Mirrors MerkleDistributor.claim (msg.sender is the caller).
123func Claim(cur realm, amount uint64, proof string) {
124	caller := unsafe.PreviousRealm().Address()
125	if HasClaimed(caller) {
126		panic("merkledrop: already claimed")
127	}
128	if !Verify(caller, amount, proof) {
129		panic("merkledrop: invalid proof")
130	}
131	claimed.Set(caller.String(), amount)
132	totalClaimed += amount
133
134	chain.Emit("Claimed",
135		"account", caller.String(),
136		"amount", strconv.FormatUint(amount, 10),
137	)
138}
139
140// Render shows the committed root, aggregate stats, and the list of claimers.
141func Render(path string) string {
142	var b strings.Builder
143	b.WriteString("# MerkleDrop\n\n")
144	b.WriteString("A fixed Merkle root gates a one-per-address airdrop. Prove your allocation with `Claim(amount, proof)`.\n\n")
145
146	b.WriteString("## Root\n\n")
147	b.WriteString("`" + merkleRoot + "`\n\n")
148
149	b.WriteString("## Stats\n\n")
150	b.WriteString("- Distinct claimers: " + strconv.Itoa(claimed.Size()) + "\n")
151	b.WriteString("- Total claimed: " + strconv.FormatUint(totalClaimed, 10) + "\n\n")
152
153	b.WriteString("## Claimers\n\n")
154	if claimed.Size() == 0 {
155		b.WriteString("_No claims yet._\n")
156		return b.String()
157	}
158	b.WriteString("| Address | Amount |\n|---|---|\n")
159	claimed.Iterate("", "", func(key string, value interface{}) bool {
160		b.WriteString("| " + key + " | " + strconv.FormatUint(value.(uint64), 10) + " |\n")
161		return false
162	})
163	return b.String()
164}