1use 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
12pub 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
36pub 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 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 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
93pub 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
103pub 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 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}