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.
| Crate | Role |
|---|---|
merkle-distributor | The 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-vault | Purpose-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-vault | Admin-controlled BTV escrow that match-funds Identity-tier streamer-token pool seeding. Per-streamer allowlist with per-launch caps. |
merkle-tree | Off-chain SHA256 builder (forked from Jito / Jupiter). Reads (claimant, amount) or (claimant, metadata_uri) rows; produces the root + per-leaf proofs the programs verify. |
cli | Operator CLI: build merkle tree from CSV / JSON; split across multiple trees above the per-tree cap. |
api | axum HTTP server that loads tree files on startup and serves merkle proofs to the frontend at GET /user/:pubkey. |
verify | Jito-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):
| Program | ID |
|---|---|
merkle-distributor | 4ffj6hEnx6cqp4ToMALExqk6QwPNSbZyr8ro9yW1Qvok |
streamer-vault | 2qsNccNeHgqY9fsLobmpwKJXmZZKqemNdUsQ12w3ydTr |
launchpad-vault | DpAbzBsenJqFaa6Bk3cZjCdapWriEPgYek17UefkwPEb |
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
| Instruction | Caller | Purpose |
|---|---|---|
init_distributor | streamer / operator | Create distributor PDA + token vault, commit root, set enable slot + clawback timestamp |
claim | viewer | Verify proof, flip bitmap, transfer from vault via transfer_checked |
clawback | permissionless after clawback_start_ts | Sweep remaining vault balance to clawback_receiver |
set_admin / accept_admin | admin → pending admin | Two-step admin transfer |
set_clawback_receiver | admin | Change clawback destination (immutable treasury_authority validates) |
set_enable_slot | admin | Adjust claim activation slot (capped to 2-year future) |
close_distributor | admin | Reclaim rent after clawback grace window |
NFT path (Metaplex Core + Bubblegum)
| Instruction | Caller | Purpose |
|---|---|---|
init_nft_distributor | streamer / operator | Same as init_distributor for an NFT collection (kind ∈ {Core, Compressed}) |
claim_nft_core | viewer | Verify proof, flip bitmap, (Layer 2 in progress) CPI into mpl-core::CreateV2 to mint per-claim asset |
claim_nft_compressed | viewer | Same shape for Bubblegum compressed NFTs |
set_nft_enable_slot / close_nft_distributor | admin | NFT 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:
withdraw_for_distribution→ merkle-distributor vaultwithdraw_for_pool_seeding→ Meteora DLMMlb_pair.reserve_*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(discriminator0x04aa48013ab1962b) oradd_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-pathon 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
| Subsystem | State |
|---|---|
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-vaultdeclarator ID is unit-tested against all fourAnchor.tomlcluster 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 againstsha256("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
- On-chain program — merkle-distributor reference
- Streamer vault
- Launchpad vault
- Architecture — one-screen visual
- Security audits
- bot — produces the merkle trees the program verifies
- public-web — calls claim from the viewer's wallet
- admin-web — operator-side init / clawback workflows