use anchor_lang::prelude::*; use anchor_lang::solana_program::{program::invoke, system_instruction}; use common::utils::{create_pda, safe_read_pda, write_to_pda, ErrCode}; use pyth_solana_receiver_sdk::error::GetPriceError; use pyth_solana_receiver_sdk::price_update::{get_feed_id_from_hex, Price, PriceUpdateV2}; use std::str::FromStr; pub mod settings; declare_id!("m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR"); #[program] pub mod shine_payments { use super::*; pub fn init(ctx: Context) -> Result<()> { ensure_expected_pdas(ctx.program_id, &ctx.accounts)?; require!( ctx.accounts.config_pda.owner == &Pubkey::default(), ErrCode::SystemAlreadyInitialized ); require!( ctx.accounts.coef_limit_pda.owner == &Pubkey::default(), ErrCode::SystemAlreadyInitialized ); require!( ctx.accounts.queues_pda.owner == &Pubkey::default(), ErrCode::SystemAlreadyInitialized ); require!( ctx.accounts.inflow_vault_pda.owner == &Pubkey::default(), ErrCode::SystemAlreadyInitialized ); let dao_wallet = Pubkey::from_str(settings::DAO_WALLET) .map_err(|_| error!(PaymentsError::InvalidSettingsWallet))?; let system_program_ai = ctx.accounts.system_program.to_account_info(); let config = ConfigState { version: 1, dao_wallet, inflow_vault: ctx.accounts.inflow_vault_pda.key(), }; create_and_store_state( ctx.program_id, &ctx.accounts.payer, &system_program_ai, &ctx.accounts.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( ctx.program_id, &ctx.accounts.payer, &system_program_ai, &ctx.accounts.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, }; create_and_store_state( ctx.program_id, &ctx.accounts.payer, &system_program_ai, &ctx.accounts.queues_pda, settings::QUEUES_SEED, settings::QUEUES_SPACE, &queues, )?; let vault = VaultState { version: 1 }; create_and_store_state( ctx.program_id, &ctx.accounts.payer, &system_program_ai, &ctx.accounts.inflow_vault_pda, settings::INFLOW_VAULT_SEED, settings::INFLOW_VAULT_SPACE, &vault, )?; Ok(()) } pub fn update_coef_limit( ctx: Context, args: UpdateCoefLimitArgs, ) -> Result<()> { let config = read_state::(&ctx.accounts.config_pda)?; require_keys_eq!( config.dao_wallet, ctx.accounts.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::(&ctx.accounts.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(&ctx.accounts.coef_limit_pda, &coef_limit)?; Ok(()) } pub fn grant_manager_limits( ctx: Context, args: GrantManagerLimitsArgs, ) -> Result<()> { let config = read_state::(&ctx.accounts.config_pda)?; require_keys_eq!( config.dao_wallet, ctx.accounts.signer.key(), PaymentsError::UnauthorizedDao ); require!( args.add_q1_usd_cents > 0 || args.add_q2_usd_cents > 0, PaymentsError::InvalidAmount ); let (expected_pda, bump) = find_manager_allowance_pda(ctx.program_id, &args.manager_wallet); require_keys_eq!( expected_pda, ctx.accounts.manager_allowance_pda.key(), ErrCode::InvalidPdaAddress ); let system_program_ai = ctx.accounts.system_program.to_account_info(); let mut state = if ctx.accounts.manager_allowance_pda.owner == &Pubkey::default() { let initial = ManagerAllowanceState { version: 1, manager_wallet: args.manager_wallet, q1_available_usd_cents: 0, q2_available_usd_cents: 0, }; create_state_with_seeds( ctx.program_id, &ctx.accounts.signer, &system_program_ai, &ctx.accounts.manager_allowance_pda, &[ settings::MANAGER_ALLOWANCE_SEED, args.manager_wallet.as_ref(), &[bump], ], settings::MANAGER_ALLOWANCE_SPACE, &initial, )?; initial } else { read_state::(&ctx.accounts.manager_allowance_pda)? }; require_keys_eq!( state.manager_wallet, args.manager_wallet, PaymentsError::InvalidManagerWallet ); state.q1_available_usd_cents = state .q1_available_usd_cents .checked_add(args.add_q1_usd_cents) .ok_or(error!(ErrCode::MathOverflow))?; state.q2_available_usd_cents = state .q2_available_usd_cents .checked_add(args.add_q2_usd_cents) .ok_or(error!(ErrCode::MathOverflow))?; write_state(&ctx.accounts.manager_allowance_pda, &state)?; Ok(()) } pub fn buy_ticket(ctx: Context, args: BuyTicketArgs) -> Result<()> { let sol_usd = read_sol_usd_price( &ctx.accounts.sol_usd_price_update, &ctx.accounts.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( &ctx, purchase_usd_cents, args.amount_lamports, args.recipient_wallet, ) } pub fn buy_ticket_usd(ctx: Context, args: BuyTicketUsdArgs) -> Result<()> { require!(args.amount_usd_cents > 0, PaymentsError::InvalidAmount); require!(args.max_pay_lamports > 0, PaymentsError::InvalidAmount); let sol_usd = read_sol_usd_price( &ctx.accounts.sol_usd_price_update, &ctx.accounts.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(&ctx, args.amount_usd_cents, pay_lamports, args.recipient_wallet) } pub fn buy_ticket_sol(ctx: Context, args: BuyTicketSolArgs) -> Result<()> { require!(args.amount_lamports > 0, PaymentsError::InvalidAmount); let sol_usd = read_sol_usd_price( &ctx.accounts.sol_usd_price_update, &ctx.accounts.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( &ctx, purchase_usd_cents, args.amount_lamports, args.recipient_wallet, ) } pub fn manager_add_ticket( ctx: Context, args: ManagerAddTicketArgs, ) -> Result<()> { require!(args.payout_usd_cents > 0, PaymentsError::InvalidPayoutAmount); require!( args.queue_id == 1 || args.queue_id == 2, PaymentsError::InvalidTicketQueue ); let (expected_manager_pda, _) = find_manager_allowance_pda(ctx.program_id, ctx.accounts.signer.key); require_keys_eq!( expected_manager_pda, ctx.accounts.manager_allowance_pda.key(), ErrCode::InvalidPdaAddress ); let mut allowance = read_state::(&ctx.accounts.manager_allowance_pda)?; require_keys_eq!( allowance.manager_wallet, ctx.accounts.signer.key(), PaymentsError::InvalidManagerWallet ); let mut queues = read_state::(&ctx.accounts.queues_pda)?; let debt_before_total = if args.queue_id == 1 { queues.q1_sum_total_usd_cents } else { queues.q2_sum_total_usd_cents }; if args.queue_id == 1 { require!( allowance.q1_available_usd_cents >= args.payout_usd_cents, PaymentsError::ManagerLimitExceeded ); } else { require!( allowance.q2_available_usd_cents >= args.payout_usd_cents, PaymentsError::ManagerLimitExceeded ); } let ticket_index = if args.queue_id == 1 { queues .q1_tickets_total .checked_add(1) .ok_or(error!(ErrCode::MathOverflow))? } else { queues .q2_tickets_total .checked_add(1) .ok_or(error!(ErrCode::MathOverflow))? }; let (expected_ticket_pda, ticket_bump) = find_ticket_pda(ctx.program_id, args.queue_id, ticket_index); require_keys_eq!( expected_ticket_pda, ctx.accounts.ticket_pda.key(), ErrCode::InvalidPdaAddress ); require!( ctx.accounts.ticket_pda.owner == &Pubkey::default(), ErrCode::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, }; let seed_prefix = if args.queue_id == 1 { settings::Q1_TICKET_SEED } else { settings::Q2_TICKET_SEED }; create_state_with_seeds( ctx.program_id, &ctx.accounts.signer, &ctx.accounts.system_program.to_account_info(), &ctx.accounts.ticket_pda, &[seed_prefix, &ticket_index.to_le_bytes(), &[ticket_bump]], settings::TICKET_SPACE, &ticket, )?; if args.queue_id == 1 { allowance.q1_available_usd_cents = allowance .q1_available_usd_cents .checked_sub(args.payout_usd_cents) .ok_or(error!(ErrCode::MathOverflow))?; queues.q1_tickets_total = ticket_index; queues.q1_sum_total_usd_cents = queues .q1_sum_total_usd_cents .checked_add(args.payout_usd_cents) .ok_or(error!(ErrCode::MathOverflow))?; } else { allowance.q2_available_usd_cents = allowance .q2_available_usd_cents .checked_sub(args.payout_usd_cents) .ok_or(error!(ErrCode::MathOverflow))?; queues.q2_tickets_total = ticket_index; queues.q2_sum_total_usd_cents = queues .q2_sum_total_usd_cents .checked_add(args.payout_usd_cents) .ok_or(error!(ErrCode::MathOverflow))?; } write_state(&ctx.accounts.manager_allowance_pda, &allowance)?; write_state(&ctx.accounts.queues_pda, &queues)?; Ok(()) } pub fn step_payout(ctx: Context) -> Result<()> { let config = read_state::(&ctx.accounts.config_pda)?; let coef_limit = read_state::(&ctx.accounts.coef_limit_pda)?; let mut queues = read_state::(&ctx.accounts.queues_pda)?; let _vault_state = read_state::(&ctx.accounts.inflow_vault_pda)?; require_keys_eq!( ctx.accounts.dao_wallet.key(), config.dao_wallet, PaymentsError::InvalidDaoWallet ); require_keys_eq!( ctx.accounts.inflow_vault_pda.key(), config.inflow_vault, PaymentsError::InvalidInflowVault ); let q1_pending = queues .q1_tickets_total .checked_sub(queues.q1_tickets_paid) .ok_or(error!(ErrCode::MathOverflow))?; let q2_pending = queues .q2_tickets_total .checked_sub(queues.q2_tickets_paid) .ok_or(error!(ErrCode::MathOverflow))?; if q1_pending == 0 && q2_pending == 0 { transfer_all_available_to_dao( &ctx.accounts.inflow_vault_pda, &ctx.accounts.dao_wallet, )?; return Ok(()); } let target_queue = if q1_pending > 0 { 1 } else { 2 }; let next_index = if target_queue == 1 { queues .q1_tickets_paid .checked_add(1) .ok_or(error!(ErrCode::MathOverflow))? } else { queues .q2_tickets_paid .checked_add(1) .ok_or(error!(ErrCode::MathOverflow))? }; let (expected_ticket_pda, _) = find_ticket_pda(ctx.program_id, target_queue, next_index); require_keys_eq!( expected_ticket_pda, ctx.accounts.next_ticket_pda.key(), ErrCode::InvalidPdaAddress ); let mut ticket = read_state::(&ctx.accounts.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!( ctx.accounts.ticket_recipient_wallet.key(), ticket.recipient_wallet, PaymentsError::InvalidTicketRecipient ); let sol_usd = read_sol_usd_price( &ctx.accounts.sol_usd_price_update, &ctx.accounts.sol_usd_price_update.key(), )?; let ticket_lamports = usd_cents_to_lamports_ceil(ticket.payout_usd_cents, &sol_usd)?; let dao_multiplier: u64 = if target_queue == 1 { 1 } else { 2 }; let dao_usd_cents = ticket .payout_usd_cents .checked_mul(dao_multiplier) .ok_or(error!(ErrCode::MathOverflow))?; let dao_lamports = usd_cents_to_lamports_ceil(dao_usd_cents, &sol_usd)?; let needed = ticket_lamports .checked_add(dao_lamports) .and_then(|v| v.checked_add(coef_limit.call_reward_lamports)) .ok_or(error!(ErrCode::MathOverflow))?; require!( available_vault_lamports(&ctx.accounts.inflow_vault_pda)? >= needed, PaymentsError::NotEnoughInflowForStep ); transfer_from_vault( &ctx.accounts.inflow_vault_pda, &ctx.accounts.ticket_recipient_wallet, ticket_lamports, )?; transfer_from_vault( &ctx.accounts.inflow_vault_pda, &ctx.accounts.dao_wallet, dao_lamports, )?; transfer_from_vault( &ctx.accounts.inflow_vault_pda, &ctx.accounts.signer, coef_limit.call_reward_lamports, )?; ticket.is_paid = true; write_state(&ctx.accounts.next_ticket_pda, &ticket)?; if target_queue == 1 { queues.q1_tickets_paid = queues .q1_tickets_paid .checked_add(1) .ok_or(error!(ErrCode::MathOverflow))?; queues.q1_sum_paid_usd_cents = queues .q1_sum_paid_usd_cents .checked_add(ticket.payout_usd_cents) .ok_or(error!(ErrCode::MathOverflow))?; } else { queues.q2_tickets_paid = queues .q2_tickets_paid .checked_add(1) .ok_or(error!(ErrCode::MathOverflow))?; queues.q2_sum_paid_usd_cents = queues .q2_sum_paid_usd_cents .checked_add(ticket.payout_usd_cents) .ok_or(error!(ErrCode::MathOverflow))?; } write_state(&ctx.accounts.queues_pda, &queues)?; Ok(()) } pub fn change_ticket_recipient( ctx: Context, args: ChangeTicketRecipientArgs, ) -> Result<()> { let queues = read_state::(&ctx.accounts.queues_pda)?; let mut ticket = read_state::(&ctx.accounts.ticket_pda)?; require!(!ticket.is_paid, PaymentsError::TicketAlreadyPaid); require_keys_eq!( ctx.accounts.signer.key(), ticket.recipient_wallet, PaymentsError::UnauthorizedTicketOwner ); let (expected_ticket_pda, _) = find_ticket_pda(ctx.program_id, ticket.queue_id, ticket.index); require_keys_eq!( expected_ticket_pda, ctx.accounts.ticket_pda.key(), ErrCode::InvalidPdaAddress ); let q1_pending = queues .q1_tickets_total .checked_sub(queues.q1_tickets_paid) .ok_or(error!(ErrCode::MathOverflow))?; let q2_pending = queues .q2_tickets_total .checked_sub(queues.q2_tickets_paid) .ok_or(error!(ErrCode::MathOverflow))?; if q1_pending > 0 || q2_pending > 0 { let target_queue = if q1_pending > 0 { 1 } else { 2 }; let next_index = if target_queue == 1 { queues .q1_tickets_paid .checked_add(1) .ok_or(error!(ErrCode::MathOverflow))? } else { queues .q2_tickets_paid .checked_add(1) .ok_or(error!(ErrCode::MathOverflow))? }; require!( !(ticket.queue_id == target_queue && ticket.index == next_index), PaymentsError::CannotChangeRecipientForNextPayoutTicket ); } ticket.recipient_wallet = args.new_recipient_wallet; write_state(&ctx.accounts.ticket_pda, &ticket)?; Ok(()) } } #[derive(Accounts)] pub struct Init<'info> { /// CHECK: подписант и плательщик, проверяется атрибутом `signer`. #[account(mut, signer)] pub payer: AccountInfo<'info>, /// CHECK: PDA конфига, адрес проверяется вручную в `ensure_expected_pdas`. #[account(mut)] pub config_pda: AccountInfo<'info>, /// CHECK: PDA коэффициента/лимита, адрес проверяется вручную в `ensure_expected_pdas`. #[account(mut)] pub coef_limit_pda: AccountInfo<'info>, /// CHECK: PDA состояния очередей, адрес проверяется вручную в `ensure_expected_pdas`. #[account(mut)] pub queues_pda: AccountInfo<'info>, /// CHECK: PDA inflow-вольта, адрес проверяется вручную в `ensure_expected_pdas`. #[account(mut)] pub inflow_vault_pda: AccountInfo<'info>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct UpdateCoefLimit<'info> { /// CHECK: подписант-DAO, проверяется атрибутом `signer` и сверкой адреса в коде. #[account(mut, signer)] pub signer: AccountInfo<'info>, /// CHECK: PDA конфига, читается и валидируется вручную. #[account(mut)] pub config_pda: AccountInfo<'info>, /// CHECK: PDA коэффициента/лимита, читается и валидируется вручную. #[account(mut)] pub coef_limit_pda: AccountInfo<'info>, } #[derive(Accounts)] pub struct GrantManagerLimits<'info> { /// CHECK: подписант DAO, проверяется атрибутом `signer` и сверкой с config.dao_wallet. #[account(mut, signer)] pub signer: AccountInfo<'info>, /// CHECK: PDA конфига, читается и валидируется вручную. #[account(mut)] pub config_pda: AccountInfo<'info>, /// CHECK: PDA лимитов менеджера, адрес проверяется вручную по manager pubkey. #[account(mut)] pub manager_allowance_pda: AccountInfo<'info>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct BuyTicket<'info> { /// CHECK: подписант-покупатель, проверяется атрибутом `signer`. #[account(mut, signer)] pub signer: AccountInfo<'info>, /// CHECK: PDA конфига, читается и валидируется вручную. #[account(mut)] pub config_pda: AccountInfo<'info>, /// CHECK: PDA коэффициента/лимита, читается и валидируется вручную. #[account(mut)] pub coef_limit_pda: AccountInfo<'info>, /// CHECK: PDA очередей, читается и валидируется вручную. #[account(mut)] pub queues_pda: AccountInfo<'info>, /// CHECK: PDA тикета, адрес и состояние (должен быть пустым) проверяются вручную. #[account(mut)] pub ticket_pda: AccountInfo<'info>, /// CHECK: DAO-кошелек, адрес сверяется с конфигом вручную. #[account(mut)] pub dao_wallet: AccountInfo<'info>, pub sol_usd_price_update: Account<'info, PriceUpdateV2>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct ManagerAddTicket<'info> { /// CHECK: подписант-менеджер, проверяется атрибутом `signer`. #[account(mut, signer)] pub signer: AccountInfo<'info>, /// CHECK: PDA лимитов менеджера, адрес сверяется вручную по signer. #[account(mut)] pub manager_allowance_pda: AccountInfo<'info>, /// CHECK: PDA очередей, читается и валидируется вручную. #[account(mut)] pub queues_pda: AccountInfo<'info>, /// CHECK: PDA тикета, адрес и состояние (должен быть пустым) проверяются вручную. #[account(mut)] pub ticket_pda: AccountInfo<'info>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct StepPayout<'info> { /// CHECK: подписант-вызвавший шаг выплат, проверяется атрибутом `signer`. #[account(mut, signer)] pub signer: AccountInfo<'info>, /// CHECK: PDA конфига, читается и валидируется вручную. #[account(mut)] pub config_pda: AccountInfo<'info>, /// CHECK: PDA очередей, читается и валидируется вручную. #[account(mut)] pub queues_pda: AccountInfo<'info>, /// CHECK: PDA коэффициента/лимита/награды, читается и валидируется вручную. #[account(mut)] pub coef_limit_pda: AccountInfo<'info>, /// CHECK: PDA inflow-вольта, адрес сверяется с конфигом вручную. #[account(mut)] pub inflow_vault_pda: AccountInfo<'info>, /// CHECK: PDA следующего тикета, адрес и содержимое валидируются вручную. #[account(mut)] pub next_ticket_pda: AccountInfo<'info>, /// CHECK: кошелек получателя тикета, адрес сверяется с полем тикета вручную. #[account(mut)] pub ticket_recipient_wallet: AccountInfo<'info>, /// CHECK: DAO-кошелек, адрес сверяется с конфигом вручную. #[account(mut)] pub dao_wallet: AccountInfo<'info>, pub sol_usd_price_update: Account<'info, PriceUpdateV2>, } #[derive(Accounts)] pub struct ChangeTicketRecipient<'info> { /// CHECK: подписант-владелец текущего recipient тикета. #[account(mut, signer)] pub signer: AccountInfo<'info>, /// CHECK: PDA очередей, читается вручную. #[account(mut)] pub queues_pda: AccountInfo<'info>, /// CHECK: PDA тикета, читается и валидируется вручную. #[account(mut)] pub ticket_pda: AccountInfo<'info>, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct UpdateCoefLimitArgs { pub coef_ppm: u64, pub limit_usd_cents: u64, pub call_reward_lamports: u64, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct GrantManagerLimitsArgs { pub manager_wallet: Pubkey, pub add_q1_usd_cents: u64, pub add_q2_usd_cents: u64, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct BuyTicketArgs { pub amount_lamports: u64, pub recipient_wallet: Pubkey, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct BuyTicketUsdArgs { pub amount_usd_cents: u64, pub max_pay_lamports: u64, pub recipient_wallet: Pubkey, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct BuyTicketSolArgs { pub amount_lamports: u64, pub min_expected_usd_cents: u64, pub recipient_wallet: Pubkey, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct ManagerAddTicketArgs { pub queue_id: u8, pub recipient_wallet: Pubkey, pub payout_usd_cents: u64, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct ChangeTicketRecipientArgs { pub new_recipient_wallet: Pubkey, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct ConfigState { pub version: u8, pub dao_wallet: Pubkey, pub inflow_vault: Pubkey, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct CoefLimitState { pub version: u8, pub coef_ppm: u64, pub limit_usd_cents: u64, pub call_reward_lamports: u64, } #[derive(AnchorSerialize, AnchorDeserialize, 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, } #[derive(AnchorSerialize, AnchorDeserialize, 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(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct ManagerAllowanceState { pub version: u8, pub manager_wallet: Pubkey, pub q1_available_usd_cents: u64, pub q2_available_usd_cents: u64, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct VaultState { pub version: u8, } struct SolUsdPrice { price_num: u128, price_den: u128, } #[error_code] pub enum PaymentsError { #[msg("Ошибка в адресах кошельков из настроек программы")] InvalidSettingsWallet, #[msg("Недостаточно данных PDA")] EmptyState, #[msg("Неверный inflow vault")] InvalidInflowVault, #[msg("Неверный DAO кошелек")] InvalidDaoWallet, #[msg("Управляющий кошелек не авторизован")] UnauthorizedManager, #[msg("DAO кошелек не авторизован для этой операции")] UnauthorizedDao, #[msg("Некорректный коэффициент")] InvalidCoefficient, #[msg("Некорректный лимит")] InvalidLimit, #[msg("Некорректная награда за шаг выплаты")] InvalidCallReward, #[msg("Некорректная сумма")] InvalidAmount, #[msg("Очередь временно приостановлена: достигнут лимит")] QueueTemporarilyPaused, #[msg("Некорректная сумма выплаты")] InvalidPayoutAmount, #[msg("Недостаточно средств на inflow vault для шага выплаты")] NotEnoughInflowForStep, #[msg("Тикет уже выплачен")] TicketAlreadyPaid, #[msg("Неверный получатель тикета")] InvalidTicketRecipient, #[msg("Неверный номер тикета")] InvalidTicketIndex, #[msg("Неверный тип очереди у тикета")] InvalidTicketQueue, #[msg("Неверный кошелек менеджера")] InvalidManagerWallet, #[msg("Лимит менеджера по выбранной очереди превышен")] ManagerLimitExceeded, #[msg("Только текущий получатель тикета может изменить получателя")] UnauthorizedTicketOwner, #[msg("Нельзя менять получателя у следующего тикета на выплату")] CannotChangeRecipientForNextPayoutTicket, #[msg("Оракул передан неверный")] InvalidOracleAccount, #[msg("Некорректный feed id оракула")] InvalidOracleFeed, #[msg("Конфигурация оракула в settings некорректна")] InvalidOracleFeedConfig, #[msg("Цена оракула устарела")] OraclePriceTooOld, #[msg("Цена оракула некорректна")] InvalidOraclePrice, #[msg("Защита от проскальзывания: лимит пользователя не проходит")] SlippageExceeded, } fn ensure_expected_pdas(program_id: &Pubkey, accounts: &Init) -> Result<()> { 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, accounts.config_pda.key(), ErrCode::InvalidPdaAddress ); require_keys_eq!( coef, accounts.coef_limit_pda.key(), ErrCode::InvalidPdaAddress ); require_keys_eq!( queues, accounts.queues_pda.key(), ErrCode::InvalidPdaAddress ); require_keys_eq!( inflow, accounts.inflow_vault_pda.key(), ErrCode::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) { let idx = index.to_le_bytes(); let seed = if queue_id == 1 { settings::Q1_TICKET_SEED } else { settings::Q2_TICKET_SEED }; Pubkey::find_program_address(&[seed, &idx], 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 buy_ticket_by_purchase_usd( ctx: &Context, purchase_usd_cents: u64, transfer_lamports: u64, recipient_wallet: Pubkey, ) -> Result<()> { let config = read_state::(&ctx.accounts.config_pda)?; let coef_limit = read_state::(&ctx.accounts.coef_limit_pda)?; let mut queues = read_state::(&ctx.accounts.queues_pda)?; require_keys_eq!( ctx.accounts.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 = queues .q1_tickets_total .checked_add(1) .ok_or(error!(ErrCode::MathOverflow))?; let (expected_ticket_pda, ticket_bump) = find_ticket_pda(ctx.program_id, 1, ticket_index); require_keys_eq!( expected_ticket_pda, ctx.accounts.ticket_pda.key(), ErrCode::InvalidPdaAddress ); require!( ctx.accounts.ticket_pda.owner == &Pubkey::default(), ErrCode::PdaAlreadyExists ); let payout_usd_cents = purchase_usd_cents .checked_mul(coef_limit.coef_ppm) .ok_or(error!(ErrCode::MathOverflow))? / settings::COEF_SCALE_PPM; require!(payout_usd_cents > 0, PaymentsError::InvalidPayoutAmount); transfer_from_signer_to_target( &ctx.accounts.signer, &ctx.accounts.dao_wallet, &ctx.accounts.system_program.to_account_info(), 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( ctx.program_id, &ctx.accounts.signer, &ctx.accounts.system_program.to_account_info(), &ctx.accounts.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 = queues .q1_sum_total_usd_cents .checked_add(payout_usd_cents) .ok_or(error!(ErrCode::MathOverflow))?; write_state(&ctx.accounts.queues_pda, &queues)?; Ok(()) } fn transfer_from_signer_to_target<'info>( signer: &AccountInfo<'info>, target: &AccountInfo<'info>, system_program: &AccountInfo<'info>, amount: u64, ) -> Result<()> { let ix = system_instruction::transfer(signer.key, target.key, amount); invoke( &ix, &[signer.clone(), target.clone(), system_program.clone()], )?; Ok(()) } fn read_sol_usd_price(price_update: &Account, key: &Pubkey) -> Result { let expected_oracle = Pubkey::from_str(settings::PYTH_SOL_USD_ACCOUNT) .map_err(|_| error!(PaymentsError::InvalidOracleFeedConfig))?; require_keys_eq!(expected_oracle, *key, PaymentsError::InvalidOracleAccount); let feed_id = get_feed_id_from_hex(settings::PYTH_SOL_USD_FEED_ID) .map_err(|_| error!(PaymentsError::InvalidOracleFeedConfig))?; let clock = Clock::get()?; let price = price_update .get_price_no_older_than(&clock, settings::ORACLE_MAX_AGE_SECS, &feed_id) .map_err(map_oracle_error)?; price_to_ratio(price) } fn map_oracle_error(err: GetPriceError) -> anchor_lang::error::Error { match err { GetPriceError::PriceTooOld => error!(PaymentsError::OraclePriceTooOld), GetPriceError::MismatchedFeedId => error!(PaymentsError::InvalidOracleFeed), GetPriceError::InsufficientVerificationLevel => error!(PaymentsError::InvalidOraclePrice), GetPriceError::FeedIdMustBe32Bytes | GetPriceError::FeedIdNonHexCharacter => { error!(PaymentsError::InvalidOracleFeedConfig) } _ => error!(PaymentsError::InvalidOraclePrice), } } fn price_to_ratio(price: Price) -> Result { require!(price.price > 0, PaymentsError::InvalidOraclePrice); let mut num = (price.price as u128) .checked_mul(settings::USD_CENTS_SCALE as u128) .ok_or(error!(ErrCode::MathOverflow))?; let mut den: u128 = 1; if price.exponent >= 0 { let pow = 10u128 .checked_pow(price.exponent as u32) .ok_or(error!(ErrCode::MathOverflow))?; num = num.checked_mul(pow).ok_or(error!(ErrCode::MathOverflow))?; } else { let pow = 10u128 .checked_pow((-price.exponent) as u32) .ok_or(error!(ErrCode::MathOverflow))?; den = den.checked_mul(pow).ok_or(error!(ErrCode::MathOverflow))?; } require!(num > 0 && den > 0, PaymentsError::InvalidOraclePrice); Ok(SolUsdPrice { price_num: num, price_den: den, }) } fn lamports_to_usd_cents_floor(lamports: u64, price: &SolUsdPrice) -> Result { let numerator = (lamports as u128) .checked_mul(price.price_num) .ok_or(error!(ErrCode::MathOverflow))?; let denominator = (settings::LAMPORTS_PER_SOL as u128) .checked_mul(price.price_den) .ok_or(error!(ErrCode::MathOverflow))?; require!(denominator > 0, PaymentsError::InvalidOraclePrice); let value = numerator / denominator; u64::try_from(value).map_err(|_| error!(ErrCode::MathOverflow)) } 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 = (usd_cents as u128) .checked_mul(settings::LAMPORTS_PER_SOL as u128) .and_then(|v| v.checked_mul(price.price_den)) .ok_or(error!(ErrCode::MathOverflow))?; let adjusted = numerator .checked_add(price.price_num - 1) .ok_or(error!(ErrCode::MathOverflow))?; let value = adjusted / price.price_num; u64::try_from(value).map_err(|_| error!(ErrCode::MathOverflow)) } fn create_and_store_state<'info, T: AnchorSerialize>( program_id: &Pubkey, payer: &AccountInfo<'info>, system_program: &AccountInfo<'info>, pda: &AccountInfo<'info>, seed: &[u8], space: usize, state: &T, ) -> Result<()> { let (_, bump) = find_single_pda(program_id, seed); create_state_with_seeds( program_id, payer, system_program, pda, &[seed, &[bump]], space, state, ) } fn create_state_with_seeds<'info, T: AnchorSerialize>( program_id: &Pubkey, payer: &AccountInfo<'info>, system_program: &AccountInfo<'info>, pda: &AccountInfo<'info>, seeds: &[&[u8]], space: usize, state: &T, ) -> Result<()> { create_pda(pda, payer, system_program, program_id, seeds, space as u64)?; write_state(pda, state) } fn write_state(pda: &AccountInfo, state: &T) -> Result<()> { let bytes = state .try_to_vec() .map_err(|_| error!(ErrCode::DeserializationError))?; write_to_pda(pda, &bytes) } fn read_state(pda: &AccountInfo) -> Result { let raw = safe_read_pda(pda); require!(!raw.is_empty(), PaymentsError::EmptyState); let mut slice: &[u8] = &raw; T::deserialize(&mut slice).map_err(|_| error!(ErrCode::DeserializationError)) } 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) -> Result<()> { 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 = vault_lamports .checked_sub(amount) .ok_or(error!(ErrCode::MathOverflow))?; **recipient_lamports = recipient_lamports .checked_add(amount) .ok_or(error!(ErrCode::MathOverflow))?; Ok(()) } fn transfer_all_available_to_dao(vault: &AccountInfo, dao_wallet: &AccountInfo) -> Result<()> { let available = available_vault_lamports(vault)?; transfer_from_vault(vault, dao_wallet, available) }