SHiNE-server/shine-solana/shine/programs/shine_payments/src/lib.rs
AidarKC cf6a2830c8 solana: закрыть griefing создания PDA и заморозку выплат, добавить аудит №2
shine_payments + shine_users:
- create_pda_account переведён на «создание поверх предзаполненного»
  (allocate+assign+добор ренты), чтобы подсев лампортов на детерминированный
  адрес PDA (тикет/логин) не блокировал создание — закрыт LOW из аудита №1;
  в shine_payments is_uninitialized_account перестала зависеть от баланса.

shine_payments (HIGH из аудита №2):
- запрещён recipient == inflow_vault в buy_ticket*, manager_add_ticket и
  change_ticket_recipient; добавлена защита по умолчанию в transfer_from_vault
  (require vault.key != recipient.key). Это убирает алиасинг аккаунта в
  step_payout, который навсегда замораживал очередь выплат и средства вольта.

Документация и учёт:
- doc/programs/shine_payments.md §3.4, §10.1; doc/programs/shine_users.md §3.3;
- Dev_Docs/audit: добавлен аудит №2, обе закрытые находки помечены ИСПРАВЛЕНО;
- Dev_Docs/Pending_Features: две записи на ручную e2e-проверку на devnet;
- VERSION.properties: client 1.2.161, server 1.2.150.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 04:10:31 +04:00

