#solana #miraland #solana-token #hashspace

airdrop

Mint and Airdrop Framework on Solana for Sovereign Individuals

1 unstable release

0.1.0 Nov 6, 2025

#1 in #solana-token

Apache-2.0

130KB
2K SLoC

Mint-and-Airdrop Framework

A flexible, production-ready Solana program for creating SPL tokens and managing airdrop campaigns with multiple allocation modes and fee structures.

Overview

The Mint-and-Airdrop Framework provides a comprehensive solution for token creation and distribution on Solana. It supports multiple campaign management strategies, flexible fee structures, and various allocation methods (on-chain accounts, off-chain signatures, and merkle proofs).

Key Features

Flexible Token Minting

  • Create SPL tokens with Metaplex metadata support
  • Custom mint addresses via PDA derivation with noise
  • Automatic mint authority revocation for immutable supply caps
  • Mint-specific treasury for token storage

Multiple Allocation Modes

  • On-chain Allocation: Creates allocation accounts for each recipient (via Allocate instruction)
  • Off-chain Record: Claims against off-chain database records (via Claim Method 1, no Allocate needed)
  • Merkle Mode: Efficient merkle tree-based claims for large campaigns (via Claim Method 2, no Allocate needed)

Flexible Fee Structure

  • Configurable fees per operation type
  • Fair pricing based on actual costs
  • Transparent fee tracking and withdrawal

Multiple Distribution Methods

  • On-chain allocation claims
  • Off-chain signature-based claims
  • Merkle proof-based claims
  • Direct batch transfers (campaign owner)

Security & Best Practices

  • PDA-based treasury (no admin co-signing required)
  • Program-controlled fee account
  • Admin authority management
  • Comprehensive account validation

Architecture

Core Components

Program Config (Singleton PDA)
├── Program Admin Authority
├── Fee Configuration
│   ├── Mint Fee (lamports)
│   ├── Allocation Fee Per Recipient (lamports)
│   ├── Merkle Fee Per Recipient (lamports)
│   └── Direct Transfer Fee Per Recipient (lamports)
├── Fee Account (PDA)
└── Initialization Status

Campaign Account (Per Campaign PDA)
├── Campaign Owner
├── Mint Address
├── Treasury Token Account
├── Merkle Root (optional)
└── Campaign Status

Allocation Account (Mode 0 Only)
├── Recipient
├── Amount
└── Claim Status

Instructions

1. Initialize

Initialize the program with admin and fee configuration.

pub struct Initialize {
    pub admin: Pubkey,
    pub mint_fee_lamports: [u8; 8],
    pub allocation_fee_per_recipient_lamports: [u8; 8],
    pub merkle_fee_per_recipient_lamports: [u8; 8],
    pub direct_transfer_fee_per_recipient_lamports: [u8; 8],
}

Accounts:

  • Initializer (signer)
  • Config PDA
  • Fee Account PDA
  • System Program
  • Rent Sysvar

2. CreateMint

Create a new SPL token mint with metadata. All tokens up to max_supply are minted upfront to the mint treasury, and mint authority is revoked.

pub struct CreateMint {
    pub mint_id: [u8; 32],
    pub noise: [u8; 4],           // For address grinding
    pub mint: Pubkey,              // Optional: use provided or derive PDA
    pub name: [u8; 32],
    pub symbol: [u8; 10],
    pub uri: [u8; 128],            // Metaplex metadata URI
    pub decimals: u8,
    pub max_supply: [u8; 8],      // Must be > 0
}

Fee: mint_fee_lamports (recommended: 0.1 SOL)

3. CreateCampaign

Create a new campaign using an existing mint. Multiple campaigns can share the same mint.

pub struct CreateCampaign {
    pub campaign_id: [u8; 32],
    pub mint: Pubkey,
    pub merkle_root: [u8; 32],     // Zero = no merkle mode
    pub recipient_count: [u8; 8],  // Required if merkle_root != 0
    pub initial_supply: [u8; 8],   // Transfer from mint treasury
    pub max_supply: [u8; 8],
}

Fee: merkle_fee_per_recipient_lamports × recipient_count (if merkle_root set)

4. Allocate

Allocate tokens to recipients by creating on-chain allocation accounts.

Mode: On-chain only (creates Allocation accounts for each recipient)

  • Fee: allocation_fee_per_recipient_lamports × count

Note: Off-chain allocations are handled via Claim Method 1 (record mode) without a prior Allocate call. Merkle-based claims use Claim Method 2 (merkle mode) without Allocate.

pub struct Allocate {
    pub campaign_id: [u8; 32],
    pub count: u8,
    // Variable: Vec<(Pubkey recipient, u64 amount)>
}

5. Claim

