use crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID;
use crate::instr::raydium_cpmm::discriminators::{
COLLECT_CREATOR_FEE, COLLECT_CREATOR_FEE_PERMISSIONLESS,
};
pub use solana_instruction::{AccountMeta, Instruction};
use solana_sdk::pubkey::Pubkey;
pub const CREATOR_FEE_SHARE_DENOMINATOR: u64 = 1_000_000;
pub fn derive_creator_fee_share(creator: &Pubkey, amm_config: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[b"creator_fee_share", creator.as_ref(), amm_config.as_ref()],
&RAYDIUM_CPMM_PROGRAM_ID,
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CollectionUpgradeError {
WrongProgram,
WrongInstruction,
WrongAccountCount,
WrongConfig,
WrongShareAddress,
UnexpectedSigner,
}
impl std::fmt::Display for CollectionUpgradeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CPMM collection upgrade: {self:?}")
}
}
impl std::error::Error for CollectionUpgradeError {}
pub fn upgrade_creator_fee_collection_instruction(
ix: &mut Instruction,
amm_config: &Pubkey,
) -> Result<Pubkey, CollectionUpgradeError> {
if ix.program_id != RAYDIUM_CPMM_PROGRAM_ID {
return Err(CollectionUpgradeError::WrongProgram);
}
let permissionless = match ix.data.as_slice() {
d if d == COLLECT_CREATOR_FEE => false,
d if d == COLLECT_CREATOR_FEE_PERMISSIONLESS => true,
_ => return Err(CollectionUpgradeError::WrongInstruction),
};
let upgraded_count = if permissionless { 16 } else { 15 };
if ix.accounts.len() != 14 && ix.accounts.len() != upgraded_count {
return Err(CollectionUpgradeError::WrongAccountCount);
}
if !permissionless && ix.accounts[3].pubkey != *amm_config {
return Err(CollectionUpgradeError::WrongConfig);
}
if ix.accounts.len() == upgraded_count {
if permissionless && ix.accounts[14].pubkey != *amm_config {
return Err(CollectionUpgradeError::WrongConfig);
}
if ix.accounts[14..].iter().any(|account| account.is_signer) {
return Err(CollectionUpgradeError::UnexpectedSigner);
}
}
let creator = ix.accounts[usize::from(permissionless)].pubkey;
let share = derive_creator_fee_share(&creator, amm_config).0;
if ix.accounts.len() == upgraded_count {
if ix.accounts[upgraded_count - 1].pubkey != share {
return Err(CollectionUpgradeError::WrongShareAddress);
}
return Ok(share);
}
ix.accounts.reserve(upgraded_count - 14);
if permissionless {
ix.accounts.push(AccountMeta::new_readonly(*amm_config, false));
}
ix.accounts.push(AccountMeta::new_readonly(share, false));
Ok(share)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CreatorFeeSplit {
pub creator_amount: u64,
pub protocol_amount: u64,
}
pub fn effective_creator_fee_share_rate(
config_rate: u64,
override_rate: Option<u64>,
) -> Option<u64> {
let rate = override_rate.unwrap_or(config_rate);
(rate <= CREATOR_FEE_SHARE_DENOMINATOR).then_some(rate)
}
pub fn split_creator_fee(accrued_fee: u64, share_rate: u64) -> Option<CreatorFeeSplit> {
if share_rate > CREATOR_FEE_SHARE_DENOMINATOR {
return None;
}
let protocol_amount =
((accrued_fee as u128 * share_rate as u128) / CREATOR_FEE_SHARE_DENOMINATOR as u128) as u64;
Some(CreatorFeeSplit { creator_amount: accrued_fee - protocol_amount, protocol_amount })
}
#[cfg(test)]
mod tests {
use super::*;
fn collection(permissionless: bool) -> Instruction {
Instruction {
program_id: RAYDIUM_CPMM_PROGRAM_ID,
data: if permissionless {
COLLECT_CREATOR_FEE_PERMISSIONLESS
} else {
COLLECT_CREATOR_FEE
}
.to_vec(),
accounts: (0..14)
.map(|i| {
if i % 2 == 0 {
AccountMeta::new(Pubkey::new_unique(), i == 0)
} else {
AccountMeta::new_readonly(Pubkey::new_unique(), false)
}
})
.collect(),
}
}
#[test]
fn already_upgraded_pdas_cannot_be_required_transaction_signers() {
for permissionless in [false, true] {
let mut ix = collection(permissionless);
let config = if permissionless { Pubkey::new_unique() } else { ix.accounts[3].pubkey };
upgrade_creator_fee_collection_instruction(&mut ix, &config).unwrap();
for index in 14..ix.accounts.len() {
let mut invalid = ix.clone();
invalid.accounts[index].is_signer = true;
let before = invalid.clone();
assert_eq!(
upgrade_creator_fee_collection_instruction(&mut invalid, &config),
Err(CollectionUpgradeError::UnexpectedSigner)
);
assert_eq!(invalid, before);
}
for account in &mut ix.accounts[14..] {
account.is_writable = true;
}
assert!(upgrade_creator_fee_collection_instruction(&mut ix, &config).is_ok());
}
}
#[test]
fn appends_only_required_accounts_without_chain_existence_and_is_idempotent() {
for permissionless in [false, true] {
let mut ix = collection(permissionless);
let first = ix.accounts.clone();
let config = if permissionless { Pubkey::new_unique() } else { ix.accounts[3].pubkey };
let creator = ix.accounts[usize::from(permissionless)].pubkey;
let share = upgrade_creator_fee_collection_instruction(&mut ix, &config).unwrap();
assert_eq!(ix.accounts[..14], first);
assert_eq!(ix.accounts.len(), if permissionless { 16 } else { 15 });
assert_eq!(
share,
Pubkey::find_program_address(
&[b"creator_fee_share", creator.as_ref(), config.as_ref()],
&RAYDIUM_CPMM_PROGRAM_ID
)
.0
);
assert_eq!(ix.accounts.last().unwrap(), &AccountMeta::new_readonly(share, false));
if permissionless {
assert_eq!(ix.accounts[14], AccountMeta::new_readonly(config, false));
}
let complete = ix.clone();
assert_eq!(upgrade_creator_fee_collection_instruction(&mut ix, &config), Ok(share));
assert_eq!(ix, complete);
}
}
#[test]
fn rejects_wrong_program_data_count_config_or_share_without_mutation() {
for case in 0..5 {
let mut ix = collection(false);
let config = ix.accounts[3].pubkey;
match case {
0 => ix.program_id = Pubkey::new_unique(),
1 => ix.data.push(0),
2 => {
ix.accounts.pop();
}
3 => ix.accounts[3].pubkey = Pubkey::new_unique(),
_ => ix.accounts.push(AccountMeta::new_readonly(Pubkey::new_unique(), false)),
}
let before = ix.clone();
assert!(upgrade_creator_fee_collection_instruction(&mut ix, &config).is_err());
assert_eq!(ix, before);
}
}
#[test]
fn exact_split_conserves_value_and_floors_protocol_share() {
for fee in [0, 1, 5, 999_999, 1_000_001, u64::MAX] {
for rate in [0, 1, 200_000, 999_999, 1_000_000] {
let split = split_creator_fee(fee, rate).unwrap();
assert_eq!(
split.creator_amount as u128 + split.protocol_amount as u128,
fee as u128
);
assert_eq!(split.protocol_amount as u128, fee as u128 * rate as u128 / 1_000_000);
}
}
assert_eq!(split_creator_fee(1, 200_000).unwrap().creator_amount, 1);
assert!(split_creator_fee(u64::MAX, 1_000_001).is_none());
assert_eq!(effective_creator_fee_share_rate(200_000, None), Some(200_000));
assert_eq!(effective_creator_fee_share_rate(200_000, Some(0)), Some(0));
assert_eq!(effective_creator_fee_share_rate(0, Some(300_000)), Some(300_000));
assert_eq!(effective_creator_fee_share_rate(0, Some(1_000_001)), None);
}
}