1399 lines
53 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use anchor_lang::AccountDeserialize;
use pyth_solana_receiver_sdk::error::GetPriceError;
use pyth_solana_receiver_sdk::price_update::{get_feed_id_from_hex, FeedId, PriceUpdateV2};
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<PaymentsError> 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<u8, ProgramError> {
let value = *self
.data
.get(self.cursor)
.ok_or(self.error.clone())?;
self.cursor += 1;
Ok(value)
}
fn read_u64(&mut self) -> Result<u64, ProgramError> {
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<Pubkey, ProgramError> {
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<u8>;
fn decode(data: &[u8]) -> Result<Self, ProgramError>;
}
impl StateCodec for ConfigState {
fn encoded_len() -> usize {
1 + 32 + 32
}
fn encode(&self) -> Vec<u8> {
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<Self, ProgramError> {
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<u8> {
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<Self, ProgramError> {
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<u8> {
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<Self, ProgramError> {
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<u8> {
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<Self, ProgramError> {
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<u8> {
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<Self, ProgramError> {
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<u8> {
vec![self.version]
}
fn decode(data: &[u8]) -> Result<Self, ProgramError> {
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<Instruction, ProgramError> {
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);
validate_singleton_state_pda(program_id, config_pda, settings::CONFIG_SEED)?;
validate_singleton_state_pda(program_id, coef_limit_pda, settings::COEF_LIMIT_SEED)?;
let config = read_state::<ConfigState>(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::<CoefLimitState>(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)?;
validate_singleton_state_pda(program_id, config_pda, settings::CONFIG_SEED)?;
let config = read_state::<ConfigState>(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::<ManagerAllowanceState>(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);
// Получатель не должен совпадать с inflow-вольтом (см. подробности в
// buy_ticket_by_purchase_usd): иначе тикет навсегда застрянет в step_payout.
let (inflow_vault_addr, _) = find_single_pda(program_id, settings::INFLOW_VAULT_SEED);
require!(args.recipient_wallet != inflow_vault_addr, PaymentsError::InvalidTicketRecipient);
let (expected_manager_pda, _) = find_manager_allowance_pda(program_id, signer.key);
require_keys_eq!(expected_manager_pda, *manager_allowance_pda.key, PaymentsError::InvalidPdaAddress);
validate_singleton_state_pda(program_id, queues_pda, settings::QUEUES_SEED)?;
let mut allowance = read_state::<ManagerAllowanceState>(manager_allowance_pda)?;
require_keys_eq!(allowance.manager_wallet, *signer.key, PaymentsError::InvalidManagerWallet);
let mut queues = read_state::<QueuesState>(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);
validate_singleton_state_pda(program_id, config_pda, settings::CONFIG_SEED)?;
validate_singleton_state_pda(program_id, coef_limit_pda, settings::COEF_LIMIT_SEED)?;
validate_singleton_state_pda(program_id, queues_pda, settings::QUEUES_SEED)?;
validate_singleton_state_pda(program_id, inflow_vault_pda, settings::INFLOW_VAULT_SEED)?;
let config = read_state::<ConfigState>(config_pda)?;
let coef_limit = read_state::<CoefLimitState>(coef_limit_pda)?;
let mut queues = read_state::<QueuesState>(queues_pda)?;
let _vault_state = read_state::<VaultState>(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::<TicketState>(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);
validate_singleton_state_pda(program_id, queues_pda, settings::QUEUES_SEED)?;
// Новый получатель не должен совпадать с inflow-вольтом (см. подробности в
// buy_ticket_by_purchase_usd): иначе тикет навсегда застрянет в step_payout.
let (inflow_vault_addr, _) = find_single_pda(program_id, settings::INFLOW_VAULT_SEED);
require!(args.new_recipient_wallet != inflow_vault_addr, PaymentsError::InvalidTicketRecipient);
let queues = read_state::<QueuesState>(queues_pda)?;
let mut ticket = read_state::<TicketState>(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<Self, ProgramError> {
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 validate_singleton_state_pda(
program_id: &Pubkey,
pda: &AccountInfo,
seed: &[u8],
) -> ProgramResult {
let (expected_pda, _) = find_single_pda(program_id, seed);
require_keys_eq!(expected_pda, *pda.key, PaymentsError::InvalidPdaAddress);
require_keys_eq!(*pda.owner, id(), 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 {
validate_singleton_state_pda(program_id, ctx.config_pda, settings::CONFIG_SEED)?;
validate_singleton_state_pda(program_id, ctx.coef_limit_pda, settings::COEF_LIMIT_SEED)?;
validate_singleton_state_pda(program_id, ctx.queues_pda, settings::QUEUES_SEED)?;
let config = read_state::<ConfigState>(ctx.config_pda)?;
let coef_limit = read_state::<CoefLimitState>(ctx.coef_limit_pda)?;
let mut queues = read_state::<QueuesState>(ctx.queues_pda)?;
require_keys_eq!(*ctx.dao_wallet.key, config.dao_wallet, PaymentsError::InvalidDaoWallet);
// Получатель тикета не должен совпадать с inflow-вольтом: иначе в step_payout
// вольт окажется и источником, и получателем перевода (алиасинг одного аккаунта),
// второй mutable-займ в transfer_from_vault упадёт, и такой тикет навсегда
// заморозит обслуживание очереди. Запрещаем такой recipient на входе.
require!(recipient_wallet != config.inflow_vault, PaymentsError::InvalidTicketRecipient);
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<SolUsdPrice, ProgramError> {
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);
require_keys_eq!(*price_update.owner, pyth_solana_receiver_sdk::ID, PaymentsError::InvalidOracleAccount);
let data = price_update.try_borrow_data()?;
let clock = Clock::get()?;
let mut data_slice: &[u8] = &data;
let price_update_state = PriceUpdateV2::try_deserialize(&mut data_slice)
.map_err(|_| ProgramError::from(PaymentsError::InvalidOraclePrice))?;
let feed_id = expected_sol_usd_feed_id()?;
let price = price_update_state
.get_price_no_older_than(&clock, settings::ORACLE_MAX_AGE_SECS, &feed_id)
.map_err(map_pyth_price_error)?;
require!(price.price > 0, PaymentsError::InvalidOraclePrice);
require_oracle_confidence_ok(price.price, price.conf)?;
sol_usd_price_from_components(price.price, price.exponent)
}
fn expected_sol_usd_feed_id() -> Result<FeedId, ProgramError> {
get_feed_id_from_hex(settings::PYTH_SOL_USD_FEED_ID)
.map_err(|_| ProgramError::from(PaymentsError::InvalidOracleFeedConfig))
}
fn map_pyth_price_error(err: GetPriceError) -> ProgramError {
match err {
GetPriceError::PriceTooOld => PaymentsError::OraclePriceTooOld.into(),
GetPriceError::MismatchedFeedId => PaymentsError::InvalidOracleFeed.into(),
GetPriceError::InsufficientVerificationLevel => PaymentsError::InvalidOraclePrice.into(),
GetPriceError::FeedIdMustBe32Bytes | GetPriceError::FeedIdNonHexCharacter => {
PaymentsError::InvalidOracleFeedConfig.into()
}
GetPriceError::InvalidWindowSize => PaymentsError::InvalidOraclePrice.into(),
}
}
fn require_oracle_confidence_ok(price: i64, conf: u64) -> ProgramResult {
require!(price > 0, PaymentsError::InvalidOraclePrice);
let conf_ppm = checked_mul_u128(conf as u128, 1_000_000u128)? / (price as u128);
require!(
conf_ppm <= settings::ORACLE_MAX_CONFIDENCE_PPM as u128,
PaymentsError::InvalidOraclePrice
);
Ok(())
}
fn sol_usd_price_from_components(price: i64, exponent: i32) -> Result<SolUsdPrice, ProgramError> {
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 lamports_to_usd_cents_floor(lamports: u64, price: &SolUsdPrice) -> Result<u64, ProgramError> {
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<u64, ProgramError> {
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 required_lamports = Rent::get()?.minimum_balance(space as usize);
let current_lamports = pda.lamports();
if current_lamports == 0 {
// Быстрый путь: адрес пуст — обычное создание аккаунта одной инструкцией.
let create_ix = system_instruction::create_account(payer.key, pda.key, required_lamports, space, program_id);
return invoke_signed(
&create_ix,
&[payer.clone(), pda.clone(), system_program_ai.clone()],
&[seeds],
);
}
// На адресе уже лежат лампорты — вероятно, «подсев» атакующим на заранее
// известный детерминированный адрес тикета/PDA. Создаём «поверх предзаполненного»:
// доводим ренту переводом, затем allocate + assign под подписью PDA.
let top_up = required_lamports.saturating_sub(current_lamports);
if top_up > 0 {
let transfer_ix = system_instruction::transfer(payer.key, pda.key, top_up);
invoke(&transfer_ix, &[payer.clone(), pda.clone(), system_program_ai.clone()])?;
}
let allocate_ix = system_instruction::allocate(pda.key, space);
invoke_signed(&allocate_ix, &[pda.clone(), system_program_ai.clone()], &[seeds])?;
let assign_ix = system_instruction::assign(pda.key, program_id);
invoke_signed(&assign_ix, &[pda.clone(), system_program_ai.clone()], &[seeds])
}
fn write_state<T: StateCodec>(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<T: StateCodec>(pda: &AccountInfo) -> Result<T, ProgramError> {
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])
}
// «Инициализируемым» считаем аккаунт без данных, всё ещё принадлежащий System
// Program. Условие lamports() == 0 сознательно убрано: адреса тикетов и прочих
// PDA детерминированы, и любой может заранее перевести на них немного лампортов,
// чтобы заблокировать создание (griefing-DoS). Наличие лампортов больше не должно
// мешать инициализации — за безопасное создание «поверх предзаполненного» отвечает
// create_pda_account. Уже созданный нашей программой PDA имеет данные/владельца id()
// и сюда не пройдёт.
fn is_uninitialized_account(account: &AccountInfo) -> bool {
account.data_len() == 0
&& (*account.owner == system_program::ID || *account.owner == Pubkey::default())
}
fn available_vault_lamports(vault: &AccountInfo) -> Result<u64, ProgramError> {
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(());
}
// Защита по умолчанию от алиасинга: источник и получатель не должны быть одним
// и тем же аккаунтом, иначе второй mutable-займ лампортов ниже завершится ошибкой.
require!(vault.key != recipient.key, PaymentsError::InvalidTicketRecipient);
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<u64, ProgramError> {
left.checked_add(right).ok_or(PaymentsError::MathOverflow.into())
}
fn checked_sub(left: u64, right: u64) -> Result<u64, ProgramError> {
left.checked_sub(right).ok_or(PaymentsError::MathOverflow.into())
}
fn checked_mul(left: u64, right: u64) -> Result<u64, ProgramError> {
left.checked_mul(right).ok_or(PaymentsError::MathOverflow.into())
}
fn checked_mul_u128(left: u128, right: u128) -> Result<u128, ProgramError> {
left.checked_mul(right).ok_or(PaymentsError::MathOverflow.into())
}