Package merkledrop is a Merkle-gated airdrop that actually moves GNOT.
It is the successor to r/moul/x/daily/merkledrop/v0, which was generated by the daily pipeline and is deployed on mainnet. v0 works, but it is a demonstration rather than a drop, and its proof scheme is safe by accident. Everything below is what changed and why.
1. The leaf scheme is domain separated
v0 hashed leaves bare and combined nodes commutatively, the OpenZeppelin scheme: leaf = sha256(addr|amount), node = sha256(min||max). Without a leaf/inner tag an inner-node hash is also a valid leaf hash, so anyone who can present a 64-byte leaf preimage can prove membership of a leaf that was never committed.
v0 is not exploitable, but only by arithmetic: an inner preimage is exactly 64 bytes, and a v0 leaf preimage is at most 40 (bech32 address) + 1 + 20 (uint64 has at most 20 digits) = 61 bytes. 61 < 64, so no collision is reachable. Change the leaf encoding, widen the amount, use a different address form, and the forgery goes live with no visible diff at the call site. That is not a property to rely on.
v1 uses gno.land/p/moul/x/merkle/v0, which is the Tendermint scheme: leaves are tagged 0x00 and inner nodes 0x01, so the two preimage spaces cannot overlap at any length.
2. Proofs are bound to a position
v0's proof was a bare sibling list of any length, folded until it ran out. v1's proof carries its index and the total leaf count, and the verifier rebuilds the tree shape from them: a proof cannot be replayed at another index, and one of the wrong length is rejected rather than folded. The sibling count is capped at merkle.MaxDepth, so an untrusted caller cannot choose the length of the loop.
3. The root is settable, and the drop can close
v0's root is a `const`, so the drop can never be re-rooted, extended or ended. v1's owner sets the root, the leaf count and an optional closing height, and can sweep the remainder once it closes.
4. It moves real coins
v0 keeps a uint64 ledger and moves nothing; its README says so, but it sits on mainnet reading like an airdrop. v1 sends ugnot from the realm's own address through the banker, and refuses a claim it cannot pay rather than marking it claimed.
Who the claimer is
The claimer is PreviousRealm().Address(), the immediate caller. Called directly by a user that is the user; called through another realm it is that REALM. This is not a hole, because the leaf binds the address and the proof must match the claimer, so an intermediary can only claim an allocation granted to the intermediary itself. It does mean a wrapper realm cannot claim on a user's behalf, which is deliberate.
Built on gno.land/p/moul/x/merkle/v0.