Skip to main content

distributor — Solana program family + tooling

The on-chain side of BitView. One Cargo workspace; three Anchor programs plus the off-chain merkle-tree builder, the operator CLI, and the axum proof API.

CrateRole
merkle-distributorThe claim program. Anchor 1.0.2 + solana-program 3.0. Verifies merkle proofs, flips bitmap, transfers SPL tokens or mints Metaplex NFTs from a single vault per distribution.
streamer-vaultPurpose-gated escrow for 85% of the streamer-token supply. Only two outflows: into a merkle-distributor vault or into a Meteora DLMM / DAMM v2 pool reserve.
launchpad-vaultAdmin-controlled BTV escrow that match-funds Identity-tier streamer-token pool seeding. Per-streamer allowlist with per-launch caps.
merkle-treeOff-chain SHA256 builder (forked from Jito / Jupiter). Reads (claimant, amount) or (claimant, metadata_uri) rows; produces the root + per-leaf proofs the programs verify.
cliOperator CLI: build merkle tree from CSV / JSON; split across multiple trees above the per-tree cap.
apiaxum HTTP server that loads tree files on startup and serves merkle proofs to the frontend at GET /user/:pubkey.
verifyJito-derived proof verification library; shared between the program and the off-chain tooling.

Anchor 1.0.2. Solana 3.0. Workspace release profile: fat LTO, overflow checks on. The 2024-vintage audit of the Jito v0.30 codebase (Neodyme + OtterSec) applies to the original code; re-audit is required after the bitmap modifications and the Anchor 1.0 / Solana 3.0 bump before the next mainnet redeploy.

Program IDs

Declared in Anchor.toml and pinned at compile time. Identical across clusters (mainnet / devnet / testnet / localnet):

ProgramID
merkle-distributor4ffj6hEnx6cqp4ToMALExqk6QwPNSbZyr8ro9yW1Qvok
streamer-vault2qsNccNeHgqY9fsLobmpwKJXmZZKqemNdUsQ12w3ydTr
launchpad-vaultDpAbzBsenJqFaa6Bk3cZjCdapWriEPgYek17UefkwPEb

For per-program instructions, PDA derivations, and account layouts see On-chain program — merkle-distributor, Streamer vault, and Launchpad vault.

merkle-distributor

Why a bitmap instead of per-claim PDAs

The v2 program packs claim status into a single bitmap on the distributor account itself. At 100K claimants this is roughly 1,800× cheaper in rent than the per-claim ClaimStatus PDA pattern the original Jito/Jupiter design uses (~$16K vs ~$9 in rent at typical SOL prices).

Layout:

[ 0 .. 8] Anchor discriminator
[ 8 .. 264] Distributor header (zero-copy, repr(C))
[264 .. N] Bitmap, ceil(max_num_nodes / 8) bytes, bit i = "leaf i claimed"

At 100K nodes the bitmap is 12.5 KB and the whole account fits well under the 10 MiB Solana account size limit.

Instruction surface (14 instructions)

Token path

InstructionCallerPurpose
init_distributorstreamer / operatorCreate distributor PDA + token vault, commit root, set enable slot + clawback timestamp
claimviewerVerify proof, flip bitmap, transfer from vault via transfer_checked
clawbackpermissionless after clawback_start_tsSweep remaining vault balance to clawback_receiver
set_admin / accept_adminadmin → pending adminTwo-step admin transfer
set_clawback_receiveradminChange clawback destination (immutable treasury_authority validates)
set_enable_slotadminAdjust claim activation slot (capped to 2-year future)
close_distributoradminReclaim rent after clawback grace window

NFT path (Metaplex Core + Bubblegum)

InstructionCallerPurpose
init_nft_distributorstreamer / operatorSame as init_distributor for an NFT collection (kind ∈ {Core, Compressed})
claim_nft_coreviewerVerify proof, flip bitmap, (Layer 2 in progress) CPI into mpl-core::CreateV2 to mint per-claim asset
claim_nft_compressedviewerSame shape for Bubblegum compressed NFTs
set_nft_enable_slot / close_nft_distributoradminNFT lifecycle counterparts

Merkle scheme

SHA256, indexed leaves, sorted siblings (Jito/Jupiter-compatible):

leaf = sha256( 0x00 || sha256(index_le_u32 || claimant_32 || amount_le_u64) )
node_pair = sha256( 0x01 || min(L, R) || max(L, R) )

NFT leaves hash a metadata URI instead of an amount:

nft_leaf = sha256( 0x00 || sha256(index_le_u32 || claimant_32 || sha256(metadata_uri_string)) )

Max proof depth is 24 (covers 2²⁴ ≈ 16.7M leaves with safety margin for unbalanced trees). Larger distributions split across multiple trees via the version seed.

PDAs

distributor_pda = pda([b"Distributor", base, mint, version_le_bytes], program_id)

base is a per-tree pubkey, mint is the SPL mint, version is a u32 so one mint can host many trees if needed.

streamer-vault

Purpose: lock 85% of streamer-token supply (Token-2022) so a compromised streamer wallet cannot drain tokens earmarked for future distributions or pool seeding. Only two outflows are allowed:

  1. withdraw_for_distribution → merkle-distributor vault
  2. withdraw_for_pool_seeding → Meteora DLMM lb_pair.reserve_*
  3. withdraw_for_damm_v2_launch → Meteora DAMM v2 pool

Every withdraw is gated by two independent checks:

  • Account-owner inspection — destination ATA's owner field must equal the canonical PDA of the target program (re-derived on-chain).
  • Sibling-instruction introspection — the next instruction in the transaction must be init_distributor (discriminator 0x04aa48013ab1962b) or add_liquidity_by_strategy2 (0x03dd95de6f8d76d5) or the DAMM v2 add-liquidity (0x14a1f118bddb4002) on the pinned program ID.

