# 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.
```rust
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.
```rust
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.
```rust
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`.
```rust
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)
```rust
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).
```rust
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.
```rust
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).
```rust
pub struct MintTokens {
pub campaign_id: [u8; 32],
pub amount: [u8; 8],
}
```
**Fee:** None
### 9. UpdateConfig
Update fee rates and admin authority (admin-only).
```rust
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).
```rust
pub struct WithdrawFees {
pub amount: [u8; 8], // 0 = withdraw all
}
```
## Fee Structure
### Recommended Default Fees
| 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
```rust
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
```rust
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)
```rust
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
```rust
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
```rust
// 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
```bash
# Build all crates
cargo build
# Build program specifically
cargo build-sbf --manifest-path program/Cargo.toml
# Run tests
cargo test
```
### Deployment
```bash
# 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.