1 unstable release
| 0.1.0 | Nov 6, 2025 |
|---|
#1 in #solana-token
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
Allocateinstruction) - Off-chain Record: Claims against off-chain database records (via
ClaimMethod 1, noAllocateneeded) - Merkle Mode: Efficient merkle tree-based claims for large campaigns (via
ClaimMethod 2, noAllocateneeded)
✅ 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
Allocatecall) - Method 1 (record): Claim against off-chain database record (with signature, no
Allocateneeded) - Method 2 (merkle): Claim against merkle proof (requires merkle root in campaign, no
Allocateneeded)
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
Recommended Default Fees
| 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
- PDA Treasury: No admin co-signing required for claims
- Account Validation: Comprehensive checks using Steel's chainable API
- Fee Account: Program-controlled PDA for secure fee storage
- Admin Verification: All admin operations verify authority
- Mint Authority: Can be revoked for immutable supply caps
- Merkle Verification: Cryptographic proof validation for claims
Reference Projects
This project follows patterns from:
miracle-copy: Merkle tree implementation, account validation patternsescrow-copy: Fee account design, PDA treasury patternssteel-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