Discriminator constants are unit-tested at build time against sha256("global:<name>")[..8] so a Meteora or merkle-distributor instruction rename breaks the build before it could silently reject every withdraw.

Full reference: Streamer vault.

launchpad-vault

Purpose: hold a bootstrap BTV supply (genesis-seeded with 150M into the Onboarding bucket) and atomically match-fund Identity-tier streamer-token pool seeding. Admin pre-approves each streamer for a specific lb_pair with a per-launch BTV cap (AllowlistEntry PDA).

Six instructions: initialize_vault, deposit, add_allowlist, disable_allowlist, close_allowlist, withdraw_for_launch_seeding, withdraw_for_damm_v2_launch.

Same dual purpose-gating shape as streamer-vault: account-owner inspection + sibling-ix discriminator pinning.

Full reference: Launchpad vault.

How a distribution flows on-chain

streamer wallet merkle-distributor viewer wallet
│ │ │
│── init_distributor ───────▶│ │
│ (root, max_total_claim, │ │
│ max_num_nodes, enable │ │
│ slot, clawback_start_ts) │ │
│ │ │
│── fund vault ─────────────▶│ (atomic with init via │
│ (SPL transfer) │ streamer-vault sibling- │
│ │ ix purpose check) │
│ │ │
│ (active) │
│ │ │
│ │◀──── claim(idx, amt, ─────── │
│ │ proof) │
│ │── verify, set bit, │
│ │ transfer_checked ─────────▶│
│ │ │
│ │ ... viewers claim ... │
│ │ │
│ (window passes) │
│ │ │
│── clawback ───────────────▶│ │
│ (sweeps remaining to │ │
│ clawback_receiver) │ │

Off-chain tooling

cli

Operator workflows — local end-to-end smoke tests, manual finalize, one-off clawback runs. Not on the critical path for normal product flow (the bot + frontend handle that). Two subcommands:

  • create-merkle-tree — CSV / JSON (pubkey, amount)tree_<version>.json. Splits across multiple trees if > max_nodes_per_tree (default 100K).
  • create-nft-merkle-tree — CSV / JSON (pubkey, metadata_uri)nft_tree_<version>.json.

api

Stateless axum REST server on :7001:

  • Loads merkle-tree files from --merkle-tree-path on startup.
  • /distributors — list loaded trees.
  • /user/:pubkey — proof lookup.
  • Middleware: rate-limit (10K req/s), 20s timeout, load-shedding, tracing.

The bot proxies to this API at /claims-api/proof/* so the frontend never hits it directly; the proof API is read-only and hot-reloads when new tree files appear.

Status

SubsystemState
merkle-distributor — core token instructions (init, claim, clawback)✅ Built, audited at v0.30
merkle-distributor — 2-step admin transfer, enable-slot scheduling✅ Built
merkle-distributor — bitmap claim status✅ Built
merkle-distributor — NFT path Layer 1 (proof + bitmap + counter)✅ Built
merkle-distributor — NFT Layer 2 (mpl-core::CreateV2 CPI)🟡 In progress
merkle-distributor — NFT Layer 2 (mpl-bubblegum::mint_to_collection_v1 CPI)🟡 In progress
streamer-vault — full surface (init + 3 withdraw paths + close) with dual purpose-gating✅ Built (devnet)
launchpad-vault — full surface (init + deposit + allowlist + 2 withdraw paths)✅ Built (devnet)
api axum proof server✅ Built
cli tree builder✅ Built
verify library (program + off-chain shared)✅ Built
Re-audit after Anchor 1.0 / Solana 3.0 bump🔴 Required before next mainnet deploy
Creator-royalty enforcement on STREAM/BTV pool🔴 Phase 2 (pool-side, not program-side)

Security posture

Audits at v0.30 (Anchor 0.30 + solana-program 1.18):

  • Neodyme — separate report on the v2 bitmap modifications.
  • OtterSec — original Jito merkle distributor scope.

Reports are in distributor/audit/ and published on Security audits. The current source has since been bumped to Anchor 1.0.2 + solana-program 3.0 and added the two vault programs — re-audit is required before the next mainnet redeploy.

Build-time pinning (defense in depth):

  • streamer-vault declarator ID is unit-tested against all four Anchor.toml cluster entries on every build.
  • Sibling-instruction discriminators (init_distributor, add_liquidity_by_strategy2, withdraw_for_launch_seeding, Meteora DAMM v2 add-liquidity) are unit-tested against sha256("global:<name>")[..8] so a downstream rename fails the build before deploy.

Bug bounty: see Bug bounty.

Reporting: see Disclosure policy.

Local development

cd distributor

# Build everything
cargo build --release

# Build the programs (Anchor)
anchor build

# Build a tree from a CSV
cargo run --release -p cli -- create-merkle-tree \
--csv-path airdrop.csv \
--merkle-tree-path tree.json

# Run the proof API locally
cargo run --release -p api -- \
--bind-addr 0.0.0.0:7001 \
--merkle-tree-path ./trees/ \
--base $BASE_PUBKEY \
--mint $MINT_PUBKEY \
--program-id 4ffj6hEnx6cqp4ToMALExqk6QwPNSbZyr8ro9yW1Qvok

See Operations for the deployable shape.

Going deeper

For the internal engineering view — workspace-level Cargo design, per-instruction account-layout deep dive, NFT Layer 2 implementation plan, audit-finding remediation log, performance budgets at 100K and 1M claimants, and the rent-cost math vs the per-claim PDA pattern — see the internal _internal/technical/distributor-detail.md doc.

Cross-references