A Solana program for managing quote-only fee distribution from DAMM v2 LP positions to investors based on locked token amounts from Streamflow vesting schedules.
This module creates and manages an "honorary" DAMM v2 LP position that accrues fees exclusively in the quote mint. Fees are distributed daily via a permissionless crank to investors pro-rata based on their still-locked tokens, with the remainder going to the creator.
- Quote-Only Enforcement: Validates and enforces that fees accrue only in quote mint
- 24-Hour Distribution Window: Permissionless crank callable once per day
- Pro-Rata Distribution: Distributes based on locked tokens from Streamflow
- Daily Caps: Optional limits on investor distributions
- Dust Handling: Minimum payout thresholds to avoid micro-transactions
- Pagination Support: Handles large investor sets across multiple transactions
- Pause Mechanism: Emergency pause/unpause functionality
- Idempotent Retries: Safe to retry failed transactions without double-paying
curl --proto '=https' --tlsv1.2 -sSfL https://solana-install.solana.workers.dev | bash
rustc --version && solana --version && anchor --version && node --version && yarn --version
# Clone repository
git clone https://github.com/1BDO/star-damm-fee-module.git
cd star-damm-fee-module
# Install dependencies
npm install
# Build program
anchor build
# Run tests
anchor testThis project includes comprehensive documentation:
- INTEGRATION_GUIDE.md - Production integration steps
- SYSTEM_ARCHITECTURE.md - Technical architecture and design
- TESTS.md - Testing documentation and results
- SECURITY_CHECKLIST.md - Security audit and best practices
const positionIndex = 0;
const investorFeeShareBps = 8000; // 80% max to investors
const minPayoutLamports = new BN(1000);
const tickLower = 0;
const tickUpper = 100;
await program.methods
.initializeHonoraryPosition(
investorFeeShareBps,
null, // No daily cap
minPayoutLamports,
tickLower,
tickUpper,
positionIndex
)
.accounts({
initializer: creator,
vault: vaultPubkey,
investorFeePositionOwnerPda: ownerPda,
honoraryPosition,
policy,
pool: poolPubkey,
quoteMint,
baseMint,
programQuoteTreasury,
// ...
})
.rpc();const y0 = new BN(1000000); // Total investor allocation at TGE
const isLastPage = true;
const investorStreams = [
{
investorPubkey: investor1,
streamPubkey: stream1,
investorQuoteAta: ata1,
y0Allocation: new BN(500000),
},
// ... more investors
];
await program.methods
.crankDistribute(y0, isLastPage, investorStreams)
.accounts({
crankCaller: crankerWallet,
vault: vaultPubkey,
pool: poolPubkey,
honoraryPosition,
ownerPda,
policy,
progress,
programQuoteTreasury,
creatorQuoteAta,
// ...
})
.remainingAccounts([
// Streamflow stream accounts
// Investor ATAs
])
.rpc();await program.methods
.updatePolicy(
7500, // New fee share
{ some: new BN(10_000_000) }, // Daily cap
new BN(5000) // New minimum payout
)
.accounts({
creator,
policy,
})
.rpc();// Pause
await program.methods
.setPauseState(true)
.accounts({ creator, policy })
.rpc();
// Unpause
await program.methods
.setPauseState(false)
.accounts({ creator, policy })
.rpc();1. Calculate locked fraction:
f_locked(t) = locked_total(t) / Y0
2. Determine eligible share:
eligible_share_bps = min(investor_fee_share_bps, floor(f_locked(t) * 10000))
3. Calculate investor fee:
investor_fee_quote = floor(claimed_quote * eligible_share_bps / 10000)
4. Apply daily cap:
capped_fee = min(investor_fee_quote, remaining_capacity)
5. Calculate creator share:
creator_share = claimed_quote - capped_fee
1. For each investor i:
weight_i(t) = locked_i(t) / locked_total(t)
2. Payout calculation:
payout_i = floor(investor_fee_quote * weight_i(t))
3. Apply dust threshold:
if payout_i >= min_payout_lamports:
distribute payout_i
else:
carry_over += payout_i
pub struct PositionInitializedEvent {
pub pool: Pubkey,
pub position: Pubkey,
pub quote_mint: Pubkey,
pub tick_lower: i32,
pub tick_upper: i32,
pub investor_fee_share_bps: u16,
}pub struct DistributionEvent {
pub day_index: u32,
pub total_claimed: u64,
pub investor_share: u64,
pub creator_share: u64,
pub investor_count: u32,
pub pagination_page: u16,
pub is_final_page: bool,
}| Code | Error | Description |
|---|---|---|
| 6000 | InvalidPDA | Invalid PDA derivation |
| 6001 | InvalidInvestorFeeShareBps | Invalid investor fee share basis points |
| 6002 | InvalidTickRange | Invalid tick range or spacing |
| 6003 | InvalidPoolConfiguration | Pool configuration does not support quote-only fee accrual |
| 6004 | BaseFeesDetected | Base fees detected - quote-only enforcement failed |
| 6005 | DistributionWindowNotElapsed | Distribution window not elapsed (24h gate) |
| 6006 | InvalidStreamflowData | Invalid Streamflow stream data |
| 6007 | MathOverflow | Mathematical overflow in distribution calculation |
| 6008 | InsufficientQuoteFees | Insufficient quote fees for distribution |
| 6009 | InvalidInvestorStreamInfo | Invalid investor stream info |
| 6010 | Unauthorized | Unauthorized - creator only |
| 6011 | SystemPaused | System is paused |
| 6012 | InvalidPagination | Invalid pagination data |
| 6013 | DailyCapExceeded | Daily cap exceeded |
| 6014 | InvalidPositionIndex | Invalid position index |
| 6015 | QuoteMintValidationFailed | Quote mint validation failed |
| 6016 | DistributionAlreadyCompleted | Distribution already completed for this page |