UtilitiesCryptography

Merkle Distributor

Source Code

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:

  1. Verification: Hashes the leaf's XDR encoding and validates that the leaf hash and proof correctly reconstruct the stored Merkle root
  2. Claiming: Marks the leaf's index as claimed to prevent duplicate claims and emits a SetClaimed event

They differ in how the tree is expected to be built:

  • verify_and_set_claimed uses Verifier::verify and expects a tree built with commutative hashing, i.e. sorted pairs.
  • verify_with_index_and_set_claimed uses Verifier::verify_with_index and expects a tree built without sorting, where the leaf's index determines its position. It also panics with the CryptoError codes of verify_with_index for 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 } (topic set_root): emitted by set_root.
  • SetClaimed { index: u32 } (topic set_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.