use solana_program::{ account_info::{next_account_info, AccountInfo}, clock::Clock, entrypoint, entrypoint::ProgramResult, program::{invoke, invoke_signed}, program_error::ProgramError, pubkey::Pubkey, rent::Rent, system_instruction, system_program, sysvar::Sysvar, }; use std::str::FromStr; pub mod settings; solana_program::declare_id!("c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW"); entrypoint!(process_instruction); const IX_INIT: u8 = 1; const IX_UPDATE_COEF_LIMIT: u8 = 2; const IX_GRANT_MANAGER_LIMITS: u8 = 3; const IX_BUY_TICKET: u8 = 4; const IX_BUY_TICKET_USD: u8 = 5; const IX_BUY_TICKET_SOL: u8 = 6; const IX_MANAGER_ADD_TICKET: u8 = 7; const IX_STEP_PAYOUT: u8 = 8; const IX_CHANGE_TICKET_RECIPIENT: u8 = 9; #[repr(u32)] #[derive(Clone, Copy, Debug)] enum PaymentsError { InvalidInstruction = 1, InvalidSigner = 2, InvalidPdaAddress = 3, EmptyState = 4, InvalidAccountData = 5, InvalidSettingsWallet = 6, InvalidInflowVault = 7, InvalidDaoWallet = 8, UnauthorizedManager = 9, UnauthorizedDao = 10, InvalidCoefficient = 11, InvalidLimit = 12, InvalidCallReward = 13, InvalidAmount = 14, QueueTemporarilyPaused = 15, InvalidPayoutAmount = 16, NotEnoughInflowForStep = 17, TicketAlreadyPaid = 18, InvalidTicketRecipient = 19, InvalidTicketIndex = 20, InvalidTicketQueue = 21, InvalidManagerWallet = 22, ManagerLimitExceeded = 23, UnauthorizedTicketOwner = 24, CannotChangeRecipientForNextPayoutTicket = 25, InvalidOracleAccount = 26, InvalidOracleFeed = 27, InvalidOracleFeedConfig = 28, OraclePriceTooOld = 29, InvalidOraclePrice = 30, SlippageExceeded = 31, MathOverflow = 32, SystemAlreadyInitialized = 33, MissingRequiredSignature = 34, InvalidSystemProgram = 35, PdaAlreadyExists = 36, } impl From for ProgramError { fn from(value: PaymentsError) -> Self { ProgramError::Custom(value as u32) } } macro_rules! require { ($cond:expr, $err:expr) => { if !($cond) { return Err(ProgramError::from($err)); } }; } macro_rules! require_keys_eq { ($left:expr, $right:expr, $err:expr) => { if $left != $right { return Err(ProgramError::from($err)); } }; } #[derive(Clone, Debug)] pub struct UpdateCoefLimitArgs { pub coef_ppm: u64, pub limit_usd_cents: u64, pub call_reward_lamports: u64, } #[derive(Clone, Debug)] pub struct GrantManagerLimitsArgs { pub manager_wallet: Pubkey, pub add_q1_usd_cents: u64, pub add_q2_usd_cents: u64, pub add_q3_usd_cents: u64, } #[derive(Clone, Debug)] pub struct BuyTicketArgs { pub amount_lamports: u64, pub recipient_wallet: Pubkey, } #[derive(Clone, Debug)] pub struct BuyTicketUsdArgs { pub amount_usd_cents: u64, pub max_pay_lamports: u64, pub recipient_wallet: Pubkey, } #[derive(Clone, Debug)] pub struct BuyTicketSolArgs { pub amount_lamports: u64, pub min_expected_usd_cents: u64, pub recipient_wallet: Pubkey, } #[derive(Clone, Debug)] pub struct ManagerAddTicketArgs { pub queue_id: u8, pub recipient_wallet: Pubkey, pub payout_usd_cents: u64, } #[derive(Clone, Debug)] pub struct ChangeTicketRecipientArgs { pub new_recipient_wallet: Pubkey, } #[derive(Clone, Debug)] pub struct ConfigState { pub version: u8, pub dao_wallet: Pubkey, pub inflow_vault: Pubkey, } #[derive(Clone, Debug)] pub struct CoefLimitState { pub version: u8, pub coef_ppm: u64, pub limit_usd_cents: u64, pub call_reward_lamports: u64, } #[derive(Clone, Debug)] pub struct QueuesState { pub version: u8, pub q1_tickets_total: u64, pub q1_tickets_paid: u64, pub q1_sum_total_usd_cents: u64, pub q1_sum_paid_usd_cents: u64, pub q2_tickets_total: u64, pub q2_tickets_paid: u64, pub q2_sum_total_usd_cents: u64, pub q2_sum_paid_usd_cents: u64, pub q3_tickets_total: u64, pub q3_tickets_paid: u64, pub q3_sum_total_usd_cents: u64, pub q3_sum_paid_usd_cents: u64, } #[derive(Clone, Debug)] pub struct TicketState { pub version: u8, pub queue_id: u8, pub index: u64, pub is_paid: bool, pub recipient_wallet: Pubkey, pub payout_usd_cents: u64, pub debt_before_usd_cents: u64, } #[derive(Clone, Debug)] pub struct ManagerAllowanceState { pub version: u8, pub manager_wallet: Pubkey, pub q1_available_usd_cents: u64, pub q2_available_usd_cents: u64, pub q3_available_usd_cents: u64, } #[derive(Clone, Debug)] pub struct VaultState { pub version: u8, } struct SolUsdPrice { price_num: u128, price_den: u128, } struct Reader<'a> { data: &'a [u8], cursor: usize, error: ProgramError, } impl<'a> Reader<'a> { fn new(data: &'a [u8], error: ProgramError) -> Self { Self { data, cursor: 0, error } } fn read_u8(&mut self) -> Result { let value = *self .data .get(self.cursor) .ok_or(self.error.clone())?; self.cursor += 1; Ok(value) } fn read_u64(&mut self) -> Result { let end = self.cursor.checked_add(8).ok_or(self.error.clone())?; let slice = self .data .get(self.cursor..end) .ok_or(self.error.clone())?; self.cursor = end; Ok(u64::from_le_bytes( slice.try_into().map_err(|_| self.error.clone())?, )) } fn read_pubkey(&mut self) -> Result { let end = self.cursor.checked_add(32).ok_or(self.error.clone())?; let slice = self .data .get(self.cursor..end) .ok_or(self.error.clone())?; self.cursor = end; Ok(Pubkey::new_from_array( slice.try_into().map_err(|_| self.error.clone())?, )) } fn finish(self) -> Result<(), ProgramError> { require!(self.cursor == self.data.len(), PaymentsError::InvalidInstruction); Ok(()) } } trait StateCodec: Sized { fn encoded_len() -> usize; fn encode(&self) -> Vec; fn decode(data: &[u8]) -> Result; } impl StateCodec for ConfigState { fn encoded_len() -> usize { 1 + 32 + 32 } fn encode(&self) -> Vec { let mut out = Vec::with_capacity(65); out.push(self.version); out.extend_from_slice(self.dao_wallet.as_ref()); out.extend_from_slice(self.inflow_vault.as_ref()); out } fn decode(data: &[u8]) -> Result { let mut reader = Reader::new(data, PaymentsError::InvalidAccountData.into()); let version = reader.read_u8()?; let dao_wallet = reader.read_pubkey()?; let inflow_vault = reader.read_pubkey()?; require!(reader.cursor == data.len(), PaymentsError::InvalidAccountData); Ok(Self { version, dao_wallet, inflow_vault }) } } impl StateCodec for CoefLimitState { fn encoded_len() -> usize { 1 + 8 + 8 + 8 } fn encode(&self) -> Vec { let mut out = Vec::with_capacity(25); out.push(self.version); out.extend_from_slice(&self.coef_ppm.to_le_bytes()); out.extend_from_slice(&self.limit_usd_cents.to_le_bytes()); out.extend_from_slice(&self.call_reward_lamports.to_le_bytes()); out } fn decode(data: &[u8]) -> Result { let mut reader = Reader::new(data, PaymentsError::InvalidAccountData.into()); let version = reader.read_u8()?; let coef_ppm = reader.read_u64()?; let limit_usd_cents = reader.read_u64()?; let call_reward_lamports = reader.read_u64()?; require!(reader.cursor == data.len(), PaymentsError::InvalidAccountData); Ok(Self { version, coef_ppm, limit_usd_cents, call_reward_lamports }) } } impl StateCodec for QueuesState { fn encoded_len() -> usize { 1 + 12 * 8 } fn encode(&self) -> Vec { let mut out = Vec::with_capacity(97); out.push(self.version); for value in [ self.q1_tickets_total, self.q1_tickets_paid, self.q1_sum_total_usd_cents, self.q1_sum_paid_usd_cents, self.q2_tickets_total, self.q2_tickets_paid, self.q2_sum_total_usd_cents, self.q2_sum_paid_usd_cents, self.q3_tickets_total, self.q3_tickets_paid, self.q3_sum_total_usd_cents, self.q3_sum_paid_usd_cents, ] { out.extend_from_slice(&value.to_le_bytes()); } out } fn decode(data: &[u8]) -> Result { let mut reader = Reader::new(data, PaymentsError::InvalidAccountData.into()); let version = reader.read_u8()?; Ok(Self { version, q1_tickets_total: reader.read_u64()?, q1_tickets_paid: reader.read_u64()?, q1_sum_total_usd_cents: reader.read_u64()?, q1_sum_paid_usd_cents: reader.read_u64()?, q2_tickets_total: reader.read_u64()?, q2_tickets_paid: reader.read_u64()?, q2_sum_total_usd_cents: reader.read_u64()?, q2_sum_paid_usd_cents: reader.read_u64()?, q3_tickets_total: reader.read_u64()?, q3_tickets_paid: reader.read_u64()?, q3_sum_total_usd_cents: reader.read_u64()?, q3_sum_paid_usd_cents: reader.read_u64()?, }) } } impl StateCodec for TicketState { fn encoded_len() -> usize { 1 + 1 + 8 + 1 + 32 + 8 + 8 } fn encode(&self) -> Vec { let mut out = Vec::with_capacity(59); out.push(self.version); out.push(self.queue_id); out.extend_from_slice(&self.index.to_le_bytes()); out.push(u8::from(self.is_paid)); out.extend_from_slice(self.recipient_wallet.as_ref()); out.extend_from_slice(&self.payout_usd_cents.to_le_bytes()); out.extend_from_slice(&self.debt_before_usd_cents.to_le_bytes()); out } fn decode(data: &[u8]) -> Result { let mut reader = Reader::new(data, PaymentsError::InvalidAccountData.into()); let version = reader.read_u8()?; let queue_id = reader.read_u8()?; let index = reader.read_u64()?; let is_paid = match reader.read_u8()? { 0 => false, 1 => true, _ => return Err(PaymentsError::InvalidAccountData.into()), }; let recipient_wallet = reader.read_pubkey()?; let payout_usd_cents = reader.read_u64()?; let debt_before_usd_cents = reader.read_u64()?; require!(reader.cursor == data.len(), PaymentsError::InvalidAccountData); Ok(Self { version, queue_id, index, is_paid, recipient_wallet, payout_usd_cents, debt_before_usd_cents }) } } impl StateCodec for ManagerAllowanceState { fn encoded_len() -> usize { 1 + 32 + 8 + 8 + 8 } fn encode(&self) -> Vec { let mut out = Vec::with_capacity(57); out.push(self.version); out.extend_from_slice(self.manager_wallet.as_ref()); out.extend_from_slice(&self.q1_available_usd_cents.to_le_bytes()); out.extend_from_slice(&self.q2_available_usd_cents.to_le_bytes()); out.extend_from_slice(&self.q3_available_usd_cents.to_le_bytes()); out } fn decode(data: &[u8]) -> Result { let mut reader = Reader::new(data, PaymentsError::InvalidAccountData.into()); let version = reader.read_u8()?; let manager_wallet = reader.read_pubkey()?; let q1_available_usd_cents = reader.read_u64()?; let q2_available_usd_cents = reader.read_u64()?; let q3_available_usd_cents = reader.read_u64()?; require!(reader.cursor == data.len(), PaymentsError::InvalidAccountData); Ok(Self { version, manager_wallet, q1_available_usd_cents, q2_available_usd_cents, q3_available_usd_cents }) } } impl StateCodec for VaultState { fn encoded_len() -> usize { 1 } fn encode(&self) -> Vec { vec![self.version] } fn decode(data: &[u8]) -> Result { require!(data.len() == 1, PaymentsError::InvalidAccountData); Ok(Self { version: data[0] }) } } #[derive(Clone, Debug)] enum Instruction { Init, UpdateCoefLimit(UpdateCoefLimitArgs), GrantManagerLimits(GrantManagerLimitsArgs), BuyTicket(BuyTicketArgs), BuyTicketUsd(BuyTicketUsdArgs), BuyTicketSol(BuyTicketSolArgs), ManagerAddTicket(ManagerAddTicketArgs), StepPayout, ChangeTicketRecipient(ChangeTicketRecipientArgs), } pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { match parse_instruction(instruction_data)? { Instruction::Init => process_init(program_id, accounts), Instruction::UpdateCoefLimit(args) => process_update_coef_limit(program_id, accounts, args), Instruction::GrantManagerLimits(args) => process_grant_manager_limits(program_id, accounts, args), Instruction::BuyTicket(args) => process_buy_ticket(program_id, accounts, args), Instruction::BuyTicketUsd(args) => process_buy_ticket_usd(program_id, accounts, args), Instruction::BuyTicketSol(args) => process_buy_ticket_sol(program_id, accounts, args), Instruction::ManagerAddTicket(args) => process_manager_add_ticket(program_id, accounts, args), Instruction::StepPayout => process_step_payout(program_id, accounts), Instruction::ChangeTicketRecipient(args) => process_change_ticket_recipient(program_id, accounts, args), } } fn parse_instruction(data: &[u8]) -> Result { let (&tag, rest) = data .split_first() .ok_or(ProgramError::InvalidInstructionData)?; let mut reader = Reader::new(rest, ProgramError::InvalidInstructionData); let instruction = match tag { IX_INIT => Instruction::Init, IX_UPDATE_COEF_LIMIT => Instruction::UpdateCoefLimit(UpdateCoefLimitArgs { coef_ppm: reader.read_u64()?, limit_usd_cents: reader.read_u64()?, call_reward_lamports: reader.read_u64()?, }), IX_GRANT_MANAGER_LIMITS => Instruction::GrantManagerLimits(GrantManagerLimitsArgs { manager_wallet: reader.read_pubkey()?, add_q1_usd_cents: reader.read_u64()?, add_q2_usd_cents: reader.read_u64()?, add_q3_usd_cents: reader.read_u64()?, }), IX_BUY_TICKET => Instruction::BuyTicket(BuyTicketArgs { amount_lamports: reader.read_u64()?, recipient_wallet: reader.read_pubkey()?, }), IX_BUY_TICKET_USD => Instruction::BuyTicketUsd(BuyTicketUsdArgs { amount_usd_cents: reader.read_u64()?, max_pay_lamports: reader.read_u64()?, recipient_wallet: reader.read_pubkey()?, }), IX_BUY_TICKET_SOL => Instruction::BuyTicketSol(BuyTicketSolArgs { amount_lamports: reader.read_u64()?, min_expected_usd_cents: reader.read_u64()?, recipient_wallet: reader.read_pubkey()?, }), IX_MANAGER_ADD_TICKET => Instruction::ManagerAddTicket(ManagerAddTicketArgs { queue_id: reader.read_u8()?, recipient_wallet: reader.read_pubkey()?, payout_usd_cents: reader.read_u64()?, }), IX_STEP_PAYOUT => Instruction::StepPayout, IX_CHANGE_TICKET_RECIPIENT => Instruction::ChangeTicketRecipient(ChangeTicketRecipientArgs { new_recipient_wallet: reader.read_pubkey()?, }), _ => return Err(ProgramError::InvalidInstructionData), }; reader.finish()?; Ok(instruction) } fn process_init(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_iter = &mut accounts.iter(); let payer = next_signer_account(account_iter)?; let config_pda = next_account_info(account_iter)?; let coef_limit_pda = next_account_info(account_iter)?; let queues_pda = next_account_info(account_iter)?; let inflow_vault_pda = next_account_info(account_iter)?; let system_program_ai = next_account_info(account_iter)?; require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction); require_system_program(system_program_ai)?; ensure_expected_pdas(program_id, config_pda, coef_limit_pda, queues_pda, inflow_vault_pda)?; require!(is_uninitialized_account(config_pda), PaymentsError::SystemAlreadyInitialized); require!(is_uninitialized_account(coef_limit_pda), PaymentsError::SystemAlreadyInitialized); require!(is_uninitialized_account(queues_pda), PaymentsError::SystemAlreadyInitialized); require!(is_uninitialized_account(inflow_vault_pda), PaymentsError::SystemAlreadyInitialized); let dao_wallet = Pubkey::from_str(settings::DAO_WALLET) .map_err(|_| ProgramError::from(PaymentsError::InvalidSettingsWallet))?; let config = ConfigState { version: 1, dao_wallet, inflow_vault: *inflow_vault_pda.key, }; create_and_store_state( program_id, payer, system_program_ai, config_pda, settings::CONFIG_SEED, settings::CONFIG_SPACE, &config, )?; let coef_limit = CoefLimitState { version: 1, coef_ppm: settings::START_COEF_PPM, limit_usd_cents: settings::START_LIMIT_USD_CENTS, call_reward_lamports: settings::START_CALL_REWARD_LAMPORTS, }; create_and_store_state( program_id, payer, system_program_ai, coef_limit_pda, settings::COEF_LIMIT_SEED, settings::COEF_LIMIT_SPACE, &coef_limit, )?; let queues = QueuesState { version: 1, q1_tickets_total: 0, q1_tickets_paid: 0, q1_sum_total_usd_cents: 0, q1_sum_paid_usd_cents: 0, q2_tickets_total: 0, q2_tickets_paid: 0, q2_sum_total_usd_cents: 0, q2_sum_paid_usd_cents: 0, q3_tickets_total: 0, q3_tickets_paid: 0, q3_sum_total_usd_cents: 0, q3_sum_paid_usd_cents: 0, }; create_and_store_state( program_id, payer, system_program_ai, queues_pda, settings::QUEUES_SEED, settings::QUEUES_SPACE, &queues, )?; let vault = VaultState { version: 1 }; create_and_store_state( program_id, payer, system_program_ai, inflow_vault_pda, settings::INFLOW_VAULT_SEED, settings::INFLOW_VAULT_SPACE, &vault, )?; Ok(()) } fn process_update_coef_limit( _program_id: &Pubkey, accounts: &[AccountInfo], args: UpdateCoefLimitArgs, ) -> ProgramResult { let account_iter = &mut accounts.iter(); let signer = next_signer_account(account_iter)?; let config_pda = next_account_info(account_iter)?; let coef_limit_pda = next_account_info(account_iter)?; require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction); let config = read_state::(config_pda)?; require_keys_eq!(config.dao_wallet, *signer.key, PaymentsError::UnauthorizedDao); require!(args.coef_ppm > 0, PaymentsError::InvalidCoefficient); require!(args.limit_usd_cents > 0, PaymentsError::InvalidLimit); require!( args.call_reward_lamports <= settings::MAX_CALL_REWARD_LAMPORTS, PaymentsError::InvalidCallReward ); let mut coef_limit = read_state::(coef_limit_pda)?; coef_limit.coef_ppm = args.coef_ppm; coef_limit.limit_usd_cents = args.limit_usd_cents; coef_limit.call_reward_lamports = args.call_reward_lamports; write_state(coef_limit_pda, &coef_limit) } fn process_grant_manager_limits( program_id: &Pubkey, accounts: &[AccountInfo], args: GrantManagerLimitsArgs, ) -> ProgramResult { let account_iter = &mut accounts.iter(); let signer = next_signer_account(account_iter)?; let config_pda = next_account_info(account_iter)?; let manager_allowance_pda = next_account_info(account_iter)?; let system_program_ai = next_account_info(account_iter)?; require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction); require_system_program(system_program_ai)?; let config = read_state::(config_pda)?; require_keys_eq!(config.dao_wallet, *signer.key, PaymentsError::UnauthorizedDao); require!( args.add_q1_usd_cents > 0 || args.add_q2_usd_cents > 0 || args.add_q3_usd_cents > 0, PaymentsError::InvalidAmount ); let (expected_pda, bump) = find_manager_allowance_pda(program_id, &args.manager_wallet); require_keys_eq!(expected_pda, *manager_allowance_pda.key, PaymentsError::InvalidPdaAddress); let mut state = if is_uninitialized_account(manager_allowance_pda) { let initial = ManagerAllowanceState { version: 1, manager_wallet: args.manager_wallet, q1_available_usd_cents: 0, q2_available_usd_cents: 0, q3_available_usd_cents: 0, }; create_state_with_seeds( program_id, signer, system_program_ai, manager_allowance_pda, &[ settings::MANAGER_ALLOWANCE_SEED, args.manager_wallet.as_ref(), &[bump], ], settings::MANAGER_ALLOWANCE_SPACE, &initial, )?; initial } else { read_state::(manager_allowance_pda)? }; require_keys_eq!(state.manager_wallet, args.manager_wallet, PaymentsError::InvalidManagerWallet); state.q1_available_usd_cents = checked_add(state.q1_available_usd_cents, args.add_q1_usd_cents)?; state.q2_available_usd_cents = checked_add(state.q2_available_usd_cents, args.add_q2_usd_cents)?; state.q3_available_usd_cents = checked_add(state.q3_available_usd_cents, args.add_q3_usd_cents)?; write_state(manager_allowance_pda, &state) } fn process_buy_ticket( program_id: &Pubkey, accounts: &[AccountInfo], args: BuyTicketArgs, ) -> ProgramResult { let ctx = BuyTicketAccounts::parse(accounts)?; let sol_usd = read_sol_usd_price(ctx.sol_usd_price_update, ctx.sol_usd_price_update.key)?; let purchase_usd_cents = lamports_to_usd_cents_floor(args.amount_lamports, &sol_usd)?; require!(purchase_usd_cents > 0, PaymentsError::InvalidAmount); buy_ticket_by_purchase_usd(program_id, &ctx, purchase_usd_cents, args.amount_lamports, args.recipient_wallet) } fn process_buy_ticket_usd( program_id: &Pubkey, accounts: &[AccountInfo], args: BuyTicketUsdArgs, ) -> ProgramResult { let ctx = BuyTicketAccounts::parse(accounts)?; require!(args.amount_usd_cents > 0, PaymentsError::InvalidAmount); require!(args.max_pay_lamports > 0, PaymentsError::InvalidAmount); let sol_usd = read_sol_usd_price(ctx.sol_usd_price_update, ctx.sol_usd_price_update.key)?; let pay_lamports = usd_cents_to_lamports_ceil(args.amount_usd_cents, &sol_usd)?; require!(pay_lamports > 0, PaymentsError::InvalidAmount); require!(pay_lamports <= args.max_pay_lamports, PaymentsError::SlippageExceeded); buy_ticket_by_purchase_usd(program_id, &ctx, args.amount_usd_cents, pay_lamports, args.recipient_wallet) } fn process_buy_ticket_sol( program_id: &Pubkey, accounts: &[AccountInfo], args: BuyTicketSolArgs, ) -> ProgramResult { let ctx = BuyTicketAccounts::parse(accounts)?; require!(args.amount_lamports > 0, PaymentsError::InvalidAmount); let sol_usd = read_sol_usd_price(ctx.sol_usd_price_update, ctx.sol_usd_price_update.key)?; let purchase_usd_cents = lamports_to_usd_cents_floor(args.amount_lamports, &sol_usd)?; require!(purchase_usd_cents > 0, PaymentsError::InvalidAmount); require!(purchase_usd_cents >= args.min_expected_usd_cents, PaymentsError::SlippageExceeded); buy_ticket_by_purchase_usd(program_id, &ctx, purchase_usd_cents, args.amount_lamports, args.recipient_wallet) } fn process_manager_add_ticket( program_id: &Pubkey, accounts: &[AccountInfo], args: ManagerAddTicketArgs, ) -> ProgramResult { let account_iter = &mut accounts.iter(); let signer = next_signer_account(account_iter)?; let manager_allowance_pda = next_account_info(account_iter)?; let queues_pda = next_account_info(account_iter)?; let ticket_pda = next_account_info(account_iter)?; let system_program_ai = next_account_info(account_iter)?; require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction); require_system_program(system_program_ai)?; require!(args.payout_usd_cents > 0, PaymentsError::InvalidPayoutAmount); require!(is_valid_queue_id(args.queue_id), PaymentsError::InvalidTicketQueue); let (expected_manager_pda, _) = find_manager_allowance_pda(program_id, signer.key); require_keys_eq!(expected_manager_pda, *manager_allowance_pda.key, PaymentsError::InvalidPdaAddress); let mut allowance = read_state::(manager_allowance_pda)?; require_keys_eq!(allowance.manager_wallet, *signer.key, PaymentsError::InvalidManagerWallet); let mut queues = read_state::(queues_pda)?; let debt_before_total = queue_sum_total(&queues, args.queue_id); require!(queue_allowance(&allowance, args.queue_id) >= args.payout_usd_cents, PaymentsError::ManagerLimitExceeded); let ticket_index = checked_add(queue_total(&queues, args.queue_id), 1)?; let (expected_ticket_pda, ticket_bump) = find_ticket_pda(program_id, args.queue_id, ticket_index); require_keys_eq!(expected_ticket_pda, *ticket_pda.key, PaymentsError::InvalidPdaAddress); require!(is_uninitialized_account(ticket_pda), PaymentsError::PdaAlreadyExists); let ticket = TicketState { version: 1, queue_id: args.queue_id, index: ticket_index, is_paid: false, recipient_wallet: args.recipient_wallet, payout_usd_cents: args.payout_usd_cents, debt_before_usd_cents: debt_before_total, }; create_state_with_seeds( program_id, signer, system_program_ai, ticket_pda, &[ queue_seed(args.queue_id), &ticket_index.to_le_bytes(), &[ticket_bump], ], settings::TICKET_SPACE, &ticket, )?; sub_queue_allowance(&mut allowance, args.queue_id, args.payout_usd_cents)?; set_queue_total(&mut queues, args.queue_id, ticket_index); add_queue_sum_total(&mut queues, args.queue_id, args.payout_usd_cents)?; write_state(manager_allowance_pda, &allowance)?; write_state(queues_pda, &queues) } fn process_step_payout(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_iter = &mut accounts.iter(); let signer = next_signer_account(account_iter)?; let config_pda = next_account_info(account_iter)?; let queues_pda = next_account_info(account_iter)?; let coef_limit_pda = next_account_info(account_iter)?; let inflow_vault_pda = next_account_info(account_iter)?; let next_ticket_pda = next_account_info(account_iter)?; let ticket_recipient_wallet = next_account_info(account_iter)?; let dao_wallet = next_account_info(account_iter)?; let sol_usd_price_update = next_account_info(account_iter)?; require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction); let config = read_state::(config_pda)?; let coef_limit = read_state::(coef_limit_pda)?; let mut queues = read_state::(queues_pda)?; let _vault_state = read_state::(inflow_vault_pda)?; require_keys_eq!(*dao_wallet.key, config.dao_wallet, PaymentsError::InvalidDaoWallet); require_keys_eq!(*inflow_vault_pda.key, config.inflow_vault, PaymentsError::InvalidInflowVault); let q1_pending = checked_sub(queues.q1_tickets_total, queues.q1_tickets_paid)?; let q2_pending = checked_sub(queues.q2_tickets_total, queues.q2_tickets_paid)?; let q3_pending = checked_sub(queues.q3_tickets_total, queues.q3_tickets_paid)?; if q1_pending == 0 && q2_pending == 0 && q3_pending == 0 { transfer_all_available_to_dao(inflow_vault_pda, dao_wallet)?; return Ok(()); } let (target_queue, next_index) = if q1_pending > 0 { (1, checked_add(queues.q1_tickets_paid, 1)?) } else if q2_pending > 0 { (2, checked_add(queues.q2_tickets_paid, 1)?) } else { (3, checked_add(queues.q3_tickets_paid, 1)?) }; let (expected_ticket_pda, _) = find_ticket_pda(program_id, target_queue, next_index); require_keys_eq!(expected_ticket_pda, *next_ticket_pda.key, PaymentsError::InvalidPdaAddress); let mut ticket = read_state::(next_ticket_pda)?; require!(ticket.queue_id == target_queue, PaymentsError::InvalidTicketQueue); require!(ticket.index == next_index, PaymentsError::InvalidTicketIndex); require!(!ticket.is_paid, PaymentsError::TicketAlreadyPaid); require_keys_eq!(*ticket_recipient_wallet.key, ticket.recipient_wallet, PaymentsError::InvalidTicketRecipient); let sol_usd = read_sol_usd_price(sol_usd_price_update, sol_usd_price_update.key)?; let ticket_lamports = usd_cents_to_lamports_ceil(ticket.payout_usd_cents, &sol_usd)?; let dao_multiplier = target_queue as u64; let dao_usd_cents = checked_mul(ticket.payout_usd_cents, dao_multiplier)?; let dao_lamports = usd_cents_to_lamports_ceil(dao_usd_cents, &sol_usd)?; let needed = checked_add(checked_add(ticket_lamports, dao_lamports)?, coef_limit.call_reward_lamports)?; require!(available_vault_lamports(inflow_vault_pda)? >= needed, PaymentsError::NotEnoughInflowForStep); transfer_from_vault(inflow_vault_pda, ticket_recipient_wallet, ticket_lamports)?; transfer_from_vault(inflow_vault_pda, dao_wallet, dao_lamports)?; transfer_from_vault(inflow_vault_pda, signer, coef_limit.call_reward_lamports)?; ticket.is_paid = true; write_state(next_ticket_pda, &ticket)?; mark_queue_paid(&mut queues, target_queue, ticket.payout_usd_cents)?; write_state(queues_pda, &queues) } fn process_change_ticket_recipient( program_id: &Pubkey, accounts: &[AccountInfo], args: ChangeTicketRecipientArgs, ) -> ProgramResult { let account_iter = &mut accounts.iter(); let signer = next_signer_account(account_iter)?; let queues_pda = next_account_info(account_iter)?; let ticket_pda = next_account_info(account_iter)?; require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction); let queues = read_state::(queues_pda)?; let mut ticket = read_state::(ticket_pda)?; require!(!ticket.is_paid, PaymentsError::TicketAlreadyPaid); require_keys_eq!(*signer.key, ticket.recipient_wallet, PaymentsError::UnauthorizedTicketOwner); let (expected_ticket_pda, _) = find_ticket_pda(program_id, ticket.queue_id, ticket.index); require_keys_eq!(expected_ticket_pda, *ticket_pda.key, PaymentsError::InvalidPdaAddress); let q1_pending = checked_sub(queues.q1_tickets_total, queues.q1_tickets_paid)?; let q2_pending = checked_sub(queues.q2_tickets_total, queues.q2_tickets_paid)?; let q3_pending = checked_sub(queues.q3_tickets_total, queues.q3_tickets_paid)?; let next = if q1_pending > 0 { Some((1, checked_add(queues.q1_tickets_paid, 1)?)) } else if q2_pending > 0 { Some((2, checked_add(queues.q2_tickets_paid, 1)?)) } else if q3_pending > 0 { Some((3, checked_add(queues.q3_tickets_paid, 1)?)) } else { None }; if let Some((queue_id, next_index)) = next { require!( !(ticket.queue_id == queue_id && ticket.index == next_index), PaymentsError::CannotChangeRecipientForNextPayoutTicket ); } ticket.recipient_wallet = args.new_recipient_wallet; write_state(ticket_pda, &ticket) } struct BuyTicketAccounts<'a, 'info> { signer: &'a AccountInfo<'info>, config_pda: &'a AccountInfo<'info>, coef_limit_pda: &'a AccountInfo<'info>, queues_pda: &'a AccountInfo<'info>, ticket_pda: &'a AccountInfo<'info>, dao_wallet: &'a AccountInfo<'info>, sol_usd_price_update: &'a AccountInfo<'info>, system_program_ai: &'a AccountInfo<'info>, } impl<'a, 'info> BuyTicketAccounts<'a, 'info> { fn parse(accounts: &'a [AccountInfo<'info>]) -> Result { let account_iter = &mut accounts.iter(); let signer = next_signer_account(account_iter)?; let config_pda = next_account_info(account_iter)?; let coef_limit_pda = next_account_info(account_iter)?; let queues_pda = next_account_info(account_iter)?; let ticket_pda = next_account_info(account_iter)?; let dao_wallet = next_account_info(account_iter)?; let sol_usd_price_update = next_account_info(account_iter)?; let system_program_ai = next_account_info(account_iter)?; require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction); require_system_program(system_program_ai)?; Ok(Self { signer, config_pda, coef_limit_pda, queues_pda, ticket_pda, dao_wallet, sol_usd_price_update, system_program_ai, }) } } fn ensure_expected_pdas( program_id: &Pubkey, config_pda: &AccountInfo, coef_limit_pda: &AccountInfo, queues_pda: &AccountInfo, inflow_vault_pda: &AccountInfo, ) -> ProgramResult { let (config, _) = find_single_pda(program_id, settings::CONFIG_SEED); let (coef, _) = find_single_pda(program_id, settings::COEF_LIMIT_SEED); let (queues, _) = find_single_pda(program_id, settings::QUEUES_SEED); let (inflow, _) = find_single_pda(program_id, settings::INFLOW_VAULT_SEED); require_keys_eq!(config, *config_pda.key, PaymentsError::InvalidPdaAddress); require_keys_eq!(coef, *coef_limit_pda.key, PaymentsError::InvalidPdaAddress); require_keys_eq!(queues, *queues_pda.key, PaymentsError::InvalidPdaAddress); require_keys_eq!(inflow, *inflow_vault_pda.key, PaymentsError::InvalidPdaAddress); Ok(()) } fn find_single_pda(program_id: &Pubkey, seed: &[u8]) -> (Pubkey, u8) { Pubkey::find_program_address(&[seed], program_id) } fn find_ticket_pda(program_id: &Pubkey, queue_id: u8, index: u64) -> (Pubkey, u8) { Pubkey::find_program_address(&[queue_seed(queue_id), &index.to_le_bytes()], program_id) } fn find_manager_allowance_pda(program_id: &Pubkey, manager_wallet: &Pubkey) -> (Pubkey, u8) { Pubkey::find_program_address( &[settings::MANAGER_ALLOWANCE_SEED, manager_wallet.as_ref()], program_id, ) } fn queue_seed(queue_id: u8) -> &'static [u8] { match queue_id { 1 => settings::Q1_TICKET_SEED, 2 => settings::Q2_TICKET_SEED, 3 => settings::Q3_TICKET_SEED, _ => settings::Q1_TICKET_SEED, } } fn is_valid_queue_id(queue_id: u8) -> bool { matches!(queue_id, 1 | 2 | 3) } fn buy_ticket_by_purchase_usd( program_id: &Pubkey, ctx: &BuyTicketAccounts<'_, '_>, purchase_usd_cents: u64, transfer_lamports: u64, recipient_wallet: Pubkey, ) -> ProgramResult { let config = read_state::(ctx.config_pda)?; let coef_limit = read_state::(ctx.coef_limit_pda)?; let mut queues = read_state::(ctx.queues_pda)?; require_keys_eq!(*ctx.dao_wallet.key, config.dao_wallet, PaymentsError::InvalidDaoWallet); let queue1_sum_total_before = queues.q1_sum_total_usd_cents; require!(queue1_sum_total_before < coef_limit.limit_usd_cents, PaymentsError::QueueTemporarilyPaused); let ticket_index = checked_add(queues.q1_tickets_total, 1)?; let (expected_ticket_pda, ticket_bump) = find_ticket_pda(program_id, 1, ticket_index); require_keys_eq!(expected_ticket_pda, *ctx.ticket_pda.key, PaymentsError::InvalidPdaAddress); require!(is_uninitialized_account(ctx.ticket_pda), PaymentsError::PdaAlreadyExists); let payout_usd_cents = checked_mul(purchase_usd_cents, coef_limit.coef_ppm)? / settings::COEF_SCALE_PPM; require!(payout_usd_cents > 0, PaymentsError::InvalidPayoutAmount); transfer_from_signer_to_target(ctx.signer, ctx.dao_wallet, ctx.system_program_ai, transfer_lamports)?; let ticket = TicketState { version: 1, queue_id: 1, index: ticket_index, is_paid: false, recipient_wallet, payout_usd_cents, debt_before_usd_cents: queue1_sum_total_before, }; create_state_with_seeds( program_id, ctx.signer, ctx.system_program_ai, ctx.ticket_pda, &[ settings::Q1_TICKET_SEED, &ticket_index.to_le_bytes(), &[ticket_bump], ], settings::TICKET_SPACE, &ticket, )?; queues.q1_tickets_total = ticket_index; queues.q1_sum_total_usd_cents = checked_add(queues.q1_sum_total_usd_cents, payout_usd_cents)?; write_state(ctx.queues_pda, &queues) } fn transfer_from_signer_to_target<'info>( signer: &AccountInfo<'info>, target: &AccountInfo<'info>, system_program_ai: &AccountInfo<'info>, amount: u64, ) -> ProgramResult { let ix = system_instruction::transfer(signer.key, target.key, amount); invoke(&ix, &[signer.clone(), target.clone(), system_program_ai.clone()]) } fn read_sol_usd_price(price_update: &AccountInfo, key: &Pubkey) -> Result { let expected_oracle = Pubkey::from_str(settings::PYTH_SOL_USD_ACCOUNT) .map_err(|_| ProgramError::from(PaymentsError::InvalidOracleFeedConfig))?; require_keys_eq!(expected_oracle, *key, PaymentsError::InvalidOracleAccount); let data = price_update.try_borrow_data()?; let clock = Clock::get()?; parse_pyth_price_update_v2(&data, &clock) } fn parse_pyth_price_update_v2(data: &[u8], clock: &Clock) -> Result { require!(data.len() > 100, PaymentsError::InvalidOraclePrice); let price = read_i64_at(data, 73)?; let exponent = read_i32_at(data, 89)?; let publish_time = read_i64_at(data, 93)?; require!( publish_time.saturating_add(settings::ORACLE_MAX_AGE_SECS as i64) >= clock.unix_timestamp, PaymentsError::OraclePriceTooOld ); require!(price > 0, PaymentsError::InvalidOraclePrice); let mut num = checked_mul_u128(price as u128, settings::USD_CENTS_SCALE as u128)?; let mut den: u128 = 1; if exponent >= 0 { let pow = 10u128 .checked_pow(exponent as u32) .ok_or(ProgramError::from(PaymentsError::MathOverflow))?; num = checked_mul_u128(num, pow)?; } else { let pow = 10u128 .checked_pow((-exponent) as u32) .ok_or(ProgramError::from(PaymentsError::MathOverflow))?; den = checked_mul_u128(den, pow)?; } require!(num > 0 && den > 0, PaymentsError::InvalidOraclePrice); Ok(SolUsdPrice { price_num: num, price_den: den }) } fn read_i32_at(data: &[u8], offset: usize) -> Result { let end = offset .checked_add(4) .ok_or(ProgramError::from(PaymentsError::InvalidOraclePrice))?; let slice = data .get(offset..end) .ok_or(ProgramError::from(PaymentsError::InvalidOraclePrice))?; Ok(i32::from_le_bytes( slice .try_into() .map_err(|_| ProgramError::from(PaymentsError::InvalidOraclePrice))?, )) } fn read_i64_at(data: &[u8], offset: usize) -> Result { let end = offset .checked_add(8) .ok_or(ProgramError::from(PaymentsError::InvalidOraclePrice))?; let slice = data .get(offset..end) .ok_or(ProgramError::from(PaymentsError::InvalidOraclePrice))?; Ok(i64::from_le_bytes( slice .try_into() .map_err(|_| ProgramError::from(PaymentsError::InvalidOraclePrice))?, )) } fn lamports_to_usd_cents_floor(lamports: u64, price: &SolUsdPrice) -> Result { let numerator = checked_mul_u128(lamports as u128, price.price_num)?; let denominator = checked_mul_u128(settings::LAMPORTS_PER_SOL as u128, price.price_den)?; require!(denominator > 0, PaymentsError::InvalidOraclePrice); u64::try_from(numerator / denominator).map_err(|_| PaymentsError::MathOverflow.into()) } fn usd_cents_to_lamports_ceil(usd_cents: u64, price: &SolUsdPrice) -> Result { require!(usd_cents > 0, PaymentsError::InvalidAmount); require!(price.price_num > 0, PaymentsError::InvalidOraclePrice); let numerator = checked_mul_u128( checked_mul_u128(usd_cents as u128, settings::LAMPORTS_PER_SOL as u128)?, price.price_den, )?; let adjusted = numerator .checked_add(price.price_num - 1) .ok_or(ProgramError::from(PaymentsError::MathOverflow))?; u64::try_from(adjusted / price.price_num).map_err(|_| PaymentsError::MathOverflow.into()) } fn create_and_store_state<'info, T: StateCodec>( program_id: &Pubkey, payer: &AccountInfo<'info>, system_program_ai: &AccountInfo<'info>, pda: &AccountInfo<'info>, seed: &[u8], space: usize, state: &T, ) -> ProgramResult { let (_, bump) = find_single_pda(program_id, seed); create_state_with_seeds( program_id, payer, system_program_ai, pda, &[seed, &[bump]], space, state, ) } fn create_state_with_seeds<'info, T: StateCodec>( program_id: &Pubkey, payer: &AccountInfo<'info>, system_program_ai: &AccountInfo<'info>, pda: &AccountInfo<'info>, seeds: &[&[u8]], space: usize, state: &T, ) -> ProgramResult { create_pda_account(pda, payer, system_program_ai, program_id, seeds, space as u64)?; write_state(pda, state) } fn create_pda_account<'info>( pda: &AccountInfo<'info>, payer: &AccountInfo<'info>, system_program_ai: &AccountInfo<'info>, program_id: &Pubkey, seeds: &[&[u8]], space: u64, ) -> ProgramResult { require!(is_uninitialized_account(pda), PaymentsError::PdaAlreadyExists); let lamports = Rent::get()?.minimum_balance(space as usize); let create_ix = system_instruction::create_account(payer.key, pda.key, lamports, space, program_id); invoke_signed( &create_ix, &[payer.clone(), pda.clone(), system_program_ai.clone()], &[seeds], ) } fn write_state(pda: &AccountInfo, state: &T) -> ProgramResult { let bytes = state.encode(); let mut data = pda.try_borrow_mut_data()?; require!(bytes.len() <= data.len(), PaymentsError::InvalidAccountData); data.fill(0); data[..bytes.len()].copy_from_slice(&bytes); Ok(()) } fn read_state(pda: &AccountInfo) -> Result { require!(!is_uninitialized_account(pda), PaymentsError::EmptyState); require_keys_eq!(*pda.owner, id(), PaymentsError::InvalidPdaAddress); let data = pda.try_borrow_data()?; let encoded_len = T::encoded_len(); require!(data.len() >= encoded_len, PaymentsError::InvalidAccountData); T::decode(&data[..encoded_len]) } fn is_uninitialized_account(account: &AccountInfo) -> bool { account.lamports() == 0 && account.data_len() == 0 && (*account.owner == system_program::ID || *account.owner == Pubkey::default()) } fn available_vault_lamports(vault: &AccountInfo) -> Result { let total = vault.lamports(); let rent_min = Rent::get()?.minimum_balance(vault.data_len()); Ok(total.saturating_sub(rent_min)) } fn transfer_from_vault(vault: &AccountInfo, recipient: &AccountInfo, amount: u64) -> ProgramResult { if amount == 0 { return Ok(()); } let mut vault_lamports = vault.try_borrow_mut_lamports()?; let mut recipient_lamports = recipient.try_borrow_mut_lamports()?; require!(**vault_lamports >= amount, PaymentsError::NotEnoughInflowForStep); **vault_lamports = checked_sub(**vault_lamports, amount)?; **recipient_lamports = checked_add(**recipient_lamports, amount)?; Ok(()) } fn transfer_all_available_to_dao(vault: &AccountInfo, dao_wallet: &AccountInfo) -> ProgramResult { let available = available_vault_lamports(vault)?; transfer_from_vault(vault, dao_wallet, available) } fn next_signer_account<'a, 'info>( account_iter: &mut std::slice::Iter<'a, AccountInfo<'info>>, ) -> Result<&'a AccountInfo<'info>, ProgramError> { let signer = next_account_info(account_iter)?; require!(signer.is_signer, PaymentsError::MissingRequiredSignature); Ok(signer) } fn require_system_program(system_program_ai: &AccountInfo) -> ProgramResult { require_keys_eq!(*system_program_ai.key, system_program::ID, PaymentsError::InvalidSystemProgram); Ok(()) } fn queue_total(queues: &QueuesState, queue_id: u8) -> u64 { match queue_id { 1 => queues.q1_tickets_total, 2 => queues.q2_tickets_total, 3 => queues.q3_tickets_total, _ => 0, } } fn queue_sum_total(queues: &QueuesState, queue_id: u8) -> u64 { match queue_id { 1 => queues.q1_sum_total_usd_cents, 2 => queues.q2_sum_total_usd_cents, 3 => queues.q3_sum_total_usd_cents, _ => 0, } } fn queue_allowance(allowance: &ManagerAllowanceState, queue_id: u8) -> u64 { match queue_id { 1 => allowance.q1_available_usd_cents, 2 => allowance.q2_available_usd_cents, 3 => allowance.q3_available_usd_cents, _ => 0, } } fn sub_queue_allowance( allowance: &mut ManagerAllowanceState, queue_id: u8, amount: u64, ) -> ProgramResult { match queue_id { 1 => allowance.q1_available_usd_cents = checked_sub(allowance.q1_available_usd_cents, amount)?, 2 => allowance.q2_available_usd_cents = checked_sub(allowance.q2_available_usd_cents, amount)?, 3 => allowance.q3_available_usd_cents = checked_sub(allowance.q3_available_usd_cents, amount)?, _ => return Err(PaymentsError::InvalidTicketQueue.into()), } Ok(()) } fn set_queue_total(queues: &mut QueuesState, queue_id: u8, value: u64) { match queue_id { 1 => queues.q1_tickets_total = value, 2 => queues.q2_tickets_total = value, 3 => queues.q3_tickets_total = value, _ => {} } } fn add_queue_sum_total(queues: &mut QueuesState, queue_id: u8, value: u64) -> ProgramResult { match queue_id { 1 => queues.q1_sum_total_usd_cents = checked_add(queues.q1_sum_total_usd_cents, value)?, 2 => queues.q2_sum_total_usd_cents = checked_add(queues.q2_sum_total_usd_cents, value)?, 3 => queues.q3_sum_total_usd_cents = checked_add(queues.q3_sum_total_usd_cents, value)?, _ => return Err(PaymentsError::InvalidTicketQueue.into()), } Ok(()) } fn mark_queue_paid(queues: &mut QueuesState, queue_id: u8, payout_usd_cents: u64) -> ProgramResult { match queue_id { 1 => { queues.q1_tickets_paid = checked_add(queues.q1_tickets_paid, 1)?; queues.q1_sum_paid_usd_cents = checked_add(queues.q1_sum_paid_usd_cents, payout_usd_cents)?; } 2 => { queues.q2_tickets_paid = checked_add(queues.q2_tickets_paid, 1)?; queues.q2_sum_paid_usd_cents = checked_add(queues.q2_sum_paid_usd_cents, payout_usd_cents)?; } 3 => { queues.q3_tickets_paid = checked_add(queues.q3_tickets_paid, 1)?; queues.q3_sum_paid_usd_cents = checked_add(queues.q3_sum_paid_usd_cents, payout_usd_cents)?; } _ => return Err(PaymentsError::InvalidTicketQueue.into()), } Ok(()) } fn checked_add(left: u64, right: u64) -> Result { left.checked_add(right).ok_or(PaymentsError::MathOverflow.into()) } fn checked_sub(left: u64, right: u64) -> Result { left.checked_sub(right).ok_or(PaymentsError::MathOverflow.into()) } fn checked_mul(left: u64, right: u64) -> Result { left.checked_mul(right).ok_or(PaymentsError::MathOverflow.into()) } fn checked_mul_u128(left: u128, right: u128) -> Result { left.checked_mul(right).ok_or(PaymentsError::MathOverflow.into()) }