1331 lines
48 KiB
Rust
1331 lines
48 KiB
Rust
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);
|
|
|
|
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)?;
|
|
|
|
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);
|
|
|
|
let (expected_manager_pda, _) = find_manager_allowance_pda(program_id, signer.key);
|
|
require_keys_eq!(expected_manager_pda, *manager_allowance_pda.key, PaymentsError::InvalidPdaAddress);
|
|
let mut allowance = read_state::<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);
|
|
|
|
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);
|
|
|
|
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 find_single_pda(program_id: &Pubkey, seed: &[u8]) -> (Pubkey, u8) {
|
|
Pubkey::find_program_address(&[seed], program_id)
|
|
}
|
|
|
|
fn find_ticket_pda(program_id: &Pubkey, queue_id: u8, index: u64) -> (Pubkey, u8) {
|
|
Pubkey::find_program_address(&[queue_seed(queue_id), &index.to_le_bytes()], program_id)
|
|
}
|
|
|
|
fn find_manager_allowance_pda(program_id: &Pubkey, manager_wallet: &Pubkey) -> (Pubkey, u8) {
|
|
Pubkey::find_program_address(
|
|
&[settings::MANAGER_ALLOWANCE_SEED, manager_wallet.as_ref()],
|
|
program_id,
|
|
)
|
|
}
|
|
|
|
fn queue_seed(queue_id: u8) -> &'static [u8] {
|
|
match queue_id {
|
|
1 => settings::Q1_TICKET_SEED,
|
|
2 => settings::Q2_TICKET_SEED,
|
|
3 => settings::Q3_TICKET_SEED,
|
|
_ => settings::Q1_TICKET_SEED,
|
|
}
|
|
}
|
|
|
|
fn is_valid_queue_id(queue_id: u8) -> bool {
|
|
matches!(queue_id, 1 | 2 | 3)
|
|
}
|
|
|
|
fn buy_ticket_by_purchase_usd(
|
|
program_id: &Pubkey,
|
|
ctx: &BuyTicketAccounts<'_, '_>,
|
|
purchase_usd_cents: u64,
|
|
transfer_lamports: u64,
|
|
recipient_wallet: Pubkey,
|
|
) -> ProgramResult {
|
|
let config = read_state::<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);
|
|
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);
|
|
|
|
let data = price_update.try_borrow_data()?;
|
|
let clock = Clock::get()?;
|
|
parse_pyth_price_update_v2(&data, &clock)
|
|
}
|
|
|
|
fn parse_pyth_price_update_v2(data: &[u8], clock: &Clock) -> Result<SolUsdPrice, ProgramError> {
|
|
require!(data.len() > 100, PaymentsError::InvalidOraclePrice);
|
|
let price = read_i64_at(data, 73)?;
|
|
let exponent = read_i32_at(data, 89)?;
|
|
let publish_time = read_i64_at(data, 93)?;
|
|
require!(
|
|
publish_time.saturating_add(settings::ORACLE_MAX_AGE_SECS as i64) >= clock.unix_timestamp,
|
|
PaymentsError::OraclePriceTooOld
|
|
);
|
|
require!(price > 0, PaymentsError::InvalidOraclePrice);
|
|
let mut num = checked_mul_u128(price as u128, settings::USD_CENTS_SCALE as u128)?;
|
|
let mut den: u128 = 1;
|
|
|
|
if exponent >= 0 {
|
|
let pow = 10u128
|
|
.checked_pow(exponent as u32)
|
|
.ok_or(ProgramError::from(PaymentsError::MathOverflow))?;
|
|
num = checked_mul_u128(num, pow)?;
|
|
} else {
|
|
let pow = 10u128
|
|
.checked_pow((-exponent) as u32)
|
|
.ok_or(ProgramError::from(PaymentsError::MathOverflow))?;
|
|
den = checked_mul_u128(den, pow)?;
|
|
}
|
|
|
|
require!(num > 0 && den > 0, PaymentsError::InvalidOraclePrice);
|
|
Ok(SolUsdPrice { price_num: num, price_den: den })
|
|
}
|
|
|
|
fn read_i32_at(data: &[u8], offset: usize) -> Result<i32, ProgramError> {
|
|
let end = offset
|
|
.checked_add(4)
|
|
.ok_or(ProgramError::from(PaymentsError::InvalidOraclePrice))?;
|
|
let slice = data
|
|
.get(offset..end)
|
|
.ok_or(ProgramError::from(PaymentsError::InvalidOraclePrice))?;
|
|
Ok(i32::from_le_bytes(
|
|
slice
|
|
.try_into()
|
|
.map_err(|_| ProgramError::from(PaymentsError::InvalidOraclePrice))?,
|
|
))
|
|
}
|
|
|
|
fn read_i64_at(data: &[u8], offset: usize) -> Result<i64, ProgramError> {
|
|
let end = offset
|
|
.checked_add(8)
|
|
.ok_or(ProgramError::from(PaymentsError::InvalidOraclePrice))?;
|
|
let slice = data
|
|
.get(offset..end)
|
|
.ok_or(ProgramError::from(PaymentsError::InvalidOraclePrice))?;
|
|
Ok(i64::from_le_bytes(
|
|
slice
|
|
.try_into()
|
|
.map_err(|_| ProgramError::from(PaymentsError::InvalidOraclePrice))?,
|
|
))
|
|
}
|
|
|
|
fn lamports_to_usd_cents_floor(lamports: u64, price: &SolUsdPrice) -> Result<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 lamports = Rent::get()?.minimum_balance(space as usize);
|
|
let create_ix = system_instruction::create_account(payer.key, pda.key, lamports, space, program_id);
|
|
invoke_signed(
|
|
&create_ix,
|
|
&[payer.clone(), pda.clone(), system_program_ai.clone()],
|
|
&[seeds],
|
|
)
|
|
}
|
|
|
|
fn write_state<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])
|
|
}
|
|
|
|
fn is_uninitialized_account(account: &AccountInfo) -> bool {
|
|
account.lamports() == 0
|
|
&& account.data_len() == 0
|
|
&& (*account.owner == system_program::ID || *account.owner == Pubkey::default())
|
|
}
|
|
|
|
fn available_vault_lamports(vault: &AccountInfo) -> Result<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(());
|
|
}
|
|
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())
|
|
}
|