SHiNE-server/shine-solana/shine/programs/shine_payments/src/lib.rs

1148 lines
41 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

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

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