Merkle Distributor
The Merkle Distributor package builds on the crypto package to provide a system for distributing tokens or other assets using Merkle proofs for verification.
Key Components
IndexableLeaf
The IndexableLeaf trait defines the interface that leaf nodes must implement to be used with the Merkle Distributor:
pub trait IndexableLeaf {
fn index(&self) -> u32;
}Each leaf in the merkle tree requires a unique index to track whether it has been claimed to prevent double-claiming. The custom leaf struct should implement this trait by returning a unique u32 value for each recipient. This index serves as the key for storing claim status on-chain.
The leaf must also implement ToXdr, which every #[contracttype] does. The distributor hashes the leaf's XDR
encoding with its hasher to obtain the leaf hash, so the off-chain tree must be built from the same encoding.
MerkleDistributor
The MerkleDistributor struct is a generic component that manages the verification and claiming process. It is parameterized by a hash function (e.g. Keccak256) and provides the following core functionality:
Setting the Merkle Root
set_root(e: &Env, root: BytesN<32>) stores the Merkle tree root on-chain and emits a SetRoot event. This is typically called once during contract construction or when updating the distribution list.
set_root performs no authorization checks. Call it only from the constructor or from an admin function that
enforces its own authorization.
The root can be updated after the initial setup, and previously claimed indices remain claimed. This supports "append-only" distributions where an admin periodically expands the set of eligible claimants. The new tree must preserve the same index-to-leaf mapping for existing entries; otherwise, new claimants assigned to already-claimed indices will be unable to claim.
get_root(e: &Env) -> BytesN<32> returns the stored root and panics with RootNotSet if none has been set.
Checking Claim Status
is_claimed(e: &Env, index: u32) -> bool queries whether a specific index has already been claimed. This allows you to check claim status before attempting verification. Claim flags are kept in persistent storage, and checking a claimed index extends its TTL.
Verifying and Claiming
pub fn verify_and_set_claimed<N: ToXdr + IndexableLeaf>(e: &Env, leaf: N, proof: Vec<BytesN<32>>);
pub fn verify_with_index_and_set_claimed<N: ToXdr + IndexableLeaf>(e: &Env, leaf: N, proof: Vec<BytesN<32>>);Both functions perform two operations atomically:
- Verification: Hashes the leaf's XDR encoding and validates that the leaf hash and proof correctly reconstruct the stored Merkle root
- Claiming: Marks the leaf's index as claimed to prevent duplicate claims and emits a
SetClaimedevent
They differ in how the tree is expected to be built:
verify_and_set_claimedusesVerifier::verifyand expects a tree built with commutative hashing, i.e. sorted pairs.verify_with_index_and_set_claimedusesVerifier::verify_with_indexand expects a tree built without sorting, where the leaf's index determines its position. It also panics with theCryptoErrorcodes ofverify_with_indexfor oversized proofs or out-of-range indices.
Both panic if the root is not set, if the index has already been claimed, or if the proof is invalid, ensuring the integrity of the distribution process.
Errors
pub enum MerkleDistributorError {
/// The merkle root is not set.
RootNotSet = 1300,
/// The provided index was already claimed.
IndexAlreadyClaimed = 1301,
/// The proof is invalid.
InvalidProof = 1302,
}Events
SetRoot { root: Bytes }(topicset_root): emitted byset_root.SetClaimed { index: u32 }(topicset_claimed): emitted when an index is claimed.
Usage Example
use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env, Vec};
use stellar_contract_utils::crypto::keccak::Keccak256;
use stellar_contract_utils::merkle_distributor::{IndexableLeaf, MerkleDistributor};
// Define a leaf node structure
#[contracttype]
#[derive(Clone)]
pub struct LeafData {
pub index: u32,
pub address: Address,
pub amount: i128,
}
// Implement IndexableLeaf for the leaf structure
impl IndexableLeaf for LeafData {
fn index(&self) -> u32 {
self.index
}
}
#[contract]
pub struct TokenDistributor;
#[contractimpl]
impl TokenDistributor {
// Set the Merkle root at deployment, since `set_root` performs no authorization checks
pub fn __constructor(e: Env, root: BytesN<32>) {
MerkleDistributor::<Keccak256>::set_root(&e, root);
}
// Claim tokens by providing a proof
pub fn claim(e: &Env, leaf: LeafData, proof: Vec<BytesN<32>>) {
// Verify the proof and mark as claimed
MerkleDistributor::<Keccak256>::verify_and_set_claimed(e, leaf.clone(), proof);
// Transfer tokens or perform other actions based on leaf data
// ...
}
// Check if an index has been claimed
pub fn is_claimed(e: &Env, index: u32) -> bool {
MerkleDistributor::<Keccak256>::is_claimed(e, index)
}
}Use Cases
Token Airdrops
Efficiently distribute tokens to a large number of recipients without requiring individual transactions for each recipient.
NFT Distributions
Distribute NFTs to a whitelist of addresses, with each address potentially receiving different NFTs.
Off-chain Allowlists
Maintain a list of eligible addresses off-chain and allow them to claim tokens or other assets on-chain.
Snapshot-based Voting
Create a snapshot of token holders at a specific block and allow them to vote based on their holdings.