A Solana program for managing honorary DAMM v2 LP positions and distributing quote-only fees to investors based on their locked token balances.
This program implements a permissionless 24-hour distribution crank that:
- Creates and manages an honorary DAMM v2 LP position owned by a program PDA
- Accrues fees exclusively in the quote mint (enforced)
- Distributes fees to investors pro-rata based on their locked amounts (tracked via DepositorRecord)
- Routes remaining fees to the creator wallet after investor distribution
-
Honorary Position Management
- Creates empty DAMM v2 position owned by program PDA
- Validates quote-only fee accrual through tick range and weight configuration
- Rejects any configuration that could accrue base token fees
-
DepositorRecord System (Streamflow Alternative)
- Tracks individual investor deposits and balances
- Stores locked amounts (current_usdc_balance) used for distribution weights
- Maintains withdrawal history and share percentages
-
24h Distribution Crank
- Permissionless execution once per 24 hours
- Supports pagination for large investor sets
- Implements idempotent resumption
- Enforces daily caps and dust thresholds
-
Distribution Math
Y0 = total investor allocation at TGE locked_total(t) = sum of current_usdc_balance across all investors f_locked(t) = locked_total(t) / Y0 eligible_investor_share_bps = min(investor_fee_share_bps, floor(f_locked(t) * 10000)) investor_fee_quote = floor(claimed_quote * eligible_investor_share_bps / 10000) For each investor: weight_i(t) = investor.current_usdc_balance / locked_total(t) payout_i = floor(investor_fee_quote * weight_i(t))
Initialize the distribution policy configuration.
Parameters:
y0_allocation: Total investor allocation at TGE (used for f_locked calculation)investor_fee_share_bps: Maximum investor share (e.g., 5000 = 50%)min_payout_lamports: Minimum payout threshold (dust handling)daily_cap_lamports: Daily distribution limit (0 = no cap)creator_wallet: Creator's wallet for remainder routingquote_mint: Quote token mint (for validation)
Accounts:
admin: Signer who initializes the configdistribution_config: PDA [b"distribution_config"]
Create an honorary DAMM v2 LP position that accrues quote-only fees.
Config Validation:
base_weight_bps: Must be 0quote_weight_bps: Must be 10000 (100%)lower_tick: Must be <= -443636upper_tick: Must be >= 443636fee_tier: Must be 100, 500, 3000, or 10000 bps
Accounts:
signer: Position owner (program PDA)amm_program: DAMM v2 programpool,position,position_nft_mint,position_nft_account: Position accountsbase_mint,quote_mint: Token mints- Token vaults and accounts
Investors deposit SOL/USDC to establish their locked balances.
Parameters:
sol_amount: Amount of SOL to deposit (lamports)usdc_amount: Amount of USDC to deposit (smallest unit)
Accounts:
investor: Signer making the depositsol_vault: Program SOL vault PDA [b"deposit_vault", b"sol"]usdc_vault: Program USDC vault PDA [b"deposit_vault", usdc_mint]depositor_record: PDA [b"investor_record", investor]vault_stats: PDA [b"deposit_vault", b"stats"]
Investors withdraw their deposited amounts.
Parameters:
sol_amount: Amount of SOL to withdrawusdc_amount: Amount of USDC to withdraw
Accounts: Same as deposit, plus investor token accounts
Claim fees from the honorary position to program vaults.
Quote-Only Enforcement:
- Records balance before/after claim
- Fails if ANY base fees are detected
- Only proceeds if base_claimed == 0
Accounts:
fee_collector: Program authority PDA [b"fee_collector"]amm_program: DAMM v2 programpool,position: Position accountsprogram_token_a_vault: Base token vault (must remain at 0)program_token_b_vault: Quote token vault (receives fees)
Initiate or continue daily fee distribution (permissionless).
Flow:
- Start new day if 24h elapsed since last distribution
- Validate no base fees (fail if base_vault.amount > 0)
- Calculate eligible investor share using f_locked formula
- Advance pagination cursor
- Track daily distributed and carry-over
Parameters:
page_index: Current page (must match cursor for idempotency)investors_count: Number of investors in this pageis_final_page: Whether this is the last page
Accounts:
payer: Transaction payerfee_collector: Program authority PDAprogram_token_a_vault: Base vault (must be 0)program_token_b_vault: Quote vault (source of fees)vault_stats: Global vault statisticsdistribution_config: Distribution policycrank_state: Pagination and timing state PDA [b"crank_state"]
Distribute quote fees to a specific investor (called per investor during crank).
Math:
- Calculates weight based on investor's current_usdc_balance
- Applies dust threshold (min_payout_lamports)
- Updates carry-over for dust amounts
- Checks daily cap before transfer
Parameters:
total_investor_fee: Total investor allocation for this distribution
Accounts:
fee_collector: Program authorityprogram_quote_vault: Quote fee vaultinvestor_quote_account: Investor's quote token accountdepositor_record: Investor's recordvault_stats: Global statisticsdistribution_config: Policy configcrank_state: Distribution stateinvestor: Investor signer
Close the distribution day and route remaining fees to creator.
Flow:
- Validate day is in progress
- Transfer all remaining quote tokens to creator
- Close the day (day_state = 2)
- Reset for next 24h period
Accounts:
fee_collector: Program authorityprogram_quote_vault: Quote fee vaultcreator_quote_account: Creator's quote token account (must match config)distribution_config: Policy configcrank_state: Distribution state
| Account | Seeds |
|---|---|
| fee_collector | [b"fee_collector"] |
| fee_vault (base) | [b"fee_vault", base_mint] |
| fee_vault (quote) | [b"fee_vault", quote_mint] |
| deposit_vault (SOL) | [b"deposit_vault", b"sol"] |
| deposit_vault (USDC) | [b"deposit_vault", usdc_mint] |
| vault_stats | [b"deposit_vault", b"stats"] |
| investor_record | [b"investor_record", investor_pubkey] |
| crank_state | [b"crank_state"] |
| distribution_config | [b"distribution_config"] |
pub struct DistributionConfig {
pub y0_allocation: u64, // TGE allocation for f_locked calc
pub investor_fee_share_bps: u16, // Max investor share (0-10000)
pub min_payout_lamports: u64, // Dust threshold
pub daily_cap_lamports: u64, // Daily limit (0 = unlimited)
pub creator_wallet: Pubkey, // Remainder destination
pub quote_mint: Pubkey, // Quote token mint
pub bump: u8,
}pub struct CrankState {
pub last_distribution_timestamp: i64,
pub current_day: u32,
pub distribution_count: u32,
pub pagination_cursor: u32, // For idempotent resumption
pub investors_processed_today: u32,
pub daily_distributed: u64,
pub carry_over: u64, // Accumulated dust
pub day_state: u8, // 0=not started, 1=in progress, 2=closed
pub bump: u8,
}pub struct DepositorRecord {
pub investor: Pubkey,
pub total_sol_deposited: u64,
pub total_usdc_deposited: u64,
pub current_sol_balance: u64, // Used for distribution weight
pub current_usdc_balance: u64, // Used for distribution weight
pub total_sol_withdrawn: u64,
pub total_usdc_withdrawn: u64,
pub first_deposit_timestamp: i64,
pub last_activity_timestamp: i64,
pub deposit_count: u32,
pub withdrawal_count: u32,
pub bump: u8,
}pub struct VaultStats {
pub total_sol_deposited: u64,
pub total_usdc_deposited: u64,
pub current_total_sol: u64, // Sum of all current_sol_balance
pub current_total_usdc: u64, // Used for locked_total(t)
pub total_sol_withdrawn: u64,
pub total_usdc_withdrawn: u64,
pub depositor_count: u32,
pub last_update_timestamp: i64,
pub bump: u8,
}| Code | Message |
|---|---|
| BaseFeesDetected | Base fees detected - quote-only position violated |
| DistributionTooFrequent | Distribution too frequent - must wait 24 hours |
| DailyCapExceeded | Daily distribution cap exceeded |
| PayoutBelowMinimum | Payout below minimum threshold |
| InvalidPaginationCursor | Invalid pagination cursor |
| DayAlreadyClosed | Day already closed - cannot distribute |
| DistributionNotStarted | Distribution not started for this day |
| InvalidY0Allocation | Invalid Y0 allocation amount |
- Owned by program PDA (fee_collector)
- Quote-only validation via config params
- Deterministic preflight checks
- Rejects base fee configurations
- Wide tick range (-443636 to +443636)
- Balance tracking before/after claim
- Transaction fails if base_claimed > 0
- Quote mint validation in config
- Crank fails if base vault has any balance
- 86400 second cooldown enforced
- Pagination support with cursor tracking
- Idempotent resumption (page_index must match cursor)
- Day state machine (0=not started, 1=in progress, 2=closed)
- f_locked(t) = locked_total(t) / Y0
- eligible_investor_share = min(investor_fee_share_bps, f_locked_bps)
- Pro-rata weights per investor
- Floor division for all calculations
- Daily cap enforcement
- Min payout threshold
- Carry-over tracking
- Dust accumulation across pages
- Separate instruction after final page
- Transfers remaining balance to creator
- Validates creator wallet from config
- Closes day state
- Replaces Streamflow with custom tracking
- current_usdc_balance = locked amount
- Supports deposits and withdrawals
- Share percentage calculations
await program.methods
.initializeDistributionConfig({
y0Allocation: new anchor.BN(1_000_000_000_000), // 1M USDC (6 decimals)
investorFeeShareBps: 5000, // 50%
minPayoutLamports: new anchor.BN(10_000),
dailyCapLamports: new anchor.BN(0), // No cap
creatorWallet: creatorPublicKey,
quoteMint: usdcMint,
})
.accounts({
admin: adminKeypair.publicKey,
distributionConfig: distributionConfigPDA,
systemProgram: SystemProgram.programId,
})
.signers([adminKeypair])
.rpc();await program.methods
.initializeHonoraryPosition({
baseWeightBps: 0,
quoteWeightBps: 10000,
lowerTick: -443636,
upperTick: 443636,
feeTier: 100,
})
.accounts({
signer: pdaOwner,
ammProgram: DAMM_V2_PROGRAM_ID,
pool: poolPublicKey,
// ... other accounts
})
.rpc();await program.methods
.deposit({
solAmount: new anchor.BN(0),
usdcAmount: new anchor.BN(100_000_000), // 100 USDC
})
.accounts({
investor: investorKeypair.publicKey,
feeCollector: feeCollectorPDA,
solVault: solVaultPDA,
usdcVault: usdcVaultPDA,
usdcMint: usdcMint,
investorUsdcAccount: investorUsdcAccount,
depositorRecord: depositorRecordPDA,
vaultStats: vaultStatsPDA,
// ...
})
.signers([investorKeypair])
.rpc();await program.methods
.claimFeesToPda()
.accounts({
feeCollector: feeCollectorPDA,
ammProgram: DAMM_V2_PROGRAM_ID,
pool: poolPublicKey,
position: positionPublicKey,
programTokenAVault: baseVaultPDA, // Must stay at 0
programTokenBVault: quoteVaultPDA, // Receives fees
// ...
})
.rpc();// Start crank (first page)
await program.methods
.crankFeeDistribution({
pageIndex: 0,
investorsCount: 10,
isFinalPage: false,
})
.accounts({
payer: payerKeypair.publicKey,
feeCollector: feeCollectorPDA,
programTokenAVault: baseVaultPDA,
programTokenBVault: quoteVaultPDA,
vaultStats: vaultStatsPDA,
distributionConfig: distributionConfigPDA,
crankState: crankStatePDA,
// ...
})
.signers([payerKeypair])
.rpc();
// Distribute to each investor in page
for (const investor of investorsInPage) {
await program.methods
.distributeToInvestor({
totalInvestorFee: calculatedInvestorFee,
})
.accounts({
feeCollector: feeCollectorPDA,
programQuoteVault: quoteVaultPDA,
investorQuoteAccount: investor.quoteAccount,
depositorRecord: investor.recordPDA,
investor: investor.publicKey,
// ...
})
.signers([investor.keypair])
.rpc();
}await program.methods
.routeCreatorRemainder()
.accounts({
feeCollector: feeCollectorPDA,
programQuoteVault: quoteVaultPDA,
creatorQuoteAccount: creatorQuoteAccount,
distributionConfig: distributionConfigPDA,
crankState: crankStatePDA,
// ...
})
.rpc();The program includes comprehensive tests covering:
- Honorary position creation and validation
- Quote-only fee enforcement
- Deposit/withdrawal flows
- Distribution math with various locked amounts
- Pagination and cursor tracking
- Daily cap and dust handling
- Creator remainder routing
Run tests:
anchor test- Quote-Only Enforcement: The program fails deterministically if ANY base fees are detected
- 24h Gating: Enforced via timestamp comparison with 86400 second cooldown
- Pagination Idempotency: Cursor validation prevents double-payment
- Daily Caps: Checked before each transfer to prevent over-distribution
- PDA Ownership: All sensitive operations require PDA signer
- Dust Handling: Small amounts carried over instead of lost
- Creator Validation: Ensures creator wallet matches config
MIT