Claim allocated tokens. Supports three methods:

  • Method 0 (allocation): Claim against on-chain Allocation account (requires prior Allocate call)
  • Method 1 (record): Claim against off-chain database record (with signature, no Allocate needed)
  • Method 2 (merkle): Claim against merkle proof (requires merkle root in campaign, no Allocate needed)
pub struct Claim {
    pub campaign_id: [u8; 32],
    pub method: u8,                // 0=allocation, 1=record, 2=merkle
    pub amount: [u8; 8],
    pub merkle_proof_length: u8,
    // Variable: merkle proof data or signature
}

Fee: None (recipient pays transaction fee)

6. DirectTransfer

Campaign owner transfers tokens directly to recipients in batches (no claim step required).

pub struct DirectTransfer {
    pub campaign_id: [u8; 32],
    pub count: u8,                 // 1-10 recipients per batch
    // Variable: Vec<(Pubkey recipient, u64 amount)>
}

Fee: direct_transfer_fee_per_recipient_lamports × count

Batch Size: Limited to 10 recipients per transaction

7. Replenish

Transfer additional tokens from mint treasury to campaign treasury.

pub struct Replenish {
    pub campaign_id: [u8; 32],
    pub amount: [u8; 8],
}

Fee: None

8. MintTokens

Mint additional tokens to campaign treasury (if mint authority not revoked).

pub struct MintTokens {
    pub campaign_id: [u8; 32],
    pub amount: [u8; 8],
}

Fee: None

9. UpdateConfig

Update fee rates and admin authority (admin-only).

pub struct UpdateConfig {
    pub new_admin: Pubkey,                      // Pubkey::default() = keep current
    pub mint_fee_lamports: [u8; 8],             // u64::MAX = keep current
    pub allocation_fee_per_recipient_lamports: [u8; 8],
    pub merkle_fee_per_recipient_lamports: [u8; 8],
    pub direct_transfer_fee_per_recipient_lamports: [u8; 8],
}

10. WithdrawFees

Withdraw accumulated fees to beneficiary (admin-only).

pub struct WithdrawFees {
    pub amount: [u8; 8],  // 0 = withdraw all
}

Fee Structure

Fee Type Amount Description
Mint Fee 0.1 SOL (100M lamports) Per mint creation
Allocation Fee 0.001 SOL (1M lamports) Per recipient (Mode 0 only)
Merkle Fee 0.0001 SOL (100K lamports) Per recipient (when merkle root set)
Direct Transfer Fee 0.0005 SOL (500K lamports) Per recipient (batch transfers)

Fee Collection Points

  • Mint Fee: Collected during CreateMint
  • Allocation Fee: Collected during Allocate (on-chain allocation accounts)
  • Merkle Fee: Collected during CreateCampaign (if merkle root set)
  • Direct Transfer Fee: Collected during DirectTransfer
  • Off-chain Record Claims: No fees (recipient pays transaction fee only)

Account Structure

Config Account

pub struct Config {
    pub admin: Pubkey,
    pub mint_fee_lamports: u64,
    pub allocation_fee_per_recipient_lamports: u64,
    pub merkle_fee_per_recipient_lamports: u64,
    pub direct_transfer_fee_per_recipient_lamports: u64,
    pub fee_account: Pubkey,
    pub total_fees_collected: u64,
    pub updated_at: i64,
}

Campaign Account

pub struct Campaign {
    pub owner: Pubkey,
    pub mint: Pubkey,
    pub treasury: Pubkey,           // Campaign treasury token account
    pub merkle_root: [u8; 32],      // Zero = no merkle mode
    pub status: u8,
    pub total_allocated: u64,
    pub total_claimed: u64,
    pub max_supply: u64,
    pub created_at: i64,
}

Allocation Account (Mode 0 Only)

pub struct Allocation {
    pub campaign: Pubkey,
    pub recipient: Pubkey,
    pub amount: u64,
    pub claimed: bool,
    pub allocated_at: i64,
    pub claimed_at: i64,
}

Usage Examples

Complete Campaign Lifecycle

use airdrop_api::sdk;

// 1. Initialize program (one-time setup)
let initialize_ix = sdk::initialize(
    &initializer,
    admin,
    100_000_000,      // mint_fee: 0.1 SOL
    1_000_000,        // allocation_fee: 0.001 SOL per recipient
    100_000,          // merkle_fee: 0.0001 SOL per recipient
    500_000,          // direct_transfer_fee: 0.0005 SOL per recipient
)?;

// 2. Create mint
let mint_id = [0u8; 32];
let noise = [0u8; 4];
let create_mint_ix = sdk::create_mint(
    &payer,
    mint_id,
    noise,
    Pubkey::default(), // Use PDA derivation
    b"My Token",
    b"MTK",
    b"https://example.com/metadata.json",
    9,
    1_000_000_000,     // max_supply
)?;

