use std::cell::RefMut;
use std::convert::TryInto;
use std::mem::size_of;
use std::ops::DerefMut;
use bonfida_utils::BorshSize;
use borsh::{BorshDeserialize, BorshSerialize};
use bytemuck::{cast_slice, from_bytes, from_bytes_mut, Pod, try_cast_slice_mut, Zeroable};
use num_derive::{FromPrimitive, ToPrimitive};
use num_traits::FromPrimitive;
use solana_program::account_info::AccountInfo;
use solana_program::clock::Clock;
use solana_program::entrypoint::ProgramResult;
use solana_program::msg;
use solana_program::program_error::ProgramError;
use solana_program::pubkey::Pubkey;
use solana_program::sysvar::Sysvar;
use crate::error::AccessError;
use crate::instruction::ProgramInstruction;
use crate::instruction::ProgramInstruction::AdminProgramFreeze;
use crate::utils::is_admin_renouncable_instruction;
pub const ACCESS_MINT: Pubkey =
solana_program::pubkey!("5MAYDfq5yxtudAhtfyuMBuHZjgAbaS9tbEyEQYAhDS5y");
pub const SECONDS_IN_DAY: u64 = if cfg!(feature = "days-to-sec-15m") {
15 * 60
} else if cfg!(feature = "days-to-sec-10s") {
10
} else {
3600 * 24
};
pub const V1_INSTRUCTIONS_ALLOWED: bool = cfg!(feature = "v1-instructions-allowed");
pub const DEFAULT_STAKER_MULTIPLIER: u64 = 50;
pub const STAKE_BUFFER_LEN: u64 = 274;
pub const MAX_FEE_RECIPIENTS: usize = 10;
pub const MIN_DISTRIBUTE_AMOUNT: u64 = 100_000_000;
pub const MAX_FEE_SPLIT_SETUP_DELAY: u64 = 5 * 60; pub const DEFAULT_FEE_BASIS_POINTS: u16 = 200;
#[derive(
BorshSerialize, BorshDeserialize, BorshSize, PartialEq, FromPrimitive, ToPrimitive, Debug,
)]
#[repr(u8)]
#[allow(missing_docs)]
pub enum Tag {
Uninitialized,
StakePool,
InactiveStakePool,
StakeAccount,
InactiveBondAccount,
BondAccount,
CentralState,
Deleted,
FrozenStakePool,
FrozenStakeAccount,
FrozenBondAccount,
BondV2Account,
CentralStateV2,
}
impl Tag {
pub fn opposite(&self) -> Result<Tag, ProgramError> {
let tag = match self {
Tag::StakePool => Tag::FrozenStakePool,
Tag::StakeAccount => Tag::FrozenStakeAccount,
Tag::BondAccount => Tag::FrozenBondAccount,
Tag::FrozenStakePool => Tag::StakePool,
Tag::FrozenStakeAccount => Tag::StakeAccount,
Tag::FrozenBondAccount => Tag::BondAccount,
_ => return Err(AccessError::InvalidTagChange.into()),
};
Ok(tag)
}
}
#[derive(BorshSerialize, BorshDeserialize, BorshSize, Copy, Clone, Pod, Zeroable, Debug)]
#[repr(C)]
#[allow(missing_docs)]
pub struct StakePoolHeader {
pub tag: u8,
pub nonce: u8,
pub current_day_idx: u16,
pub _padding: [u8; 4],
pub minimum_stake_amount: u64,
pub total_staked: u64,
pub last_claimed_offset: u64,
pub stakers_part: u64,
pub owner: [u8; 32],
pub vault: [u8; 32],
}
#[allow(missing_docs)]
pub struct StakePool<H, B> {
pub header: H,
pub balances: B,
}
#[derive(Pod, Clone, Copy, Zeroable, Debug)]
#[repr(C)]
pub struct RewardsTuple {
pub(crate) pool_reward: u128,
pub(crate) stakers_reward: u128,
}
#[allow(missing_docs)]
pub type StakePoolRef<'a> = StakePool<RefMut<'a, StakePoolHeader>, RefMut<'a, [RewardsTuple]>>;
#[allow(missing_docs)]
pub type StakePoolHeaped = StakePool<Box<StakePoolHeader>, Box<[RewardsTuple]>>;
#[allow(missing_docs)]
impl<'a> StakePoolRef<'a> {
pub fn get_checked<'b: 'a>(
account_info: &'a AccountInfo<'b>,
allowed_tags: Vec<Tag>,
) -> Result<Self, ProgramError> {
let (header, balances) = RefMut::map_split(account_info.data.borrow_mut(), |s| {
let (hd, rem) = s.split_at_mut(size_of::<StakePoolHeader>());
(
from_bytes_mut::<StakePoolHeader>(hd),
try_cast_slice_mut(rem).unwrap(),
)
});
let tag = FromPrimitive::from_u8(header.tag).ok_or(ProgramError::InvalidAccountData)?;
if !allowed_tags.contains(&tag) {
return Err(AccessError::DataTypeMismatch.into());
}
Ok(StakePool { header, balances })
}
}
#[allow(missing_docs)]
impl StakePoolHeaped {
pub fn from_buffer(buf: &[u8]) -> Self {
println!("StakePoolHeaped::from_buffer: buf.len() = {}", buf.len());
let (header, balances) = buf.split_at(size_of::<StakePoolHeader>());
println!(
"StakePoolHeaped::from_buffer: header.len() = {}",
header.len()
);
let header = from_bytes::<StakePoolHeader>(header);
println!("StakePoolHeaped::from_buffer: header = {:?}", header);
let balances = cast_slice::<_, RewardsTuple>(balances);
println!(
"StakePoolHeaped::from_buffer: balances.len() = {}",
balances.len()
);
Self {
header: Box::new(*header),
balances: Box::from(balances),
}
}
}
#[allow(missing_docs)]
impl<H: DerefMut<Target=StakePoolHeader>, B: DerefMut<Target=[RewardsTuple]>> StakePool<H, B> {
pub fn push_balances_buff(
&mut self,
current_offset: u64,
rewards: RewardsTuple,
) -> Result<(), ProgramError> {
let nb_days_passed = current_offset
.checked_sub(self.header.current_day_idx as u64)
.ok_or(AccessError::Overflow)?;
for i in 1..nb_days_passed {
self.balances[(((self.header.current_day_idx as u64)
.checked_add(i)
.ok_or(AccessError::Overflow)?)
% STAKE_BUFFER_LEN) as usize] = RewardsTuple {
pool_reward: 0,
stakers_reward: 0,
};
}
self.header.current_day_idx = self
.header
.current_day_idx
.checked_add(
nb_days_passed
.try_into()
.map_err(|_| AccessError::Overflow)?,
)
.ok_or(AccessError::Overflow)?;
self.balances[(((self.header.current_day_idx - 1) as u64) % STAKE_BUFFER_LEN) as usize] =
rewards;
Ok(())
}
pub fn create_key(
nonce: &u8,
owner: &Pubkey,
program_id: &Pubkey,
) -> Result<Pubkey, ProgramError> {
let seeds: &[&[u8]] = &[StakePoolHeader::SEED, &owner.to_bytes(), &[*nonce]];
Pubkey::create_program_address(seeds, program_id).map_err(|_| ProgramError::InvalidSeeds)
}
}
#[allow(missing_docs)]
impl StakePool<(), ()> {
pub fn find_key(owner: &Pubkey, program_id: &Pubkey) -> (Pubkey, u8) {
let seeds: &[&[u8]] = &[StakePoolHeader::SEED, &owner.to_bytes()];
Pubkey::find_program_address(seeds, program_id)
}
}
#[allow(missing_docs)]
impl StakePoolHeader {
pub const SEED: &'static [u8; 10] = b"stake_pool";
pub fn new(
owner: Pubkey,
nonce: u8,
vault: Pubkey,
minimum_stake_amount: u64,
) -> Result<Self, ProgramError> {
Ok(Self {
tag: Tag::InactiveStakePool as u8,
total_staked: 0,
current_day_idx: 0,
_padding: [0; 4],
last_claimed_offset: 0,
owner: owner.to_bytes(),
nonce,
vault: vault.to_bytes(),
minimum_stake_amount,
stakers_part: DEFAULT_STAKER_MULTIPLIER,
})
}
pub fn close(&mut self) {
self.tag = Tag::Deleted as u8
}
pub fn deposit(&mut self, amount: u64) -> ProgramResult {
self.total_staked = self
.total_staked
.checked_add(amount)
.ok_or(AccessError::Overflow)?;
Ok(())
}
pub fn withdraw(&mut self, amount: u64) -> ProgramResult {
self.total_staked = self
.total_staked
.checked_sub(amount)
.ok_or(AccessError::Overflow)?;
Ok(())
}
}
#[derive(BorshSerialize, BorshDeserialize, BorshSize, Debug)]
#[allow(missing_docs)]
pub struct StakeAccount {
pub tag: Tag,
pub owner: Pubkey,
pub stake_amount: u64,
pub stake_pool: Pubkey,
pub last_claimed_offset: u64,
pub pool_minimum_at_creation: u64,
}
#[allow(missing_docs)]
impl StakeAccount {
pub const SEED: &'static [u8; 13] = b"stake_account";
pub fn new(owner: Pubkey, stake_pool: Pubkey, pool_minimum_at_creation: u64) -> Self {
Self {
tag: Tag::StakeAccount,
owner,
stake_amount: 0,
stake_pool,
last_claimed_offset: 0,
pool_minimum_at_creation,
}
}
pub fn create_key(
nonce: &u8,
owner: &Pubkey,
stake_pool: &Pubkey,
program_id: &Pubkey,
) -> Result<Pubkey, ProgramError> {
let seeds: &[&[u8]] = &[
StakeAccount::SEED,
&owner.to_bytes(),
&stake_pool.to_bytes(),
&[*nonce],
];
Pubkey::create_program_address(seeds, program_id).map_err(|_| ProgramError::InvalidSeeds)
}
pub fn find_key(owner: &Pubkey, stake_pool: &Pubkey, program_id: &Pubkey) -> (Pubkey, u8) {
let seeds: &[&[u8]] = &[
StakeAccount::SEED,
&owner.to_bytes(),
&stake_pool.to_bytes(),
];
Pubkey::find_program_address(seeds, program_id)
}
pub fn save(&self, mut dst: &mut [u8]) -> ProgramResult {
self.serialize(&mut dst)
.map_err(|_| ProgramError::InvalidAccountData)
}
pub fn from_account_info(a: &AccountInfo) -> Result<StakeAccount, ProgramError> {
let mut data = &a.data.borrow() as &[u8];
if data[0] != Tag::StakeAccount as u8 && data[0] != Tag::Uninitialized as u8 {
return Err(AccessError::DataTypeMismatch.into());
}
let result = StakeAccount::deserialize(&mut data)?;
Ok(result)
}
pub fn close(&mut self) {
self.tag = Tag::Deleted
}
pub fn deposit(&mut self, amount: u64) -> ProgramResult {
self.stake_amount = self
.stake_amount
.checked_add(amount)
.ok_or(AccessError::Overflow)?;
Ok(())
}
pub fn withdraw(&mut self, amount: u64) -> ProgramResult {
self.stake_amount = self
.stake_amount
.checked_sub(amount)
.ok_or(AccessError::Overflow)?;
Ok(())
}
}
#[derive(BorshSerialize, BorshDeserialize, BorshSize)]
#[allow(missing_docs)]
pub struct CentralState {
pub tag: Tag,
pub signer_nonce: u8,
pub daily_inflation: u64,
pub token_mint: Pubkey,
pub authority: Pubkey,
pub creation_time: i64,
pub total_staked: u64,
pub total_staked_snapshot: u64,
pub last_snapshot_offset: u64,
}
impl CentralState {
#[allow(missing_docs)]
pub fn new(
signer_nonce: u8,
daily_inflation: u64,
token_mint: Pubkey,
authority: Pubkey,
total_staked: u64,
) -> Result<Self, ProgramError> {
Ok(Self {
tag: Tag::CentralState,
signer_nonce,
daily_inflation,
token_mint,
authority,
creation_time: Clock::get()?.unix_timestamp,
total_staked,
total_staked_snapshot: 0,
last_snapshot_offset: 0,
})
}
#[allow(missing_docs)]
pub fn create_key(signer_nonce: &u8, program_id: &Pubkey) -> Result<Pubkey, ProgramError> {
let signer_seeds: &[&[u8]] = &[&program_id.to_bytes(), &[*signer_nonce]];
Pubkey::create_program_address(signer_seeds, program_id)
.map_err(|_| ProgramError::InvalidSeeds)
}
#[allow(missing_docs)]
pub fn find_key(program_id: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[&program_id.to_bytes()], program_id)
}
#[allow(missing_docs)]
pub fn save(&self, mut dst: &mut [u8]) -> ProgramResult {
self.serialize(&mut dst)
.map_err(|_| ProgramError::InvalidAccountData)
}
#[allow(missing_docs)]
pub fn from_account_info(a: &AccountInfo) -> Result<CentralState, ProgramError> {
let mut data = &a.data.borrow() as &[u8];
if data[0] != Tag::CentralState as u8 && data[0] != Tag::Uninitialized as u8 {
return Err(AccessError::DataTypeMismatch.into());
}
let result = CentralState::deserialize(&mut data)?;
Ok(result)
}
#[allow(missing_docs)]
pub fn get_current_offset(&self) -> Result<u64, ProgramError> {
let current_time = Clock::get()?.unix_timestamp as u64;
Ok((current_time - self.creation_time as u64) / SECONDS_IN_DAY)
}
}
#[derive(BorshSerialize, BorshDeserialize, BorshSize, Debug)]
#[allow(missing_docs)]
pub struct CentralStateV2 {
pub tag: Tag,
pub bump_seed: u8,
pub daily_inflation: u64,
pub token_mint: Pubkey,
pub authority: Pubkey,
pub creation_time: i64,
pub total_staked: u64,
pub total_staked_snapshot: u64,
pub last_snapshot_offset: u64,
pub ix_gate: u128,
pub freeze_authority: Pubkey,
pub admin_ix_gate: u128,
pub fee_basis_points: u16,
pub last_fee_distribution_time: i64,
pub recipients: Vec<FeeRecipient>,
}
impl CentralStateV2 {
#[allow(missing_docs)]
pub fn from_central_state(
central_state: CentralState,
) -> Result<Self, ProgramError> {
Ok(Self {
tag: Tag::CentralStateV2,
bump_seed: central_state.signer_nonce,
daily_inflation: central_state.daily_inflation,
token_mint: central_state.token_mint,
authority: central_state.authority,
creation_time: central_state.creation_time,
total_staked: central_state.total_staked,
total_staked_snapshot: central_state.total_staked_snapshot,
last_snapshot_offset: central_state.last_snapshot_offset,
freeze_authority: central_state.authority,
ix_gate: u128::MAX, admin_ix_gate: u128::MAX, fee_basis_points: DEFAULT_FEE_BASIS_POINTS,
last_fee_distribution_time: Clock::get()?.unix_timestamp,
recipients: vec![], })
}
#[allow(missing_docs)]
pub fn create_key(signer_nonce: &u8, program_id: &Pubkey) -> Result<Pubkey, ProgramError> {
let signer_seeds: &[&[u8]] = &[&program_id.to_bytes(), &[*signer_nonce]];
Pubkey::create_program_address(signer_seeds, program_id)
.map_err(|_| ProgramError::InvalidSeeds)
}
#[allow(missing_docs)]
pub fn find_key(program_id: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[&program_id.to_bytes()], program_id)
}
#[allow(missing_docs)]
pub fn save(&self, mut dst: &mut [u8]) -> ProgramResult {
self.serialize(&mut dst)
.map_err(|_| ProgramError::InvalidAccountData)
}
#[allow(missing_docs)]
pub fn from_account_info(a: &AccountInfo) -> Result<CentralStateV2, ProgramError> {
let mut data = &a.data.borrow() as &[u8];
if data[0] != Tag::CentralStateV2 as u8 && data[0] != Tag::Uninitialized as u8 {
return Err(AccessError::DataTypeMismatch.into());
}
let result = CentralStateV2::deserialize(&mut data)?;
Ok(result)
}
#[allow(missing_docs)]
pub fn get_current_offset(&self) -> Result<u64, ProgramError> {
let current_time = Clock::get()?.unix_timestamp as u64;
Ok((current_time - self.creation_time as u64) / SECONDS_IN_DAY)
}
pub fn assert_instruction_allowed(&self, ix: &ProgramInstruction) -> ProgramResult {
let ix_num = *ix as u32;
let ix_mask = 1_u128.checked_shl(ix_num).ok_or(AccessError::Overflow)?;
if ix_mask & self.ix_gate == 0 && ix_num != AdminProgramFreeze as u32 {
return Err(AccessError::FrozenInstruction.into());
}
if is_admin_renouncable_instruction(ix) && ix_mask & self.admin_ix_gate == 0 {
return Err(AccessError::AlreadyRenounced.into());
}
Ok(())
}
pub fn calculate_fee(&self, amount: u64) -> Result<u64, ProgramError> {
let fee = amount
.checked_mul(self.fee_basis_points as u64)
.ok_or(AccessError::Overflow)?
.checked_add(9_999).ok_or(AccessError::Overflow)?
.checked_div(10_000)
.ok_or(AccessError::Overflow)?;
Ok(fee)
}
}
pub const BOND_SIGNER_THRESHOLD: u64 = 1;
pub const AUTHORIZED_BOND_SELLERS: [Pubkey; 1] = [solana_program::pubkey!(
"3Nrq6mCNL5i8Qk4APhggbwXismcsF23gNVDEaKycZBL8"
)];
#[derive(BorshSerialize, BorshDeserialize, BorshSize)]
#[allow(missing_docs)]
pub struct BondAccount {
pub tag: Tag,
pub owner: Pubkey,
pub total_amount_sold: u64,
pub total_staked: u64,
pub total_quote_amount: u64,
pub quote_mint: Pubkey,
pub seller_token_account: Pubkey,
pub unlock_start_date: i64,
pub unlock_period: i64,
pub unlock_amount: u64,
pub last_unlock_time: i64,
pub total_unlocked_amount: u64,
pub pool_minimum_at_creation: u64,
pub stake_pool: Pubkey,
pub last_claimed_offset: u64,
pub sellers: Vec<Pubkey>,
}
#[allow(missing_docs)]
impl BondAccount {
pub const SEED: &'static [u8; 12] = b"bond_account";
pub fn create_key(owner: &Pubkey, total_amount_sold: u64, program_id: &Pubkey) -> (Pubkey, u8) {
let seeds: &[&[u8]] = &[
BondAccount::SEED,
&owner.to_bytes(),
&total_amount_sold.to_le_bytes(),
];
Pubkey::find_program_address(seeds, program_id)
}
#[allow(clippy::too_many_arguments)]
pub fn new(
owner: Pubkey,
total_amount_sold: u64,
total_quote_amount: u64,
quote_mint: Pubkey,
seller_token_account: Pubkey,
unlock_start_date: i64,
unlock_period: i64,
unlock_amount: u64,
last_unlock_time: i64,
pool_minimum_at_creation: u64,
stake_pool: Pubkey,
seller: Pubkey,
) -> Self {
let sellers = vec![seller];
Self {
tag: Tag::InactiveBondAccount,
owner,
total_amount_sold,
total_staked: total_amount_sold,
total_quote_amount,
quote_mint,
seller_token_account,
unlock_start_date,
unlock_period,
unlock_amount,
last_unlock_time,
total_unlocked_amount: 0,
stake_pool,
last_claimed_offset: 0,
sellers,
pool_minimum_at_creation,
}
}
pub fn save(&self, mut dst: &mut [u8]) -> ProgramResult {
self.serialize(&mut dst)
.map_err(|_| ProgramError::InvalidAccountData)
}
pub fn is_active(&self) -> bool {
self.tag == Tag::BondAccount
}
pub fn activate(&mut self, current_offset: u64) -> ProgramResult {
self.tag = Tag::BondAccount;
self.last_claimed_offset = current_offset;
let current_time = Clock::get()?.unix_timestamp;
self.last_unlock_time = std::cmp::max(current_time, self.unlock_start_date);
Ok(())
}
pub fn from_account_info(
a: &AccountInfo,
allow_inactive: bool,
) -> Result<BondAccount, ProgramError> {
let mut data = &a.data.borrow() as &[u8];
let tag = if allow_inactive {
Tag::InactiveBondAccount
} else {
Tag::BondAccount
};
if data[0] != tag as u8 && data[0] != Tag::Uninitialized as u8 {
return Err(AccessError::DataTypeMismatch.into());
}
let result = BondAccount::deserialize(&mut data)?;
Ok(result)
}
pub fn calc_unlock_amount(&self, missed_periods: u64) -> Result<u64, ProgramError> {
msg!("Missed periods {}", missed_periods);
let cumulated_unlock_amnt = (missed_periods)
.checked_mul(self.unlock_amount)
.ok_or(AccessError::Overflow)?;
let unlock_amnt = std::cmp::min(
cumulated_unlock_amnt,
self.total_amount_sold
.checked_sub(self.total_unlocked_amount)
.ok_or(AccessError::Overflow)?,
);
msg!(
"Unlock amount {} Total amount {}",
unlock_amnt,
self.total_amount_sold
);
Ok(unlock_amnt)
}
}
#[derive(BorshSerialize, BorshDeserialize, BorshSize)]
#[allow(missing_docs)]
pub struct BondV2Account {
pub tag: Tag,
pub owner: Pubkey,
pub amount: u64,
pub pool: Pubkey,
pub last_claimed_offset: u64,
pub pool_minimum_at_creation: u64,
pub unlock_timestamp: Option<i64>,
}
#[allow(missing_docs)]
impl BondV2Account {
pub const SEED: &'static [u8; 15] = b"bond_v2_account";
pub fn create_key(
owner: &Pubkey,
pool: &Pubkey,
unlock_timestamp: Option<i64>,
program_id: &Pubkey,
) -> (Pubkey, u8) {
let seeds: &[&[u8]] = &[
BondV2Account::SEED,
&owner.to_bytes(),
&pool.to_bytes(),
&unlock_timestamp.unwrap_or(0).to_le_bytes(),
];
Pubkey::find_program_address(seeds, program_id)
}
#[allow(clippy::too_many_arguments)]
pub fn new(
owner: Pubkey,
pool: Pubkey,
pool_minimum_at_creation: u64,
amount: u64,
unlock_timestamp: Option<i64>,
current_offset: u64,
) -> Self {
Self {
tag: Tag::BondV2Account,
owner,
amount,
pool,
last_claimed_offset: current_offset,
pool_minimum_at_creation,
unlock_timestamp,
}
}
pub fn save(&self, mut dst: &mut [u8]) -> ProgramResult {
self.serialize(&mut dst)
.map_err(|_| ProgramError::InvalidAccountData)
}
pub fn from_account_info(a: &AccountInfo) -> Result<BondV2Account, ProgramError> {
let mut data = &a.data.borrow() as &[u8];
let tag = Tag::BondV2Account;
if data[0] != tag as u8 && data[0] != Tag::Uninitialized as u8 {
return Err(AccessError::DataTypeMismatch.into());
}
let result = BondV2Account::deserialize(&mut data)?;
Ok(result)
}
pub fn withdraw(&mut self, amount: u64) -> ProgramResult {
self.amount = self
.amount
.checked_sub(amount)
.ok_or(AccessError::Overflow)?;
Ok(())
}
}
#[derive(BorshSerialize, BorshDeserialize, BorshSize, Clone, Debug)]
#[allow(missing_docs)]
pub struct FeeRecipient {
pub owner: Pubkey,
pub percentage: u64,
}