1// Package merkledrop is an idiomatic gno.land port of the classic Solidity2// MerkleDistributor airdrop (Uniswap's contract, OpenZeppelin's MerkleProof).3//4// A fixed merkleRoot commits to a set of (address, amount) allocations. A5// recipient proves membership by supplying the sibling hashes on the path from6// 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), the9// same primitive Solidity distributors use via keccak — here sha256 keeps the10// off-chain tree builder trivially reproducible. Internal nodes hash the two11// children in sorted order (the OpenZeppelin "commutative" scheme), so proofs12// 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 no18// msg.value on gno, so this models the allocation ledger only.19package merkledrop2021import (22 "bytes"23 "crypto/sha256"24 "encoding/hex"25 "strconv"26 "strings"2728 "chain"29 "chain/runtime/unsafe"3031 "gno.land/p/nt/avl/v0"32)3334// merkleRoot commits to the airdrop allocation set. Generated off-chain with35// the exact scheme above over the four example recipients in README.md.36const merkleRoot = "0d00b73577019dc92924f0ae001d3e1839d51fb358adda2ad1510c39eb82b13f"3738// claimed maps claimer address string -> claimed amount (uint64).39var claimed avl.Tree4041// totalClaimed is the running sum of all claimed allocations.42var totalClaimed uint644344// 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}5051// hashPair hashes two nodes in sorted order (commutative), so the proof does52// not need to encode child positions.53func hashPair(a, b []byte) []byte {54 var d []byte55 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}6364// parseProof splits a comma-separated list of hex hashes into raw 32-byte65// nodes, panicking on any malformed element.66func parseProof(proof string) [][]byte {67 proof = strings.TrimSpace(proof)68 if proof == "" {69 return nil70 }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 continue77 }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 out88}8990// computeRoot walks the proof from the given leaf up to a candidate root and91// returns its hex encoding.92func computeRoot(leafHash []byte, proof [][]byte) string {93 node := leafHash94 for _, sib := range proof {95 node = hashPair(node, sib)96 }97 return hex.EncodeToString(node)98}99100// Verify reports whether (claimer, amount, proof) resolves to merkleRoot. Pure101// 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 == merkleRoot105}106107// 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 0111 }112 return claimed.Get(claimer.String()).(uint64)113}114115// HasClaimed reports whether the address already claimed.116func HasClaimed(claimer address) bool {117 return claimed.Has(claimer.String())118}119120// Claim proves the caller is entitled to `amount` via `proof` (comma-separated121// hex sibling hashes) and marks the allocation claimed. Panics on a bad proof122// 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 += amount133134 chain.Emit("Claimed",135 "account", caller.String(),136 "amount", strconv.FormatUint(amount, 10),137 )138}139140// Render shows the committed root, aggregate stats, and the list of claimers.141func Render(path string) string {142 var b strings.Builder143 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")145146 b.WriteString("## Root\n\n")147 b.WriteString("`" + merkleRoot + "`\n\n")148149 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")152153 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 false162 })163 return b.String()164}165AmountClaimed(claimer string) uint64
Claim(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, amount uint64, proof string)
HasClaimed(claimer string) bool
Render(path string) string
Verify(claimer string, amount uint64, proof string) bool
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.