use {
crate::{
error::SinglePoolError,
inline_mpl_token_metadata::{
self,
instruction::{create_metadata_accounts_v3, update_metadata_accounts_v2},
pda::find_metadata_account,
state::DataV2,
},
instruction::SinglePoolInstruction,
state::{SinglePool, SinglePoolAccountType},
MINT_DECIMALS, PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH, PHANTOM_TOKEN_AMOUNT,
POOL_MINT_AUTHORITY_PREFIX, POOL_MINT_PREFIX, POOL_MPL_AUTHORITY_PREFIX,
POOL_ONRAMP_PREFIX, POOL_PREFIX, POOL_STAKE_AUTHORITY_PREFIX, POOL_STAKE_PREFIX,
VOTE_STATE_AUTHORIZED_WITHDRAWER_END, VOTE_STATE_AUTHORIZED_WITHDRAWER_START,
VOTE_STATE_DISCRIMINATOR_END,
},
borsh::BorshDeserialize,
solana_account_info::{next_account_info, AccountInfo},
solana_borsh::v1::try_from_slice_unchecked,
solana_clock::Clock,
solana_cpi::invoke_signed,
solana_msg::msg,
solana_native_token::LAMPORTS_PER_SOL,
solana_program_entrypoint::ProgramResult,
solana_program_error::ProgramError,
solana_program_pack::Pack,
solana_pubkey::Pubkey,
solana_rent::Rent,
solana_stake_interface::{
self as stake,
state::{Meta, Stake, StakeActivationStatus, StakeStateV2},
sysvar::stake_history::StakeHistorySysvar,
},
solana_system_interface::{instruction as system_instruction, program as system_program},
solana_sysvar::{Sysvar, SysvarSerialize},
solana_vote_interface::program as vote_program,
spl_token_interface::{self as spl_token, state::Mint},
};
fn pool_net_asset_value(
pool_stake_info: &AccountInfo,
pool_onramp_info: &AccountInfo,
rent: &Rent,
) -> u64 {
let pool_rent_exempt_reserve = rent.minimum_balance(pool_stake_info.data_len());
let onramp_rent_exempt_reserve = rent.minimum_balance(pool_onramp_info.data_len());
pool_stake_info
.lamports()
.saturating_add(pool_onramp_info.lamports())
.saturating_sub(pool_rent_exempt_reserve)
.saturating_sub(onramp_rent_exempt_reserve)
}
fn calculate_deposit_amount(
pre_token_supply: u64,
pre_pool_nev: u64,
user_deposit_amount: u64,
) -> Option<u64> {
if pre_pool_nev == 0 || pre_token_supply == 0 {
Some(user_deposit_amount)
} else {
u64::try_from(
(user_deposit_amount as u128)
.checked_mul(pre_token_supply as u128)?
.checked_div(pre_pool_nev as u128)?,
)
.ok()
}
}
fn calculate_withdraw_amount(
pre_token_supply: u64,
pre_pool_nev: u64,
user_tokens_to_burn: u64,
) -> Option<u64> {
let numerator = (user_tokens_to_burn as u128).checked_mul(pre_pool_nev as u128)?;
let denominator = pre_token_supply as u128;
if numerator < denominator || denominator == 0 {
Some(0)
} else {
u64::try_from(numerator.checked_div(denominator)?).ok()
}
}
fn get_stake_state(stake_account_info: &AccountInfo) -> Result<(Meta, Stake), ProgramError> {
match deserialize_stake(stake_account_info) {
Ok(StakeStateV2::Stake(meta, stake, _)) => Ok((meta, stake)),
_ => Err(SinglePoolError::WrongStakeState.into()),
}
}
fn get_stake_amount(stake_account_info: &AccountInfo) -> Result<u64, ProgramError> {
Ok(get_stake_state(stake_account_info)?.1.delegation.stake)
}
fn deserialize_stake(stake_account_info: &AccountInfo) -> Result<StakeStateV2, ProgramError> {
Ok(try_from_slice_unchecked::<StakeStateV2>(
&stake_account_info.data.borrow(),
)?)
}
fn is_stake_fully_active(stake_activation_status: &StakeActivationStatus) -> bool {
matches!(stake_activation_status, StakeActivationStatus {
effective,
activating: 0,
deactivating: 0,
} if *effective > 0)
}
fn is_stake_newly_activating(stake_activation_status: &StakeActivationStatus) -> bool {
matches!(stake_activation_status, StakeActivationStatus {
effective: 0,
activating,
deactivating: 0,
} if *activating > 0)
}
fn check_pool_address(
program_id: &Pubkey,
vote_account_address: &Pubkey,
check_address: &Pubkey,
) -> Result<u8, ProgramError> {
check_pool_pda(
program_id,
vote_account_address,
check_address,
&crate::find_pool_address_and_bump,
"pool",
SinglePoolError::InvalidPoolAccount,
)
}
fn check_pool_stake_address(
program_id: &Pubkey,
pool_address: &Pubkey,
check_address: &Pubkey,
) -> Result<u8, ProgramError> {
check_pool_pda(
program_id,
pool_address,
check_address,
&crate::find_pool_stake_address_and_bump,
"stake account",
SinglePoolError::InvalidPoolStakeAccount,
)
}
fn check_pool_onramp_address(
program_id: &Pubkey,
pool_address: &Pubkey,
check_address: &Pubkey,
) -> Result<u8, ProgramError> {
check_pool_pda(
program_id,
pool_address,
check_address,
&crate::find_pool_onramp_address_and_bump,
"onramp account",
SinglePoolError::InvalidPoolOnRampAccount,
)
}
fn check_pool_mint_address(
program_id: &Pubkey,
pool_address: &Pubkey,
check_address: &Pubkey,
) -> Result<u8, ProgramError> {
check_pool_pda(
program_id,
pool_address,
check_address,
&crate::find_pool_mint_address_and_bump,
"mint",
SinglePoolError::InvalidPoolMint,
)
}
fn check_pool_stake_authority_address(
program_id: &Pubkey,
pool_address: &Pubkey,
check_address: &Pubkey,
) -> Result<u8, ProgramError> {
check_pool_pda(
program_id,
pool_address,
check_address,
&crate::find_pool_stake_authority_address_and_bump,
"stake authority",
SinglePoolError::InvalidPoolStakeAuthority,
)
}
fn check_pool_mint_authority_address(
program_id: &Pubkey,
pool_address: &Pubkey,
check_address: &Pubkey,
) -> Result<u8, ProgramError> {
check_pool_pda(
program_id,
pool_address,
check_address,
&crate::find_pool_mint_authority_address_and_bump,
"mint authority",
SinglePoolError::InvalidPoolMintAuthority,
)
}
fn check_pool_mpl_authority_address(
program_id: &Pubkey,
pool_address: &Pubkey,
check_address: &Pubkey,
) -> Result<u8, ProgramError> {
check_pool_pda(
program_id,
pool_address,
check_address,
&crate::find_pool_mpl_authority_address_and_bump,
"MPL authority",
SinglePoolError::InvalidPoolMplAuthority,
)
}
fn check_pool_pda(
program_id: &Pubkey,
base_address: &Pubkey,
check_address: &Pubkey,
pda_lookup_fn: &dyn Fn(&Pubkey, &Pubkey) -> (Pubkey, u8),
pda_name: &str,
pool_error: SinglePoolError,
) -> Result<u8, ProgramError> {
let (derived_address, bump_seed) = pda_lookup_fn(program_id, base_address);
if *check_address != derived_address {
msg!(
"Incorrect {} address for base {}: expected {}, received {}",
pda_name,
base_address,
derived_address,
check_address,
);
Err(pool_error.into())
} else {
Ok(bump_seed)
}
}
fn check_vote_account(vote_account_info: &AccountInfo) -> Result<(), ProgramError> {
check_account_owner(vote_account_info, &vote_program::id())?;
let vote_account_data = &vote_account_info.try_borrow_data()?;
let state_variant = vote_account_data
.get(..VOTE_STATE_DISCRIMINATOR_END)
.and_then(|s| s.try_into().ok())
.ok_or(SinglePoolError::UnparseableVoteAccount)?;
#[allow(clippy::manual_range_patterns)]
match u32::from_le_bytes(state_variant) {
1 | 2 | 3 => Ok(()),
0 => Err(SinglePoolError::LegacyVoteAccount.into()),
_ => Err(SinglePoolError::UnparseableVoteAccount.into()),
}
}
fn check_pool_mint_with_supply(
program_id: &Pubkey,
pool_address: &Pubkey,
pool_mint_info: &AccountInfo,
) -> Result<u64, ProgramError> {
check_pool_mint_address(program_id, pool_address, pool_mint_info.key)?;
let pool_mint_data = pool_mint_info.try_borrow_data()?;
let pool_mint = Mint::unpack_from_slice(&pool_mint_data)?;
Ok(pool_mint.supply.saturating_add(PHANTOM_TOKEN_AMOUNT))
}
fn check_mpl_metadata_account_address(
metadata_address: &Pubkey,
pool_mint: &Pubkey,
) -> Result<(), ProgramError> {
let (metadata_account_pubkey, _) = find_metadata_account(pool_mint);
if metadata_account_pubkey != *metadata_address {
Err(SinglePoolError::InvalidMetadataAccount.into())
} else {
Ok(())
}
}
fn check_system_program(program_id: &Pubkey) -> Result<(), ProgramError> {
if *program_id != system_program::id() {
msg!(
"Expected system program {}, received {}",
system_program::id(),
program_id
);
Err(ProgramError::IncorrectProgramId)
} else {
Ok(())
}
}
fn check_token_program(address: &Pubkey) -> Result<(), ProgramError> {
if *address != spl_token::id() {
msg!(
"Incorrect token program, expected {}, received {}",
spl_token::id(),
address
);
Err(ProgramError::IncorrectProgramId)
} else {
Ok(())
}
}
fn check_stake_program(program_id: &Pubkey) -> Result<(), ProgramError> {
if *program_id != stake::program::id() {
msg!(
"Expected stake program {}, received {}",
stake::program::id(),
program_id
);
Err(ProgramError::IncorrectProgramId)
} else {
Ok(())
}
}
fn check_mpl_metadata_program(program_id: &Pubkey) -> Result<(), ProgramError> {
if *program_id != inline_mpl_token_metadata::id() {
msg!(
"Expected MPL metadata program {}, received {}",
inline_mpl_token_metadata::id(),
program_id
);
Err(ProgramError::IncorrectProgramId)
} else {
Ok(())
}
}
fn check_account_owner(
account_info: &AccountInfo,
program_id: &Pubkey,
) -> Result<(), ProgramError> {
if *program_id != *account_info.owner {
msg!(
"Expected account to be owned by program {}, received {}",
program_id,
account_info.owner
);
Err(ProgramError::IncorrectProgramId)
} else {
Ok(())
}
}
fn minimum_pool_balance() -> Result<u64, ProgramError> {
Ok(std::cmp::max(
stake::tools::get_minimum_delegation()?,
LAMPORTS_PER_SOL,
))
}
pub struct Processor {}
impl Processor {
#[allow(clippy::too_many_arguments)]
fn stake_merge<'a>(
pool_account_key: &Pubkey,
source_account: AccountInfo<'a>,
authority: AccountInfo<'a>,
bump_seed: u8,
destination_account: AccountInfo<'a>,
clock: AccountInfo<'a>,
stake_history: AccountInfo<'a>,
) -> Result<(), ProgramError> {
let authority_seeds = &[
POOL_STAKE_AUTHORITY_PREFIX,
pool_account_key.as_ref(),
&[bump_seed],
];
let signers = &[&authority_seeds[..]];
invoke_signed(
&stake::instruction::merge(destination_account.key, source_account.key, authority.key)
[0],
&[
destination_account,
source_account,
clock,
stake_history,
authority,
],
signers,
)
}
fn stake_split<'a>(
pool_account_key: &Pubkey,
stake_account: AccountInfo<'a>,
authority: AccountInfo<'a>,
bump_seed: u8,
amount: u64,
split_stake: AccountInfo<'a>,
) -> Result<(), ProgramError> {
let authority_seeds = &[
POOL_STAKE_AUTHORITY_PREFIX,
pool_account_key.as_ref(),
&[bump_seed],
];
let signers = &[&authority_seeds[..]];
let split_instruction =
stake::instruction::split(stake_account.key, authority.key, amount, split_stake.key);
invoke_signed(
split_instruction.last().unwrap(),
&[stake_account, split_stake, authority],
signers,
)
}
#[allow(clippy::too_many_arguments)]
fn stake_authorize<'a>(
pool_account_key: &Pubkey,
stake_account: AccountInfo<'a>,
stake_authority: AccountInfo<'a>,
bump_seed: u8,
new_stake_authority: &Pubkey,
clock: AccountInfo<'a>,
) -> Result<(), ProgramError> {
let authority_seeds = &[
POOL_STAKE_AUTHORITY_PREFIX,
pool_account_key.as_ref(),
&[bump_seed],
];
let signers = &[&authority_seeds[..]];
let authorize_instruction = stake::instruction::authorize(
stake_account.key,
stake_authority.key,
new_stake_authority,
stake::state::StakeAuthorize::Staker,
None,
);
invoke_signed(
&authorize_instruction,
&[
stake_account.clone(),
clock.clone(),
stake_authority.clone(),
],
signers,
)?;
let authorize_instruction = stake::instruction::authorize(
stake_account.key,
stake_authority.key,
new_stake_authority,
stake::state::StakeAuthorize::Withdrawer,
None,
);
invoke_signed(
&authorize_instruction,
&[stake_account, clock, stake_authority],
signers,
)
}
#[allow(clippy::too_many_arguments)]
fn stake_withdraw<'a>(
pool_account_key: &Pubkey,
stake_account: AccountInfo<'a>,
stake_authority: AccountInfo<'a>,
bump_seed: u8,
destination_account: AccountInfo<'a>,
clock: AccountInfo<'a>,
stake_history: AccountInfo<'a>,
lamports: u64,
) -> Result<(), ProgramError> {
let authority_seeds = &[
POOL_STAKE_AUTHORITY_PREFIX,
pool_account_key.as_ref(),
&[bump_seed],
];
let signers = &[&authority_seeds[..]];
let withdraw_instruction = stake::instruction::withdraw(
stake_account.key,
stake_authority.key,
destination_account.key,
lamports,
None,
);
invoke_signed(
&withdraw_instruction,
&[
stake_account,
destination_account,
clock,
stake_history,
stake_authority,
],
signers,
)
}
#[allow(clippy::too_many_arguments)]
fn token_mint_to<'a>(
pool_account_key: &Pubkey,
token_program: AccountInfo<'a>,
mint: AccountInfo<'a>,
destination: AccountInfo<'a>,
authority: AccountInfo<'a>,
bump_seed: u8,
amount: u64,
) -> Result<(), ProgramError> {
let authority_seeds = &[
POOL_MINT_AUTHORITY_PREFIX,
pool_account_key.as_ref(),
&[bump_seed],
];
let signers = &[&authority_seeds[..]];
let ix = spl_token::instruction::mint_to(
token_program.key,
mint.key,
destination.key,
authority.key,
&[],
amount,
)?;
invoke_signed(&ix, &[mint, destination, authority], signers)
}
#[allow(clippy::too_many_arguments)]
fn token_burn<'a>(
pool_account_key: &Pubkey,
token_program: AccountInfo<'a>,
burn_account: AccountInfo<'a>,
mint: AccountInfo<'a>,
authority: AccountInfo<'a>,
bump_seed: u8,
amount: u64,
) -> Result<(), ProgramError> {
let authority_seeds = &[
POOL_MINT_AUTHORITY_PREFIX,
pool_account_key.as_ref(),
&[bump_seed],
];
let signers = &[&authority_seeds[..]];
let ix = spl_token::instruction::burn(
token_program.key,
burn_account.key,
mint.key,
authority.key,
&[],
amount,
)?;
invoke_signed(&ix, &[burn_account, mint, authority], signers)
}
fn process_initialize_pool(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let vote_account_info = next_account_info(account_info_iter)?;
let pool_info = next_account_info(account_info_iter)?;
let pool_stake_info = next_account_info(account_info_iter)?;
let pool_mint_info = next_account_info(account_info_iter)?;
let pool_stake_authority_info = next_account_info(account_info_iter)?;
let pool_mint_authority_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
let rent = &Rent::from_account_info(rent_info)?;
let clock_info = next_account_info(account_info_iter)?;
let stake_history_info = next_account_info(account_info_iter)?;
let stake_config_info = next_account_info(account_info_iter)?;
let system_program_info = next_account_info(account_info_iter)?;
let token_program_info = next_account_info(account_info_iter)?;
let stake_program_info = next_account_info(account_info_iter)?;
check_vote_account(vote_account_info)?;
let pool_bump_seed = check_pool_address(program_id, vote_account_info.key, pool_info.key)?;
let stake_bump_seed =
check_pool_stake_address(program_id, pool_info.key, pool_stake_info.key)?;
let mint_bump_seed =
check_pool_mint_address(program_id, pool_info.key, pool_mint_info.key)?;
let stake_authority_bump_seed = check_pool_stake_authority_address(
program_id,
pool_info.key,
pool_stake_authority_info.key,
)?;
let mint_authority_bump_seed = check_pool_mint_authority_address(
program_id,
pool_info.key,
pool_mint_authority_info.key,
)?;
check_system_program(system_program_info.key)?;
check_token_program(token_program_info.key)?;
check_stake_program(stake_program_info.key)?;
let pool_seeds = &[
POOL_PREFIX,
vote_account_info.key.as_ref(),
&[pool_bump_seed],
];
let pool_signers = &[&pool_seeds[..]];
let stake_seeds = &[
POOL_STAKE_PREFIX,
pool_info.key.as_ref(),
&[stake_bump_seed],
];
let stake_signers = &[&stake_seeds[..]];
let mint_seeds = &[POOL_MINT_PREFIX, pool_info.key.as_ref(), &[mint_bump_seed]];
let mint_signers = &[&mint_seeds[..]];
let stake_authority_seeds = &[
POOL_STAKE_AUTHORITY_PREFIX,
pool_info.key.as_ref(),
&[stake_authority_bump_seed],
];
let stake_authority_signers = &[&stake_authority_seeds[..]];
let mint_authority_seeds = &[
POOL_MINT_AUTHORITY_PREFIX,
pool_info.key.as_ref(),
&[mint_authority_bump_seed],
];
let mint_authority_signers = &[&mint_authority_seeds[..]];
let pool_space = SinglePool::size_of();
if !rent.is_exempt(pool_info.lamports(), pool_space) {
return Err(SinglePoolError::WrongRentAmount.into());
}
if pool_info.data_len() != 0 {
return Err(SinglePoolError::PoolAlreadyInitialized.into());
}
invoke_signed(
&system_instruction::allocate(pool_info.key, pool_space as u64),
&[pool_info.clone()],
pool_signers,
)?;
invoke_signed(
&system_instruction::assign(pool_info.key, program_id),
&[pool_info.clone()],
pool_signers,
)?;
let mut pool = try_from_slice_unchecked::<SinglePool>(&pool_info.data.borrow())?;
pool.account_type = SinglePoolAccountType::Pool;
pool.vote_account_address = *vote_account_info.key;
borsh::to_writer(&mut pool_info.data.borrow_mut()[..], &pool)?;
let mint_space = spl_token::state::Mint::LEN;
invoke_signed(
&system_instruction::allocate(pool_mint_info.key, mint_space as u64),
&[pool_mint_info.clone()],
mint_signers,
)?;
invoke_signed(
&system_instruction::assign(pool_mint_info.key, token_program_info.key),
&[pool_mint_info.clone()],
mint_signers,
)?;
invoke_signed(
&spl_token::instruction::initialize_mint2(
token_program_info.key,
pool_mint_info.key,
pool_mint_authority_info.key,
None,
MINT_DECIMALS,
)?,
&[pool_mint_info.clone()],
mint_authority_signers,
)?;
let minimum_pool_balance = minimum_pool_balance()?;
let stake_space = StakeStateV2::size_of();
let stake_rent_plus_initial = rent
.minimum_balance(stake_space)
.saturating_add(minimum_pool_balance);
if pool_stake_info.lamports() < stake_rent_plus_initial {
return Err(SinglePoolError::WrongRentAmount.into());
}
let authorized = stake::state::Authorized::auto(pool_stake_authority_info.key);
invoke_signed(
&system_instruction::allocate(pool_stake_info.key, stake_space as u64),
&[pool_stake_info.clone()],
stake_signers,
)?;
invoke_signed(
&system_instruction::assign(pool_stake_info.key, stake_program_info.key),
&[pool_stake_info.clone()],
stake_signers,
)?;
invoke_signed(
&stake::instruction::initialize_checked(pool_stake_info.key, &authorized),
&[
pool_stake_info.clone(),
rent_info.clone(),
pool_stake_authority_info.clone(),
pool_stake_authority_info.clone(),
],
stake_authority_signers,
)?;
invoke_signed(
&stake::instruction::delegate_stake(
pool_stake_info.key,
pool_stake_authority_info.key,
vote_account_info.key,
),
&[
pool_stake_info.clone(),
vote_account_info.clone(),
clock_info.clone(),
stake_history_info.clone(),
stake_config_info.clone(),
pool_stake_authority_info.clone(),
],
stake_authority_signers,
)?;
Ok(())
}
fn process_replenish_pool(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let vote_account_info = next_account_info(account_info_iter)?;
let pool_info = next_account_info(account_info_iter)?;
let pool_stake_info = next_account_info(account_info_iter)?;
let pool_onramp_info = next_account_info(account_info_iter)?;
let pool_stake_authority_info = next_account_info(account_info_iter)?;
let clock_info = next_account_info(account_info_iter)?;
let clock = &Clock::from_account_info(clock_info)?;
let stake_history_info = next_account_info(account_info_iter)?;
let stake_config_info = next_account_info(account_info_iter)?;
let stake_program_info = next_account_info(account_info_iter)?;
let rent = Rent::get()?;
let stake_history = &StakeHistorySysvar(clock.epoch);
check_vote_account(vote_account_info)?;
check_pool_address(program_id, vote_account_info.key, pool_info.key)?;
SinglePool::from_account_info(pool_info, program_id)?;
check_pool_stake_address(program_id, pool_info.key, pool_stake_info.key)?;
check_pool_onramp_address(program_id, pool_info.key, pool_onramp_info.key)?;
let stake_authority_bump_seed = check_pool_stake_authority_address(
program_id,
pool_info.key,
pool_stake_authority_info.key,
)?;
check_stake_program(stake_program_info.key)?;
let minimum_delegation = stake::tools::get_minimum_delegation()?;
let pool_rent_exempt_reserve = rent.minimum_balance(pool_stake_info.data_len());
let onramp_rent_exempt_reserve = rent.minimum_balance(pool_onramp_info.data_len());
let (_, pool_stake_state) = get_stake_state(pool_stake_info)?;
let pool_stake_status = pool_stake_state
.delegation
.stake_activating_and_deactivating(
clock.epoch,
stake_history,
PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH,
);
let pool_stake_is_fully_active = is_stake_fully_active(&pool_stake_status);
let (option_onramp_status, onramp_deactivation_epoch) =
match deserialize_stake(pool_onramp_info) {
Ok(StakeStateV2::Initialized(_)) => (None, u64::MAX),
Ok(StakeStateV2::Stake(_, stake, _)) => (
Some(stake.delegation.stake_activating_and_deactivating(
clock.epoch,
stake_history,
PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH,
)),
stake.delegation.deactivation_epoch,
),
_ => return Err(SinglePoolError::OnRampDoesntExist.into()),
};
let stake_authority_seeds = &[
POOL_STAKE_AUTHORITY_PREFIX,
pool_info.key.as_ref(),
&[stake_authority_bump_seed],
];
let stake_authority_signers = &[&stake_authority_seeds[..]];
if pool_stake_state.delegation.deactivation_epoch == clock.epoch
|| (pool_stake_state.delegation.deactivation_epoch < clock.epoch
&& pool_stake_status.effective == 0)
{
invoke_signed(
&stake::instruction::delegate_stake(
pool_stake_info.key,
pool_stake_authority_info.key,
vote_account_info.key,
),
&[
pool_stake_info.clone(),
vote_account_info.clone(),
clock_info.clone(),
stake_history_info.clone(),
stake_config_info.clone(),
pool_stake_authority_info.clone(),
],
stake_authority_signers,
)?;
}
if pool_stake_is_fully_active {
let pool_excess_lamports = pool_stake_info
.lamports()
.saturating_sub(pool_stake_state.delegation.stake)
.saturating_sub(pool_rent_exempt_reserve);
if let Some(ref onramp_status) = option_onramp_status {
if is_stake_fully_active(onramp_status) {
invoke_signed(
&stake::instruction::move_stake(
pool_onramp_info.key,
pool_stake_info.key,
pool_stake_authority_info.key,
onramp_status.effective,
),
&[
pool_onramp_info.clone(),
pool_stake_info.clone(),
pool_stake_authority_info.clone(),
],
stake_authority_signers,
)?;
}
}
if pool_excess_lamports > 0 {
invoke_signed(
&stake::instruction::move_lamports(
pool_stake_info.key,
pool_onramp_info.key,
pool_stake_authority_info.key,
pool_excess_lamports,
),
&[
pool_stake_info.clone(),
pool_onramp_info.clone(),
pool_stake_authority_info.clone(),
],
stake_authority_signers,
)?;
}
let onramp_non_rent_lamports = pool_onramp_info
.lamports()
.saturating_sub(onramp_rent_exempt_reserve);
let must_delegate_onramp = match option_onramp_status.unwrap_or_default() {
StakeActivationStatus {
effective: 0,
activating,
deactivating: 0,
} if activating > 0 => {
onramp_non_rent_lamports >= minimum_delegation
&& onramp_non_rent_lamports > activating
}
StakeActivationStatus {
effective: _,
activating: 0,
deactivating,
} if deactivating == 0 || onramp_deactivation_epoch == clock.epoch => {
onramp_non_rent_lamports >= minimum_delegation
}
_ => false,
};
if must_delegate_onramp {
invoke_signed(
&stake::instruction::delegate_stake(
pool_onramp_info.key,
pool_stake_authority_info.key,
vote_account_info.key,
),
&[
pool_onramp_info.clone(),
vote_account_info.clone(),
clock_info.clone(),
stake_history_info.clone(),
stake_config_info.clone(),
pool_stake_authority_info.clone(),
],
stake_authority_signers,
)?;
}
}
Ok(())
}
fn process_deposit_stake(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let pool_info = next_account_info(account_info_iter)?;
let pool_stake_info = next_account_info(account_info_iter)?;
let pool_onramp_info = next_account_info(account_info_iter)?;
let pool_mint_info = next_account_info(account_info_iter)?;
let pool_stake_authority_info = next_account_info(account_info_iter)?;
let pool_mint_authority_info = next_account_info(account_info_iter)?;
let user_stake_info = next_account_info(account_info_iter)?;
let user_token_account_info = next_account_info(account_info_iter)?;
let user_lamport_account_info = next_account_info(account_info_iter)?;
let clock_info = next_account_info(account_info_iter)?;
let clock = &Clock::from_account_info(clock_info)?;
let stake_history_info = next_account_info(account_info_iter)?;
let token_program_info = next_account_info(account_info_iter)?;
let stake_program_info = next_account_info(account_info_iter)?;
let rent = &Rent::get()?;
let stake_history = &StakeHistorySysvar(clock.epoch);
SinglePool::from_account_info(pool_info, program_id)?;
check_pool_stake_address(program_id, pool_info.key, pool_stake_info.key)?;
check_pool_onramp_address(program_id, pool_info.key, pool_onramp_info.key)?;
let token_supply = check_pool_mint_with_supply(program_id, pool_info.key, pool_mint_info)?;
let stake_authority_bump_seed = check_pool_stake_authority_address(
program_id,
pool_info.key,
pool_stake_authority_info.key,
)?;
let mint_authority_bump_seed = check_pool_mint_authority_address(
program_id,
pool_info.key,
pool_mint_authority_info.key,
)?;
check_token_program(token_program_info.key)?;
check_stake_program(stake_program_info.key)?;
if pool_stake_info.key == user_stake_info.key {
return Err(SinglePoolError::InvalidPoolStakeAccountUsage.into());
}
if pool_onramp_info.key == user_stake_info.key {
return Err(SinglePoolError::InvalidPoolStakeAccountUsage.into());
}
let (pre_pool_stake, pool_is_active, pool_is_activating) = {
let (_, pool_stake_state) = get_stake_state(pool_stake_info)?;
let pool_stake_status = pool_stake_state
.delegation
.stake_activating_and_deactivating(
clock.epoch,
stake_history,
PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH,
);
(
pool_stake_state.delegation.stake,
is_stake_fully_active(&pool_stake_status),
is_stake_newly_activating(&pool_stake_status),
)
};
if !pool_is_active && !pool_is_activating {
return Err(SinglePoolError::ReplenishRequired.into());
} else if pool_is_active && pool_is_activating {
unreachable!();
};
let pre_total_nev = pool_net_asset_value(pool_stake_info, pool_onramp_info, rent);
let pre_user_lamports = user_stake_info.lamports();
let (user_stake_meta, user_stake_status) = match deserialize_stake(user_stake_info) {
Ok(StakeStateV2::Stake(meta, stake, _)) => (
meta,
stake.delegation.stake_activating_and_deactivating(
clock.epoch,
stake_history,
PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH,
),
),
Ok(StakeStateV2::Initialized(meta)) => (meta, StakeActivationStatus::default()),
_ => return Err(SinglePoolError::WrongStakeState.into()),
};
if user_stake_meta.authorized
!= stake::state::Authorized::auto(pool_stake_authority_info.key)
|| user_stake_meta.lockup.is_in_force(clock, None)
{
return Err(SinglePoolError::WrongStakeState.into());
}
if pool_is_active && is_stake_fully_active(&user_stake_status) {
} else if pool_is_activating && is_stake_newly_activating(&user_stake_status) {
} else if pool_is_activating && user_stake_status == StakeActivationStatus::default() {
} else {
return Err(SinglePoolError::WrongStakeState.into());
}
Self::stake_merge(
pool_info.key,
user_stake_info.clone(),
pool_stake_authority_info.clone(),
stake_authority_bump_seed,
pool_stake_info.clone(),
clock_info.clone(),
stake_history_info.clone(),
)?;
let post_pool_stake = get_stake_amount(pool_stake_info)?;
let new_stake_added = post_pool_stake
.checked_sub(pre_pool_stake)
.ok_or(SinglePoolError::ArithmeticOverflow)?;
let user_excess_lamports = pre_user_lamports
.checked_sub(new_stake_added)
.ok_or(SinglePoolError::ArithmeticOverflow)?;
if user_stake_info.lamports() != 0 {
return Err(SinglePoolError::UnexpectedMathError.into());
}
let new_pool_tokens =
calculate_deposit_amount(token_supply, pre_total_nev, new_stake_added)
.ok_or(SinglePoolError::UnexpectedMathError)?;
if new_pool_tokens == 0 {
return Err(SinglePoolError::DepositTooSmall.into());
}
Self::token_mint_to(
pool_info.key,
token_program_info.clone(),
pool_mint_info.clone(),
user_token_account_info.clone(),
pool_mint_authority_info.clone(),
mint_authority_bump_seed,
new_pool_tokens,
)?;
if user_excess_lamports > 0 {
Self::stake_withdraw(
pool_info.key,
pool_stake_info.clone(),
pool_stake_authority_info.clone(),
stake_authority_bump_seed,
user_lamport_account_info.clone(),
clock_info.clone(),
stake_history_info.clone(),
user_excess_lamports,
)?;
}
Ok(())
}
fn process_withdraw_stake(
program_id: &Pubkey,
accounts: &[AccountInfo],
user_stake_authority: &Pubkey,
token_amount: u64,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let pool_info = next_account_info(account_info_iter)?;
let pool_stake_info = next_account_info(account_info_iter)?;
let pool_onramp_info = next_account_info(account_info_iter)?;
let pool_mint_info = next_account_info(account_info_iter)?;
let pool_stake_authority_info = next_account_info(account_info_iter)?;
let pool_mint_authority_info = next_account_info(account_info_iter)?;
let user_stake_info = next_account_info(account_info_iter)?;
let user_token_account_info = next_account_info(account_info_iter)?;
let clock_info = next_account_info(account_info_iter)?;
let clock = &Clock::from_account_info(clock_info)?;
let token_program_info = next_account_info(account_info_iter)?;
let stake_program_info = next_account_info(account_info_iter)?;
let rent = &Rent::get()?;
let stake_history = &StakeHistorySysvar(clock.epoch);
SinglePool::from_account_info(pool_info, program_id)?;
check_pool_stake_address(program_id, pool_info.key, pool_stake_info.key)?;
check_pool_onramp_address(program_id, pool_info.key, pool_onramp_info.key)?;
let token_supply = check_pool_mint_with_supply(program_id, pool_info.key, pool_mint_info)?;
let stake_authority_bump_seed = check_pool_stake_authority_address(
program_id,
pool_info.key,
pool_stake_authority_info.key,
)?;
let mint_authority_bump_seed = check_pool_mint_authority_address(
program_id,
pool_info.key,
pool_mint_authority_info.key,
)?;
check_token_program(token_program_info.key)?;
check_stake_program(stake_program_info.key)?;
if pool_stake_info.key == user_stake_info.key {
return Err(SinglePoolError::InvalidPoolStakeAccountUsage.into());
}
if pool_onramp_info.key == user_stake_info.key {
return Err(SinglePoolError::InvalidPoolStakeAccountUsage.into());
}
if token_amount == 0 {
return Err(SinglePoolError::WithdrawalTooSmall.into());
}
let minimum_delegation = stake::tools::get_minimum_delegation()?;
let pre_total_nev = pool_net_asset_value(pool_stake_info, pool_onramp_info, rent);
let (withdrawable_value, pool_is_fully_inactive) = {
let (_, pool_stake_state) = get_stake_state(pool_stake_info)?;
let pool_stake_status = pool_stake_state
.delegation
.stake_activating_and_deactivating(
clock.epoch,
stake_history,
PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH,
);
if pool_stake_status == StakeActivationStatus::default() {
(
pool_stake_info
.lamports()
.saturating_sub(rent.minimum_balance(pool_stake_info.data_len())),
true,
)
} else {
(pool_stake_state.delegation.stake, false)
}
};
let stake_to_withdraw =
calculate_withdraw_amount(token_supply, pre_total_nev, token_amount)
.ok_or(SinglePoolError::UnexpectedMathError)?;
if stake_to_withdraw == 0 {
return Err(SinglePoolError::WithdrawalTooSmall.into());
}
if withdrawable_value.saturating_sub(stake_to_withdraw) < minimum_delegation {
return Err(SinglePoolError::WithdrawalViolatesPoolRequirements.into());
}
if stake_to_withdraw == pool_stake_info.lamports() {
return Err(SinglePoolError::WithdrawalViolatesPoolRequirements.into());
}
if !pool_is_fully_inactive && stake_to_withdraw < minimum_delegation {
return Err(SinglePoolError::WithdrawalTooSmall.into());
}
if stake_to_withdraw > withdrawable_value {
return Err(SinglePoolError::WithdrawalTooLarge.into());
}
Self::token_burn(
pool_info.key,
token_program_info.clone(),
user_token_account_info.clone(),
pool_mint_info.clone(),
pool_mint_authority_info.clone(),
mint_authority_bump_seed,
token_amount,
)?;
Self::stake_split(
pool_info.key,
pool_stake_info.clone(),
pool_stake_authority_info.clone(),
stake_authority_bump_seed,
stake_to_withdraw,
user_stake_info.clone(),
)?;
Self::stake_authorize(
pool_info.key,
user_stake_info.clone(),
pool_stake_authority_info.clone(),
stake_authority_bump_seed,
user_stake_authority,
clock_info.clone(),
)?;
Ok(())
}
fn process_create_pool_token_metadata(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let pool_info = next_account_info(account_info_iter)?;
let pool_mint_info = next_account_info(account_info_iter)?;
let pool_mint_authority_info = next_account_info(account_info_iter)?;
let pool_mpl_authority_info = next_account_info(account_info_iter)?;
let payer_info = next_account_info(account_info_iter)?;
let metadata_info = next_account_info(account_info_iter)?;
let mpl_token_metadata_program_info = next_account_info(account_info_iter)?;
let system_program_info = next_account_info(account_info_iter)?;
let pool = SinglePool::from_account_info(pool_info, program_id)?;
let mint_authority_bump_seed = check_pool_mint_authority_address(
program_id,
pool_info.key,
pool_mint_authority_info.key,
)?;
let mpl_authority_bump_seed = check_pool_mpl_authority_address(
program_id,
pool_info.key,
pool_mpl_authority_info.key,
)?;
check_pool_mint_address(program_id, pool_info.key, pool_mint_info.key)?;
check_system_program(system_program_info.key)?;
check_account_owner(payer_info, &system_program::id())?;
check_mpl_metadata_program(mpl_token_metadata_program_info.key)?;
check_mpl_metadata_account_address(metadata_info.key, pool_mint_info.key)?;
if !payer_info.is_signer {
msg!("Payer did not sign metadata creation");
return Err(SinglePoolError::SignatureMissing.into());
}
let vote_address_str = pool.vote_account_address.to_string();
let token_name = format!("SPL Single Pool {}", &vote_address_str[0..15]);
let token_symbol = format!("st{}", &vote_address_str[0..7]);
let new_metadata_instruction = create_metadata_accounts_v3(
*mpl_token_metadata_program_info.key,
*metadata_info.key,
*pool_mint_info.key,
*pool_mint_authority_info.key,
*payer_info.key,
*pool_mpl_authority_info.key,
token_name,
token_symbol,
"".to_string(),
);
let mint_authority_seeds = &[
POOL_MINT_AUTHORITY_PREFIX,
pool_info.key.as_ref(),
&[mint_authority_bump_seed],
];
let mpl_authority_seeds = &[
POOL_MPL_AUTHORITY_PREFIX,
pool_info.key.as_ref(),
&[mpl_authority_bump_seed],
];
let signers = &[&mint_authority_seeds[..], &mpl_authority_seeds[..]];
invoke_signed(
&new_metadata_instruction,
&[
metadata_info.clone(),
pool_mint_info.clone(),
pool_mint_authority_info.clone(),
payer_info.clone(),
pool_mpl_authority_info.clone(),
system_program_info.clone(),
],
signers,
)?;
Ok(())
}
fn process_update_pool_token_metadata(
program_id: &Pubkey,
accounts: &[AccountInfo],
name: String,
symbol: String,
uri: String,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let vote_account_info = next_account_info(account_info_iter)?;
let pool_info = next_account_info(account_info_iter)?;
let pool_mpl_authority_info = next_account_info(account_info_iter)?;
let authorized_withdrawer_info = next_account_info(account_info_iter)?;
let metadata_info = next_account_info(account_info_iter)?;
let mpl_token_metadata_program_info = next_account_info(account_info_iter)?;
check_vote_account(vote_account_info)?;
check_pool_address(program_id, vote_account_info.key, pool_info.key)?;
let pool = SinglePool::from_account_info(pool_info, program_id)?;
if pool.vote_account_address != *vote_account_info.key {
return Err(SinglePoolError::InvalidPoolAccount.into());
}
let mpl_authority_bump_seed = check_pool_mpl_authority_address(
program_id,
pool_info.key,
pool_mpl_authority_info.key,
)?;
let pool_mint_address = crate::find_pool_mint_address(program_id, pool_info.key);
check_mpl_metadata_program(mpl_token_metadata_program_info.key)?;
check_mpl_metadata_account_address(metadata_info.key, &pool_mint_address)?;
let vote_account_data = &vote_account_info.try_borrow_data()?;
let vote_account_withdrawer = vote_account_data
.get(VOTE_STATE_AUTHORIZED_WITHDRAWER_START..VOTE_STATE_AUTHORIZED_WITHDRAWER_END)
.and_then(|x| Pubkey::try_from(x).ok())
.ok_or(SinglePoolError::UnparseableVoteAccount)?;
if *authorized_withdrawer_info.key != vote_account_withdrawer {
msg!("Vote account authorized withdrawer does not match the account provided.");
return Err(SinglePoolError::InvalidMetadataSigner.into());
}
if !authorized_withdrawer_info.is_signer {
msg!("Vote account authorized withdrawer did not sign metadata update.");
return Err(SinglePoolError::SignatureMissing.into());
}
let update_metadata_accounts_instruction = update_metadata_accounts_v2(
*mpl_token_metadata_program_info.key,
*metadata_info.key,
*pool_mpl_authority_info.key,
None,
Some(DataV2 {
name,
symbol,
uri,
seller_fee_basis_points: 0,
creators: None,
collection: None,
uses: None,
}),
None,
Some(true),
);
let mpl_authority_seeds = &[
POOL_MPL_AUTHORITY_PREFIX,
pool_info.key.as_ref(),
&[mpl_authority_bump_seed],
];
let signers = &[&mpl_authority_seeds[..]];
invoke_signed(
&update_metadata_accounts_instruction,
&[metadata_info.clone(), pool_mpl_authority_info.clone()],
signers,
)?;
Ok(())
}
fn process_initialize_pool_onramp(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let pool_info = next_account_info(account_info_iter)?;
let pool_onramp_info = next_account_info(account_info_iter)?;
let pool_stake_authority_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
let rent = &Rent::from_account_info(rent_info)?;
let system_program_info = next_account_info(account_info_iter)?;
let stake_program_info = next_account_info(account_info_iter)?;
SinglePool::from_account_info(pool_info, program_id)?;
let onramp_bump_seed =
check_pool_onramp_address(program_id, pool_info.key, pool_onramp_info.key)?;
let stake_authority_bump_seed = check_pool_stake_authority_address(
program_id,
pool_info.key,
pool_stake_authority_info.key,
)?;
check_system_program(system_program_info.key)?;
check_stake_program(stake_program_info.key)?;
let onramp_seeds = &[
POOL_ONRAMP_PREFIX,
pool_info.key.as_ref(),
&[onramp_bump_seed],
];
let onramp_signers = &[&onramp_seeds[..]];
let stake_authority_seeds = &[
POOL_STAKE_AUTHORITY_PREFIX,
pool_info.key.as_ref(),
&[stake_authority_bump_seed],
];
let stake_authority_signers = &[&stake_authority_seeds[..]];
let stake_space = StakeStateV2::size_of();
let stake_rent = rent.minimum_balance(stake_space);
if pool_onramp_info.lamports() < stake_rent {
return Err(SinglePoolError::WrongRentAmount.into());
}
let authorized = stake::state::Authorized::auto(pool_stake_authority_info.key);
invoke_signed(
&system_instruction::allocate(pool_onramp_info.key, stake_space as u64),
&[pool_onramp_info.clone()],
onramp_signers,
)?;
invoke_signed(
&system_instruction::assign(pool_onramp_info.key, stake_program_info.key),
&[pool_onramp_info.clone()],
onramp_signers,
)?;
invoke_signed(
&stake::instruction::initialize_checked(pool_onramp_info.key, &authorized),
&[
pool_onramp_info.clone(),
rent_info.clone(),
pool_stake_authority_info.clone(),
pool_stake_authority_info.clone(),
],
stake_authority_signers,
)?;
Ok(())
}
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
let instruction = SinglePoolInstruction::try_from_slice(input)?;
match instruction {
SinglePoolInstruction::InitializePool => {
msg!("Instruction: InitializePool");
Self::process_initialize_pool(program_id, accounts)
}
SinglePoolInstruction::ReplenishPool => {
msg!("Instruction: ReplenishPool");
Self::process_replenish_pool(program_id, accounts)
}
SinglePoolInstruction::DepositStake => {
msg!("Instruction: DepositStake");
Self::process_deposit_stake(program_id, accounts)
}
SinglePoolInstruction::WithdrawStake {
user_stake_authority,
token_amount,
} => {
msg!("Instruction: WithdrawStake");
Self::process_withdraw_stake(
program_id,
accounts,
&user_stake_authority,
token_amount,
)
}
SinglePoolInstruction::CreateTokenMetadata => {
msg!("Instruction: CreateTokenMetadata");
Self::process_create_pool_token_metadata(program_id, accounts)
}
SinglePoolInstruction::UpdateTokenMetadata { name, symbol, uri } => {
msg!("Instruction: UpdateTokenMetadata");
Self::process_update_pool_token_metadata(program_id, accounts, name, symbol, uri)
}
SinglePoolInstruction::InitializePoolOnRamp => {
msg!("Instruction: InitializePoolOnRamp");
Self::process_initialize_pool_onramp(program_id, accounts)
}
SinglePoolInstruction::DepositSol { lamports: _ } => {
msg!("Instruction: DepositSol (NOT IMPLEMENTED)");
Err(ProgramError::InvalidInstructionData)
}
}
}
}
#[cfg(test)]
#[allow(clippy::arithmetic_side_effects)]
mod tests {
use {
super::*,
approx::assert_relative_eq,
rand::{
distr::{Distribution, Uniform},
rngs::StdRng,
seq::IteratorRandom,
Rng, SeedableRng,
},
std::collections::BTreeMap,
test_case::test_case,
};
const INFLATION_BASE_RATE: f64 = 0.0004;
#[derive(Clone, Debug, Default)]
struct PoolState {
pub token_supply: u64,
pub total_stake: u64,
pub user_token_balances: BTreeMap<Pubkey, u64>,
}
impl PoolState {
#[rustfmt::skip]
pub fn deposit(&mut self, user_pubkey: &Pubkey, stake_to_deposit: u64) -> Option<u64> {
calculate_deposit_amount(self.token_supply, self.total_stake, stake_to_deposit)
.and_then(|tokens_to_mint| self.token_supply.checked_add(tokens_to_mint)
.and_then(|new_token_supply| self.total_stake.checked_add(stake_to_deposit)
.and_then(|new_total_stake| self.user_token_balances.remove(user_pubkey).or(Some(0))
.and_then(|old_user_token_balance| old_user_token_balance.checked_add(tokens_to_mint)
.map(|new_user_token_balance| {
self.token_supply = new_token_supply;
self.total_stake = new_total_stake;
let _ = self.user_token_balances.insert(*user_pubkey, new_user_token_balance);
tokens_to_mint
})))))
}
#[rustfmt::skip]
pub fn withdraw(&mut self, user_pubkey: &Pubkey, tokens_to_burn: u64) -> Option<u64> {
calculate_withdraw_amount(self.token_supply, self.total_stake, tokens_to_burn)
.and_then(|stake_to_withdraw| self.token_supply.checked_sub(tokens_to_burn)
.and_then(|new_token_supply| self.total_stake.checked_sub(stake_to_withdraw)
.and_then(|new_total_stake| self.user_token_balances.remove(user_pubkey)
.and_then(|old_user_token_balance| old_user_token_balance.checked_sub(tokens_to_burn)
.map(|new_user_token_balance| {
self.token_supply = new_token_supply;
self.total_stake = new_total_stake;
let _ = self.user_token_balances.insert(*user_pubkey, new_user_token_balance);
stake_to_withdraw
})))))
}
pub fn reward(&mut self, reward_amount: u64) {
self.total_stake = self.total_stake.checked_add(reward_amount).unwrap();
}
pub fn tokens(&self, user_pubkey: &Pubkey) -> u64 {
*self.user_token_balances.get(user_pubkey).unwrap_or(&0)
}
pub fn stake(&self, user_pubkey: &Pubkey) -> u64 {
let tokens = self.tokens(user_pubkey);
if tokens > 0 {
u64::try_from(tokens as u128 * self.total_stake as u128 / self.token_supply as u128)
.unwrap()
} else {
0
}
}
pub fn share(&self, user_pubkey: &Pubkey) -> f64 {
let tokens = self.tokens(user_pubkey);
if tokens > 0 {
tokens as f64 / self.token_supply as f64
} else {
0.0
}
}
}
#[test]
fn simple_deposit_withdraw() {
let mut pool = PoolState::default();
let alice = Pubkey::new_unique();
let bob = Pubkey::new_unique();
let chad = Pubkey::new_unique();
pool.deposit(&alice, 250).unwrap();
assert_eq!(pool.tokens(&alice), 250);
assert_eq!(pool.token_supply, 250);
assert_eq!(pool.total_stake, 250);
pool.deposit(&bob, 750).unwrap();
assert_eq!(pool.tokens(&bob), 750);
assert_eq!(pool.token_supply, 1000);
assert_eq!(pool.total_stake, 1000);
assert_relative_eq!(pool.share(&alice), 0.25);
assert_relative_eq!(pool.share(&bob), 0.75);
pool.reward(1000);
assert_eq!(pool.stake(&alice), pool.tokens(&alice) * 2);
assert_eq!(pool.stake(&bob), pool.tokens(&bob) * 2);
assert_relative_eq!(pool.share(&alice), 0.25);
assert_relative_eq!(pool.share(&bob), 0.75);
let stake_removed = pool.withdraw(&alice, 125).unwrap();
pool.deposit(&chad, 250).unwrap();
assert_eq!(stake_removed, 250);
assert_relative_eq!(pool.share(&alice), 0.125);
assert_relative_eq!(pool.share(&bob), 0.75);
let stake_removed = pool.withdraw(&bob, 750).unwrap();
assert_eq!(stake_removed, 1500);
assert_relative_eq!(pool.share(&bob), 0.0);
pool.withdraw(&chad, 125).unwrap();
assert_relative_eq!(pool.share(&alice), 1.0);
}
#[test_case(rand::random(), false, false; "no_rewards")]
#[test_case(rand::random(), true, false; "with_rewards")]
#[test_case(rand::random(), true, true; "no_minimum")]
fn random_deposit_withdraw(seed: u64, with_rewards: bool, no_minimum: bool) {
println!(
"TEST SEED: {seed}. edit the test case to pass this value if needed to debug failures",
);
let mut prng = rand::rngs::StdRng::seed_from_u64(seed);
let deposit_range = Uniform::try_from(LAMPORTS_PER_SOL..LAMPORTS_PER_SOL * 1000).unwrap();
let minnow_range = Uniform::try_from(1..LAMPORTS_PER_SOL).unwrap();
let op_range = Uniform::try_from(if with_rewards { 0.0..1.0 } else { 0.0..0.65 }).unwrap();
let std_range = Uniform::try_from(0.0..1.0).unwrap();
let deposit_amount = |prng: &mut StdRng| {
if no_minimum && prng.random_bool(0.2) {
minnow_range.sample(prng)
} else {
deposit_range.sample(prng)
}
};
for _ in 0..100 {
let mut pool = PoolState::default();
let mut users = vec![];
let user_count: usize = prng.random_range(1..=100);
for _ in 0..user_count {
let user = Pubkey::new_unique();
if prng.random_bool(0.5) {
pool.deposit(&user, deposit_amount(&mut prng)).unwrap();
}
users.push(user);
}
for _ in 0..1000 {
match op_range.sample(&mut prng) {
n if n <= 0.35 => {
let user = users.iter().choose(&mut prng).unwrap();
let prev_share = pool.share(user);
let prev_stake = pool.stake(user);
let prev_token_supply = pool.token_supply;
let prev_total_stake = pool.total_stake;
let stake_deposited = deposit_amount(&mut prng);
let tokens_minted = pool.deposit(user, stake_deposited).unwrap();
assert_eq!(pool.total_stake - prev_total_stake, stake_deposited);
assert!(
(pool.stake(user) as i64 - prev_stake as i64 - stake_deposited as i64)
.abs()
<= 2
);
assert_eq!(pool.token_supply - prev_token_supply, tokens_minted);
if prev_total_stake > 0 {
assert_relative_eq!(
pool.share(user) - prev_share,
pool.stake(user) as f64 / pool.total_stake as f64
- prev_stake as f64 / prev_total_stake as f64,
epsilon = 1e-6
);
}
}
n if n > 0.35 && n <= 0.65 => {
if let Some(user) = users
.iter()
.filter(|user| pool.tokens(user) > 0)
.choose(&mut prng)
{
let prev_tokens = pool.tokens(user);
let prev_share = pool.share(user);
let prev_stake = pool.stake(user);
let prev_token_supply = pool.token_supply;
let prev_total_stake = pool.total_stake;
let tokens_burned = if std_range.sample(&mut prng) <= 0.1 {
prev_tokens
} else {
prng.random_range(0..prev_tokens)
};
let stake_received = pool.withdraw(user, tokens_burned).unwrap();
assert_eq!(prev_total_stake - pool.total_stake, stake_received);
assert!(
(prev_stake as i64
- pool.stake(user) as i64
- stake_received as i64)
.abs()
<= 2
);
assert_eq!(prev_token_supply - pool.token_supply, tokens_burned);
if pool.total_stake > 0 {
assert_relative_eq!(
prev_share - pool.share(user),
prev_stake as f64 / prev_total_stake as f64
- pool.stake(user) as f64 / pool.total_stake as f64,
epsilon = 1e-6
);
}
};
}
_ => {
assert!(with_rewards);
let prev_shares_stakes = users
.iter()
.map(|user| (user, pool.share(user), pool.stake(user)))
.filter(|(_, _, stake)| stake > &0)
.collect::<Vec<_>>();
pool.reward((pool.total_stake as f64 * INFLATION_BASE_RATE) as u64);
for (user, prev_share, prev_stake) in prev_shares_stakes {
assert_eq!(pool.share(user), prev_share);
let curr_stake = pool.stake(user);
let stake_share = prev_stake as f64 * INFLATION_BASE_RATE;
let stake_diff = (curr_stake - prev_stake) as f64;
assert!((stake_share - stake_diff).abs() <= 2.0);
}
}
}
}
}
}
}