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>
1399 lines
53 KiB
Rust
1399 lines
53 KiB
Rust
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())
|
||
}
|