How to Distribute Tokens on Solana: A Developer’s Guide to Merkle Tree-Based Token Claims

·

Distributing tokens efficiently and securely is a common requirement for blockchain projects, especially those launching new SPL tokens on the Solana network. One of the most effective and gas-efficient methods is using a Merkle tree-based distribution system. This approach minimizes on-chain storage costs while ensuring only eligible users can claim their allocated tokens.

In this guide, we’ll walk through the core concepts, smart contract logic, and practical implementation steps for creating a token distributor on Solana using Merkle proofs—inspired by Uniswap’s model but adapted for Solana’s unique architecture.


Understanding the Merkle Tree Distribution Model

A Merkle tree (or hash tree) allows you to compress large datasets into a single root hash. In token distribution, this means you can pre-calculate a list of eligible wallets and their token amounts off-chain, generate a Merkle root, and store only that root on-chain.

Users who believe they’re eligible must provide a Merkle proof—a cryptographic path proving their inclusion in the tree—to claim their tokens. This shifts the computational burden from the project team to the claimer, significantly reducing distribution costs.

👉 Learn how to securely manage token distributions with advanced tools


Core Components of the Solana Token Distributor

The following sections break down the key parts of a typical Merkle-based token distributor implemented in Rust using the Anchor framework.

1. Initializing the Distributor

The new_distributor function sets up the on-chain account that will manage claims:

pub fn new_distributor(
    ctx: Context,
    _bump: u8,
    root: [u8; 32],
    max_total_claim: u64,
    max_num_nodes: u64,
) -> Result<()> {
    let distributor = &mut ctx.accounts.distributor;
    distributor.base = ctx.accounts.base.key();
    distributor.bump = unwrap_bump!(ctx, "distributor");
    distributor.root = root;
    distributor.mint = ctx.accounts.mint.key();
    distributor.max_total_claim = max_total_claim;
    distributor.max_num_nodes = max_num_nodes;
    distributor.total_amount_claimed = 0;
    distributor.num_nodes_claimed = 0;
    Ok(())
}

This function stores:

Only authorized signers (typically the project team) can initialize this account.


2. Claiming Tokens with Merkle Proof

Eligible users call the claim instruction with:

pub fn claim(
    ctx: Context,
    _bump: u8,
    index: u64,
    amount: u64,
    proof: Vec<[u8; 32]>,
) -> Result<()> {
    let node = anchor_lang::solana_program::keccak::hashv(&[
        &index.to_le_bytes(),
        &ctx.accounts.claimant.key().to_bytes(),
        &amount.to_le_bytes(),
    ]);

    require!(
        merkle_proof::verify(proof, ctx.accounts.distributor.root, node.0),
        ErrorCode::InvalidProof
    );

    // Transfer tokens via CPI to SPL Token program
    token::transfer(
        CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            token::Transfer {
                from: ctx.accounts.from.to_account_info(),
                to: ctx.accounts.to.to_account_info(),
                authority: ctx.accounts.distributor.to_account_info(),
            },
        )
        .with_signer(&[&seeds[..]]),
        amount,
    )?;

    // Update global and user-specific state
    let distributor = &mut ctx.accounts.distributor;
    distributor.total_amount_claimed += amount;
    distributor.num_nodes_claimed += 1;

    emit!(ClaimedEvent {
        index,
        claimant: ctx.accounts.claimant.key(),
        amount
    });

    Ok(())
}

The system verifies:

If all checks pass, tokens are transferred from the distributor’s associated token account (ATA) to the claimant’s wallet.


3. Off-Chain Merkle Proof Verification

The merkle_proof::verify function checks whether a given leaf (user data) exists under the root:

pub fn verify(proof: Vec<[u8; 32]>, root: [u8; 32], leaf: [u8; 32]) -> bool {
    let mut computed_hash = leaf;
    for proof_element in proof.into_iter() {
        computed_hash = if computed_hash <= proof_element {
            anchor_lang::solana_program::keccak::hashv(&[&computed_hash, &proof_element]).0
        } else {
            anchor_lang::solana_program::keccak::hashv(&[&proof_element, &computed_hash]).0
        };
    }
    computed_hash == root
}

This logic ensures deterministic verification, matching Ethereum-style Merkle proofs for cross-platform compatibility.


SPL Token vs SOL Transfers: Know the Difference

A common point of confusion is handling native SOL versus SPL tokens. They use different instructions:

✅ SPL Token Transfer (via CPI)

Used for custom tokens built on the SPL Token standard.

token::transfer(
    CpiContext::new(...),
    amount
)

Calls the SPL Token Program, requires:

✅ Native SOL Transfer

Used to send SOL directly.

invoke(
    &system_instruction::transfer(signer.key, recipient.key, lamports),
    &[signer.clone(), recipient.clone()]
);

Calls the System Program, simpler but only works with SOL.

🔍 Important: Never confuse these two. Use SPL transfers for token distributions, not native SOL instructions.

Why Use a Merkle Distributor?

  1. Cost Efficiency: Avoids paying rent for thousands of token accounts.
  2. Scalability: Supports tens of thousands of recipients without bloating the chain.
  3. Security: Cryptographic proofs prevent fake claims.
  4. Rent Recovery: Closed ATA accounts return lamports, minimizing net cost per claim (~0.000010 SOL).

👉 Explore secure blockchain development practices with trusted tools


Frequently Asked Questions (FAQ)

Q1: What is a Merkle root in token distribution?

A Merkle root is a single hash derived from all eligible user balances. It allows verification of individual claims without storing every address on-chain, reducing cost and complexity.

Q2: Can someone claim more than once?

No. The contract includes a ClaimStatus account per claim index. Once is_claimed = true, further attempts revert with DropAlreadyClaimed.

Q3: How do I generate Merkle proofs off-chain?

Use libraries like merkletreejs (Node.js) or Python’s py-merkle. Generate a tree from (index, address, amount) tuples and export each user’s proof path.

Q4: Who pays for the claim transaction?

The claimer pays gas fees and account creation costs. This makes large airdrops economically viable for developers.

Q5: Can I update the Merkle root after deployment?

No. The root is immutable once set. For updates, deploy a new distributor instance.

Q6: What happens if I exceed max_total_claim?

The transaction fails with ExceededMaxClaim. This cap ensures no more tokens are distributed than intended.


Final Thoughts

Building a token distribution system on Solana using Merkle trees offers a scalable, secure, and cost-effective solution for airdrops, incentives, or community rewards. By leveraging Anchor’s framework and SPL standards, developers can create robust contracts that protect both project teams and users.

Whether you're launching an NFT project, DeFi protocol, or DAO governance token, implementing a Merkle-based claim mechanism should be a core part of your distribution strategy.

👉 Start building secure and efficient dApps today