// 3. Create campaign (with merkle root)
let campaign_id = [1u8; 32];
let merkle_root = compute_merkle_root(&recipients);
let create_campaign_ix = sdk::create_campaign(
    &payer,
    &campaign_owner,
    campaign_id,
    mint_address,
    merkle_root,
    1000,              // recipient_count
    100_000_000,       // initial_supply
    1_000_000_000,     // max_supply
)?;

// 4. Allocate tokens (Mode 0 - on-chain)
let allocate_ix = sdk::allocate_onchain(
    &campaign_owner,
    campaign_id,
    vec![
        (recipient1, 1000),
        (recipient2, 2000),
        // ... more recipients
    ],
)?;

// 5. Claim tokens (Method 0 - on-chain)
let claim_ix = sdk::claim_onchain(
    &recipient,
    campaign_id,
)?;

// 6. Direct transfer (batch)
let direct_transfer_ix = sdk::direct_transfer(
    &campaign_owner,
    campaign_id,
    vec![
        (recipient1, 500),
        (recipient2, 500),
        // ... up to 10 recipients
    ],
)?;

Merkle Claim Example

// Generate merkle proof off-chain
let merkle_proof = generate_merkle_proof(&recipient, &amount, &merkle_tree)?;

// Claim using merkle proof
let claim_ix = sdk::claim_merkle(
    &recipient,
    campaign_id,
    amount,
    merkle_proof.path,
    merkle_proof.indices,
)?;

Project Structure

.
├── api/                    # API crate (types, SDK, PDA functions)
│   ├── src/
│   │   ├── lib.rs         # Main exports
│   │   ├── consts.rs      # Constants
│   │   ├── error.rs       # Error types
│   │   ├── instruction.rs # Instruction definitions
│   │   ├── event.rs       # Event definitions
│   │   ├── sdk.rs         # SDK helper functions
│   │   ├── pda.rs         # PDA derivation functions
│   │   ├── merkle.rs      # Merkle proof utilities
│   │   ├── loaders.rs     # Account validation traits
│   │   └── state/         # State structs
│   │       ├── config.rs
│   │       ├── campaign.rs
│   │       └── allocation.rs
│   └── Cargo.toml
├── program/                # Program crate (instruction handlers)
│   ├── src/
│   │   ├── lib.rs         # Entrypoint and dispatch
│   │   ├── initialize.rs
│   │   ├── create_mint.rs
│   │   ├── create_campaign.rs
│   │   ├── allocate.rs
│   │   ├── claim.rs
│   │   ├── direct_transfer.rs
│   │   ├── replenish.rs
│   │   ├── mint_tokens.rs
│   │   ├── update_config.rs
│   │   └── withdraw_fees.rs
│   └── Cargo.toml
├── docs/                   # Design documentation
├── Cargo.toml              # Workspace configuration
└── README.md

Setup & Installation

Prerequisites

  • Rust 1.70+ (with rust-toolchain specified)
  • Solana CLI tools
  • Anchor CLI (optional, for testing)

Build

# Build all crates
cargo build

# Build program specifically
cargo build-sbf --manifest-path program/Cargo.toml

# Run tests
cargo test

Deployment

# Deploy to devnet
solana program deploy target/deploy/airdrop_program.so --program-id <PROGRAM_ID>

# Set program ID in code
# Update api/src/lib.rs with actual program ID

Design Philosophy

"Simple Is Best, Yet Elegant"

  • Simple: Clear fee structure, straightforward operations
  • Elegant: Follows Steel framework patterns, consistent codebase
  • Professional: Thoughtful fee pricing and account management
  • Secure: PDA-based accounts, comprehensive validation
  • Flexible: Configurable fees, multiple allocation modes

Security Features

  1. PDA Treasury: No admin co-signing required for claims
  2. Account Validation: Comprehensive checks using Steel's chainable API
  3. Fee Account: Program-controlled PDA for secure fee storage
  4. Admin Verification: All admin operations verify authority
  5. Mint Authority: Can be revoked for immutable supply caps
  6. Merkle Verification: Cryptographic proof validation for claims

Reference Projects

This project follows patterns from:

  • miracle-copy: Merkle tree implementation, account validation patterns
  • escrow-copy: Fee account design, PDA treasury patterns
  • steel-master-copy: Framework idioms and best practices

License

Apache-2.0

Contributing

Contributions welcome! Please follow the existing code patterns and Steel framework idioms.

Status

Production Ready - All core features implemented and tested

  • ✅ Initialize program
  • ✅ Create mint with metadata
  • ✅ Create campaigns
  • ✅ Allocate tokens (on-chain allocation accounts)
  • ✅ Claim tokens (Methods 0, 1, 2)
  • ✅ Direct transfer batches
  • ✅ Fee collection and withdrawal
  • ✅ Config management

For detailed design documentation, see the docs/ directory.

Dependencies

~38MB
~645K SLoC