Skip to main content

sol_parser_sdk/
cpmm_creator_fee.rs

1//! CPMM creator-fee collection upgrade helpers. No RPC or account-existence check is required.
2
3use crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID;
4use crate::instr::raydium_cpmm::discriminators::{
5    COLLECT_CREATOR_FEE, COLLECT_CREATOR_FEE_PERMISSIONLESS,
6};
7pub use solana_instruction::{AccountMeta, Instruction};
8use solana_sdk::pubkey::Pubkey;
9
10pub const CREATOR_FEE_SHARE_DENOMINATOR: u64 = 1_000_000;
11
12/// Canonical address, including when the PDA has not been created on-chain.
13pub fn derive_creator_fee_share(creator: &Pubkey, amm_config: &Pubkey) -> (Pubkey, u8) {
14    Pubkey::find_program_address(
15        &[b"creator_fee_share", creator.as_ref(), amm_config.as_ref()],
16        &RAYDIUM_CPMM_PROGRAM_ID,
17    )
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum CollectionUpgradeError {
22    WrongProgram,
23    WrongInstruction,
24    WrongAccountCount,
25    WrongConfig,
26    WrongShareAddress,
27    UnexpectedSigner,
28}
29impl std::fmt::Display for CollectionUpgradeError {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        write!(f, "CPMM collection upgrade: {self:?}")
32    }
33}
34impl std::error::Error for CollectionUpgradeError {}
35
36/// Append mandatory readonly accounts to a pre-upgrade collection call.
37/// The first 14 accounts (including all privileges) remain unchanged. Already-upgraded
38/// calls have their appended addresses and signer flags checked and are returned
39/// unchanged. Writable supersets are accepted. Errors leave the instruction unchanged.
40/// `amm_config` must be the pool's config; this helper does not fetch PoolState.
41pub fn upgrade_creator_fee_collection_instruction(
42    ix: &mut Instruction,
43    amm_config: &Pubkey,
44) -> Result<Pubkey, CollectionUpgradeError> {
45    if ix.program_id != RAYDIUM_CPMM_PROGRAM_ID {
46        return Err(CollectionUpgradeError::WrongProgram);
47    }
48    let permissionless = match ix.data.as_slice() {
49        d if d == COLLECT_CREATOR_FEE => false,
50        d if d == COLLECT_CREATOR_FEE_PERMISSIONLESS => true,
51        _ => return Err(CollectionUpgradeError::WrongInstruction),
52    };
53    let upgraded_count = if permissionless { 16 } else { 15 };
54    if ix.accounts.len() != 14 && ix.accounts.len() != upgraded_count {
55        return Err(CollectionUpgradeError::WrongAccountCount);
56    }
57    if !permissionless && ix.accounts[3].pubkey != *amm_config {
58        return Err(CollectionUpgradeError::WrongConfig);
59    }
60    // Validate cheap metadata before performing the curve checks used by PDA
61    // derivation. All failures still leave the caller's instruction untouched.
62    if ix.accounts.len() == upgraded_count {
63        if permissionless && ix.accounts[14].pubkey != *amm_config {
64            return Err(CollectionUpgradeError::WrongConfig);
65        }
66        if ix.accounts[14..].iter().any(|account| account.is_signer) {
67            // The appended config/share PDAs cannot supply transaction signatures.
68            return Err(CollectionUpgradeError::UnexpectedSigner);
69        }
70    }
71    let creator = ix.accounts[usize::from(permissionless)].pubkey;
72    let share = derive_creator_fee_share(&creator, amm_config).0;
73    if ix.accounts.len() == upgraded_count {
74        if ix.accounts[upgraded_count - 1].pubkey != share {
75            return Err(CollectionUpgradeError::WrongShareAddress);
76        }
77        return Ok(share);
78    }
79    ix.accounts.reserve(upgraded_count - 14);
80    if permissionless {
81        ix.accounts.push(AccountMeta::new_readonly(*amm_config, false));
82    }
83    ix.accounts.push(AccountMeta::new_readonly(share, false));
84    Ok(share)
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub struct CreatorFeeSplit {
89    pub creator_amount: u64,
90    pub protocol_amount: u64,
91}
92
93/// Use the existing PDA's rate (including zero), otherwise the config's rate.
94/// The caller must supply a validated PDA for the relevant creator/config pair.
95pub fn effective_creator_fee_share_rate(
96    config_rate: u64,
97    override_rate: Option<u64>,
98) -> Option<u64> {
99    let rate = override_rate.unwrap_or(config_rate);
100    (rate <= CREATOR_FEE_SHARE_DENOMINATOR).then_some(rate)
101}
102
103/// Exact collection-time estimate: protocol share floors; all remaining dust goes
104/// to the creator. Apply independently to creator_fees_token_0 and _1. This does
105/// not mutate PoolState or infer a collection result from a shred transaction.
106pub fn split_creator_fee(accrued_fee: u64, share_rate: u64) -> Option<CreatorFeeSplit> {
107    if share_rate > CREATOR_FEE_SHARE_DENOMINATOR {
108        return None;
109    }
110    let protocol_amount =
111        ((accrued_fee as u128 * share_rate as u128) / CREATOR_FEE_SHARE_DENOMINATOR as u128) as u64;
112    Some(CreatorFeeSplit { creator_amount: accrued_fee - protocol_amount, protocol_amount })
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    fn collection(permissionless: bool) -> Instruction {
119        Instruction {
120            program_id: RAYDIUM_CPMM_PROGRAM_ID,
121            data: if permissionless {
122                COLLECT_CREATOR_FEE_PERMISSIONLESS
123            } else {
124                COLLECT_CREATOR_FEE
125            }
126            .to_vec(),
127            accounts: (0..14)
128                .map(|i| {
129                    if i % 2 == 0 {
130                        AccountMeta::new(Pubkey::new_unique(), i == 0)
131                    } else {
132                        AccountMeta::new_readonly(Pubkey::new_unique(), false)
133                    }
134                })
135                .collect(),
136        }
137    }
138    #[test]
139    fn already_upgraded_pdas_cannot_be_required_transaction_signers() {
140        for permissionless in [false, true] {
141            let mut ix = collection(permissionless);
142            let config = if permissionless { Pubkey::new_unique() } else { ix.accounts[3].pubkey };
143            upgrade_creator_fee_collection_instruction(&mut ix, &config).unwrap();
144            for index in 14..ix.accounts.len() {
145                let mut invalid = ix.clone();
146                invalid.accounts[index].is_signer = true;
147                let before = invalid.clone();
148                assert_eq!(
149                    upgrade_creator_fee_collection_instruction(&mut invalid, &config),
150                    Err(CollectionUpgradeError::UnexpectedSigner)
151                );
152                assert_eq!(invalid, before);
153            }
154            // Writable flags can be a valid superset; only impossible signatures fail.
155            for account in &mut ix.accounts[14..] {
156                account.is_writable = true;
157            }
158            assert!(upgrade_creator_fee_collection_instruction(&mut ix, &config).is_ok());
159        }
160    }
161    #[test]
162    fn appends_only_required_accounts_without_chain_existence_and_is_idempotent() {
163        for permissionless in [false, true] {
164            let mut ix = collection(permissionless);
165            let first = ix.accounts.clone();
166            let config = if permissionless { Pubkey::new_unique() } else { ix.accounts[3].pubkey };
167            let creator = ix.accounts[usize::from(permissionless)].pubkey;
168            let share = upgrade_creator_fee_collection_instruction(&mut ix, &config).unwrap();
169            assert_eq!(ix.accounts[..14], first);
170            assert_eq!(ix.accounts.len(), if permissionless { 16 } else { 15 });
171            assert_eq!(
172                share,
173                Pubkey::find_program_address(
174                    &[b"creator_fee_share", creator.as_ref(), config.as_ref()],
175                    &RAYDIUM_CPMM_PROGRAM_ID
176                )
177                .0
178            );
179            assert_eq!(ix.accounts.last().unwrap(), &AccountMeta::new_readonly(share, false));
180            if permissionless {
181                assert_eq!(ix.accounts[14], AccountMeta::new_readonly(config, false));
182            }
183            let complete = ix.clone();
184            assert_eq!(upgrade_creator_fee_collection_instruction(&mut ix, &config), Ok(share));
185            assert_eq!(ix, complete);
186        }
187    }
188    #[test]
189    fn rejects_wrong_program_data_count_config_or_share_without_mutation() {
190        for case in 0..5 {
191            let mut ix = collection(false);
192            let config = ix.accounts[3].pubkey;
193            match case {
194                0 => ix.program_id = Pubkey::new_unique(),
195                1 => ix.data.push(0),
196                2 => {
197                    ix.accounts.pop();
198                }
199                3 => ix.accounts[3].pubkey = Pubkey::new_unique(),
200                _ => ix.accounts.push(AccountMeta::new_readonly(Pubkey::new_unique(), false)),
201            }
202            let before = ix.clone();
203            assert!(upgrade_creator_fee_collection_instruction(&mut ix, &config).is_err());
204            assert_eq!(ix, before);
205        }
206    }
207    #[test]
208    fn exact_split_conserves_value_and_floors_protocol_share() {
209        for fee in [0, 1, 5, 999_999, 1_000_001, u64::MAX] {
210            for rate in [0, 1, 200_000, 999_999, 1_000_000] {
211                let split = split_creator_fee(fee, rate).unwrap();
212                assert_eq!(
213                    split.creator_amount as u128 + split.protocol_amount as u128,
214                    fee as u128
215                );
216                assert_eq!(split.protocol_amount as u128, fee as u128 * rate as u128 / 1_000_000);
217            }
218        }
219        assert_eq!(split_creator_fee(1, 200_000).unwrap().creator_amount, 1);
220        assert!(split_creator_fee(u64::MAX, 1_000_001).is_none());
221        assert_eq!(effective_creator_fee_share_rate(200_000, None), Some(200_000));
222        assert_eq!(effective_creator_fee_share_rate(200_000, Some(0)), Some(0));
223        assert_eq!(effective_creator_fee_share_rate(0, Some(300_000)), Some(300_000));
224        assert_eq!(effective_creator_fee_share_rate(0, Some(1_000_001)), None);
225    }
226}