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

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}