A modern, script-driven Solana airdrop template that distributes SOL to many recipients efficiently using a Merkle tree. Only the 32‑byte Merkle root is stored on-chain. The project uses Anchor for the on-chain program, Codama for a generated TypeScript client, and the modern Solana Kit ("Gill") for transactions. This README focuses on how the program works and how to use it through the provided scripts.
- Solana Merkle Airdrop Distributor (Gill + Codama + Anchor)
- Prerequisites
- Quick Installation (Recommended)
- Manual Installation
- Verify Installation
- Solana CLI Basics
- Quick Setup
- Architecture Overview
- Merkle Airdrop Model
- On-Chain Design
- Program Interactions
- Security and Safety
- Testing and Validation
- Version and Compatibility Notes
- Using the Scripts
- FAQ
- Glossary
- Gaps and Suggestions
- 🎓 Key Technologies
Before you can build and deploy Solana programs with this template, you need to install Rust, Solana CLI, and Anchor CLI on your system.
On Mac and Linux, run this single command to install all dependencies:
curl --proto '=https' --tlsv1.2 -sSfL https://solana-install.solana.workers.dev | bashWindows Users: You must first install WSL (Windows Subsystem for Linux). Then run the command above in the Ubuntu (Linux) terminal.
After installation, you should see output similar to:
Installed Versions:
Rust: rustc 1.85.0 (4d91de4e4 2025-02-17)
Solana CLI: solana-cli 2.1.15 (src:53545685; feat:3271415109, client:Agave)
Anchor CLI: anchor-cli 0.31.1
Node.js: v23.9.0
Yarn: 1.22.1
Installation complete. Please restart your terminal to apply all changes.
If the quick installation works, skip to Verify Installation. If it doesn't work, follow the manual installation steps below.
Solana programs are written in Rust. Install it using rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -yReload your PATH environment variable:
. "$HOME/.cargo/env"Install the Solana CLI tool suite:
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"Add Solana to your PATH (if prompted):
export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH"Install Anchor Version Manager (AVM) for managing Anchor versions:
cargo install --git https://github.com/coral-xyz/anchor avm --forceInstall the latest Anchor CLI:
avm install latest
avm use latestNode.js and Yarn are required for running TypeScript tests and the frontend:
Node.js:
- Visit nodejs.org and install the LTS version
- Or use a version manager like
nvm:nvm install --lts && nvm use --lts
Yarn:
npm install -g yarnCheck that all tools are installed correctly:
# Check Rust
rustc --version
# Expected: rustc 1.84.1+
# Check Solana CLI
solana --version
# Expected: solana-cli 2.0.26+
# Check Anchor CLI
anchor --version
# Expected: anchor-cli 0.31.1
# Check Node.js
node --version
# Expected: v22.0.0+
# Check Yarn
yarn --version
# Expected: 1.22.1+Set your cluster to devnet for development:
solana config set --url devnetCheck your current configuration:
solana config getGenerate a new keypair for development:
solana-keygen newGet your wallet address:
solana addressRequest devnet SOL for testing:
solana airdrop 2Check your balance:
solana balanceNote: The airdrop command is limited to 5 SOL per request and may have rate limits. Alternatively, use the Solana Web Faucet.
pnpm create solana-dapp@latest -t gh:solana-foundation/templates/community/gill-jito-airdropcd <your-project>
pnpm installGenerates the necessary TypeScript types and client code from the Solana program:
pnpm codama:generateThen setup the program:
pnpm airdrop:setupThis single command will:
- ✅ Create deployment wallet and fund it with SOL
- ✅ Generate test wallets for airdrop recipients
- ✅ Build and deploy the Solana program
- ✅ Update all configuration files
- ✅ Generate Merkle tree for airdrop distribution
pnpm airdrop:initpnpm devAirdrop distribution is reduced to a single on-chain commitment (the Merkle root), with off-chain generated proofs enabling recipients to claim their exact allocation. The template provides scripts to deploy, initialize, and claim using a type-safe, generated client.
┌──────────────────────────────────────────────────────────┐
│ Off-chain Preparation │
│ recipients.json → Merkle Tree → Root + Proofs │
└───────────────┬───────────────────────┬──────────────────┘
│ │
initialize(root, totalAmount) claim(proof, amount, index)
│ │
┌───────▼─────────┐ ┌─────▼────────────────┐
│ Airdrop State │ │ Claim Status (PDA) │
│ (root stored) │ │ per recipient │
└───────┬─────────┘ └──────────┬───────────┘
│ │
│ funds │ prevent double-claim
│ │
┌────▼──────────┐ ┌────▼──────────┐
│ Vault (PDA) │ ───SOL──▶ │ Recipient │
└───────────────┘ └───────────────┘
- Storing each recipient on-chain is expensive. A Merkle tree commits to the entire set with a single 32‑byte root.
- Each recipient proves inclusion with a logarithmic-size proof. On-chain verifies the proof against the stored root.
- Benefits: smaller state, predictable compute cost, scalable to large recipient sets.
- Leaf: hash of recipient data, typically
H(pubkey || amount). The exact encoding must match what the generator uses. - Proof: ordered array of sibling hashes along the path to the root.
- Verification (simplified):
- Compute
leaf = H(pubkey || amount)matching the off-chain generator’s format. - Fold siblings: for each proof node,
hash = H(order(left, right))using the known left/right order (often guided byleafIndex). - Compare computed hash to the stored Merkle root; if equal, the claim is valid.
- Compute
Gotcha: Proof order and the leaf encoding must match exactly. Any mismatch yields “invalid proof.”
-
Airdrop State PDA
- Purpose: Stores immutable Merkle root and global airdrop parameters.
- Example seeds: ["airdrop", authority, airdropId]
- Data (typical):
authority: Pubkey— entity initializing the airdropmerkleRoot: [u8; 32]totalAmount: u64— total lamports allocatedclaimedAmount: u64— cumulative claimed lamportsbump: u8
- Space:
8 (discriminator) + 32 + 32 + 8 + 8 + 1≈ 89 bytes; allocate with headroom (e.g., 128 bytes)
-
Vault PDA (System Account owned by program)
- Purpose: Holds SOL to be distributed.
- Example seeds: ["vault", airdropState]
- Data: lamports only; no data account needed if purely a System Account
- Property: Only the program can move lamports from this PDA.
-
Claim Status PDA (per recipient)
- Purpose: Prevents double-claim.
- Example seeds: ["claim", airdropState, recipientPubkey]
- Data (typical):
claimed: boolamount: u64(optional bookkeeping of claimed amount)bump: u8
- Space:
8 + 1 + 8 + 1≈ 18 bytes; allocate with headroom (e.g., 64 bytes)
Gotcha: Seeds shown are representative. Use the seeds compiled into your program and generated client.
-
initializeAirdrop
- Inputs:
merkleRoot: [u8; 32],totalAmount: u64 - Accounts:
authority (signer),airdropState (PDA),vault (PDA),systemProgram - Effects:
- Creates/initializes
airdropState - Optionally creates
vault - Records the Merkle root and total allocation
- May assert that sufficient SOL is present or transferred to
vault
- Creates/initializes
- Inputs:
-
claimAirdrop
- Inputs:
amount: u64,leafIndex: u32|u64,proof: [[u8; 32]] - Accounts:
signer (recipient),airdropState,claimStatus (PDA),vault (PDA),systemProgram - Effects:
- Verifies the Merkle proof matches
(recipient, amount, index) - Ensures
claimStatusindicates not yet claimed - Marks as claimed and transfers
amountlamports fromvaulttosigner - Updates
claimedAmount
- Verifies the Merkle proof matches
- Inputs:
Safeguards:
- Double-claim protection via
Claim StatusPDA. - Root immutability: once set, the airdrop membership is fixed.
- Program-owned vault: only program logic moves funds.
- Initialize:
airdropStateis created with root and totals;vaultis established and funded for the airdrop. - Claim: Upon proof verification, lamports flow from
vaultto the claimant;claimStatusis created and marked to prevent reuse. - Completion: When
claimedAmount == totalAmount, distribution is complete. Any remainder handling (e.g., sweep/close) depends on program design.
Below are concise TypeScript examples using the generated Codama client. These snippets assume the scripts have already generated and wired the client paths. Use Solana Kit ("Gill") to create and send transactions.
Initialize airdrop:
import { getInitializeAirdropInstruction } from './anchor/generated/clients/ts/instructions/initializeAirdrop';
import { address } from 'gill'; // Gill/Solana Kit address helpers
// import your client, RPC, and wallet abstractions from your app’s runtime
const initIx = getInitializeAirdropInstruction({
airdropState: airdropStatePda, // PDA derived by client/helpers
authority: authorityPubkey, // wallet pubkey
merkleRoot: new Uint8Array(root32), // 32-byte root
amount: BigInt(totalLamports), // u64
});
// Use your Solana Kit transaction helpers to send:
// await sendInstructions([initIx], { payer: authority, rpc });Claim airdrop:
import { getClaimAirdropInstruction } from './anchor/generated/clients/ts/instructions/claimAirdrop';
import { address } from 'gill';
const proofBytes = proofHexArray.map(h => new Uint8Array(Buffer.from(h.slice(2), 'hex')));
const claimIx = getClaimAirdropInstruction({
airdropState: airdropStatePda,
userClaim: claimStatusPda, // PDA for this recipient
signer: recipientPubkey, // claimant wallet
proof: proofBytes, // [[u8; 32]]
amount: BigInt(recipientLamports),
leafIndex: recipientIndex,
});
// await sendInstructions([claimIx], { payer: recipient, rpc });Gotcha: Ensure the proof, amount, and index fed to the instruction are exactly those used by the Merkle generator that produced the root.
-
Common pitfalls
- Proof mismatch: Using a different hashing or encoding than the generator yields “invalid proof.”
- Program ID mismatch: If the deployed ID differs from what the client expects, instruction builders point to the wrong program.
- Replay/double-claim: Prevented by
Claim StatusPDA; if missing or mismatched seeds, a second claim may slip through in theory—stick to the generated client and canonical seeds.
-
Upgrade authority and immutability
- Keep the upgrade authority secure. Consider making the program immutable after thorough testing.
- If upgradable, document any migration strategy for vault and state.
-
Limits
- Compute budget: Proof depth increases compute cost (~O(log n)). Very deep trees need budget tuning.
- Account sizes: Reserve adequate space for PDAs (Anchor discriminator adds 8 bytes).
- Transaction size: Large proofs or multiple instructions may approach limits; use single-claim per transaction.
Gotcha: Root immutability means membership is fixed. Changing recipients requires a new root and a new airdrop state.
The test suite validates:
- Initialization creates PDAs and records the Merkle root
- Happy-path claim transfers lamports and marks the claim
- Double-claim is rejected via
Claim StatusPDA - Incorrect proof or wrong amount fails verification
- Aggregate
claimedAmountreflects actual transfers
See anchor/tests/solana-distributor-comprehensive.test.ts for end‑to‑end coverage using the generated client and Solana Kit helpers.
- Anchor CLI: 0.31.1
- Solana CLI: 2.2.20+ (2.2.x)
- Rust: 1.88.0+
- Node.js: 22+
The template and generated client target these versions for consistent behavior and type compatibility.
- Run the provided scripts in order to generate the client, deploy, and initialize the airdrop; then use the app or scripts to claim.
- Environment, program IDs, recipients, and Merkle artifacts are auto-managed by the scripts and committed to the expected paths.
-
Why does my claim say “Address not eligible for this airdrop”?
- The wallet is not in the recipients set used to produce the current Merkle root, or the proof/amount/index don’t match.
-
I see “Program ID mismatch.” What now?
- Ensure your generated client and scripts reference the deployed program ID. Re-run the script that fixes IDs and regenerates the client.
-
Claims fail with “invalid proof.”
- Ensure the generator and on-chain hashing agree on leaf encoding and sibling order. Regenerate proofs after any recipients change.
-
Can someone claim twice with the same wallet?
- No. The claim creates a
Claim StatusPDA keyed by(airdropState, recipient). Second attempts are rejected.
- No. The claim creates a
-
What if the vault runs out of SOL?
- Claims will fail. Replenishment behavior depends on your program’s design. This template expects sufficient initial funding during initialization.
-
Can I rotate the authority?
- Not by default. Authority primarily matters at initialization. Changing authorities typically requires explicit program support and migration.
- Merkle root: A 32‑byte commitment to a set. Verifies inclusion with minimal proofs.
- Merkle proof: A sequence of sibling hashes used to reconstruct the root from a leaf.
- PDA (Program Derived Address): Deterministic, program-owned address derived from seeds, not signable by a private key.
- Lamports: Smallest unit of SOL (1 SOL = 1,000,000,000 lamports).
- Discriminator: Anchor’s 8‑byte account type prefix stored in every account it manages.
- Authority: The signer that initializes the airdrop; typically controls setup, not claims.
- Vault: Program-owned System Account holding the lamports to distribute.
- Explicit seeds and data layouts: Document the exact PDA seeds and account layouts as compiled, including endianness and serialization formats, in
anchor/README.mdalongside the IDL. - Hashing details: Add a dedicated note specifying the leaf encoding and hash function, including byte order and any domain separators, to eliminate proof mismatches.
- Vault lifecycle: Clarify whether the vault can be topped up, swept, or closed, and under what authority or conditions.
- Compute guidance: Provide recommended compute budget and proof depth limits for large distributions, plus tips for splitting claims if needed.
- Error catalog: Include a short table mapping common on-chain error codes to actionable fixes to speed up debugging.
- Post-initialize governance: If upgrades remain enabled, document upgrade procedures and how they affect the client and deployed state; if not, state immutability explicitly.
- Gill: Modern Solana JavaScript SDK
- @solana/kit
- Codama: Automatic client generation
- Anchor Framework: Solana program development
- Vitest: Fast unit testing framework