diff --git a/stake-pool/program/src/error.rs b/stake-pool/program/src/error.rs index 6efcb389554..39fae32961a 100644 --- a/stake-pool/program/src/error.rs +++ b/stake-pool/program/src/error.rs @@ -141,6 +141,9 @@ pub enum StakePoolError { /// The fee account has an unsupported extension #[error("UnsupportedFeeAccountExtension")] UnsupportedFeeAccountExtension, + /// Instruction exceeds desired slippage limit + #[error("Instruction exceeds desired slippage limit")] + ExceededSlippage, } impl From for ProgramError { fn from(e: StakePoolError) -> Self { diff --git a/stake-pool/program/src/instruction.rs b/stake-pool/program/src/instruction.rs index 9f2d36c7db7..4e60467e47d 100644 --- a/stake-pool/program/src/instruction.rs +++ b/stake-pool/program/src/instruction.rs @@ -524,6 +524,105 @@ pub enum StakePoolInstruction { #[allow(dead_code)] // but it's not destination_transient_stake_seed: u64, }, + + /// Deposit some stake into the pool, with a specified slippage constraint. + /// The output is a "pool" token representing ownership into the pool. + /// Inputs are converted at the current ratio. + /// + /// 0. `[w]` Stake pool + /// 1. `[w]` Validator stake list storage account + /// 2. `[s]/[]` Stake pool deposit authority + /// 3. `[]` Stake pool withdraw authority + /// 4. `[w]` Stake account to join the pool (withdraw authority for the stake account should be first set to the stake pool deposit authority) + /// 5. `[w]` Validator stake account for the stake account to be merged with + /// 6. `[w]` Reserve stake account, to withdraw rent exempt reserve + /// 7. `[w]` User account to receive pool tokens + /// 8. `[w]` Account to receive pool fee tokens + /// 9. `[w]` Account to receive a portion of pool fee tokens as referral fees + /// 10. `[w]` Pool token mint account + /// 11. '[]' Sysvar clock account + /// 12. '[]' Sysvar stake history account + /// 13. `[]` Pool token program id, + /// 14. `[]` Stake program id, + DepositStakeWithSlippage { + /// Minimum amount of pool tokens that must be received + minimum_pool_tokens_out: u64, + }, + + /// Withdraw the token from the pool at the current ratio, specifying a + /// minimum expected output lamport amount. + /// + /// Succeeds if the stake account has enough SOL to cover the desired amount + /// of pool tokens, and if the withdrawal keeps the total staked amount + /// above the minimum of rent-exempt amount + + /// `max(crate::MINIMUM_ACTIVE_STAKE, solana_program::stake::tools::get_minimum_delegation())`. + /// + /// 0. `[w]` Stake pool + /// 1. `[w]` Validator stake list storage account + /// 2. `[]` Stake pool withdraw authority + /// 3. `[w]` Validator or reserve stake account to split + /// 4. `[w]` Unitialized stake account to receive withdrawal + /// 5. `[]` User account to set as a new withdraw authority + /// 6. `[s]` User transfer authority, for pool token account + /// 7. `[w]` User account with pool tokens to burn from + /// 8. `[w]` Account to receive pool fee tokens + /// 9. `[w]` Pool token mint account + /// 10. `[]` Sysvar clock account (required) + /// 11. `[]` Pool token program id + /// 12. `[]` Stake program id, + /// userdata: amount of pool tokens to withdraw + WithdrawStakeWithSlippage { + /// Pool tokens to burn in exchange for lamports + pool_tokens_in: u64, + /// Minimum amount of lamports that must be received + minimum_lamports_out: u64, + }, + + /// Deposit SOL directly into the pool's reserve account, with a specified + /// slippage constraint. The output is a "pool" token representing ownership + /// into the pool. Inputs are converted at the current ratio. + /// + /// 0. `[w]` Stake pool + /// 1. `[]` Stake pool withdraw authority + /// 2. `[w]` Reserve stake account, to deposit SOL + /// 3. `[s]` Account providing the lamports to be deposited into the pool + /// 4. `[w]` User account to receive pool tokens + /// 5. `[w]` Account to receive fee tokens + /// 6. `[w]` Account to receive a portion of fee as referral fees + /// 7. `[w]` Pool token mint account + /// 8. `[]` System program account + /// 9. `[]` Token program id + /// 10. `[s]` (Optional) Stake pool sol deposit authority. + DepositSolWithSlippage { + /// Amount of lamports to deposit into the reserve + lamports_in: u64, + /// Minimum amount of pool tokens that must be received + minimum_pool_tokens_out: u64, + }, + + /// Withdraw SOL directly from the pool's reserve account. Fails if the + /// reserve does not have enough SOL or if the slippage constraint is not + /// met. + /// + /// 0. `[w]` Stake pool + /// 1. `[]` Stake pool withdraw authority + /// 2. `[s]` User transfer authority, for pool token account + /// 3. `[w]` User account to burn pool tokens + /// 4. `[w]` Reserve stake account, to withdraw SOL + /// 5. `[w]` Account receiving the lamports from the reserve, must be a system account + /// 6. `[w]` Account to receive pool fee tokens + /// 7. `[w]` Pool token mint account + /// 8. '[]' Clock sysvar + /// 9. '[]' Stake history sysvar + /// 10. `[]` Stake program account + /// 11. `[]` Token program id + /// 12. `[s]` (Optional) Stake pool sol withdraw authority + WithdrawSolWithSlippage { + /// Pool tokens to burn in exchange for lamports + pool_tokens_in: u64, + /// Minimum amount of lamports that must be received + minimum_lamports_out: u64, + }, } /// Creates an 'initialize' instruction. @@ -1274,12 +1373,11 @@ pub fn update_stake_pool( (update_list_instructions, final_instructions) } -/// Creates instructions required to deposit into a stake pool, given a stake -/// account owned by the user. -pub fn deposit_stake( +fn deposit_stake_internal( program_id: &Pubkey, stake_pool: &Pubkey, validator_list_storage: &Pubkey, + stake_pool_deposit_authority: Option<&Pubkey>, stake_pool_withdraw_authority: &Pubkey, deposit_stake_address: &Pubkey, deposit_stake_withdraw_authority: &Pubkey, @@ -1290,13 +1388,60 @@ pub fn deposit_stake( referrer_pool_tokens_account: &Pubkey, pool_mint: &Pubkey, token_program_id: &Pubkey, + minimum_pool_tokens_out: Option, ) -> Vec { - let stake_pool_deposit_authority = - find_deposit_authority_program_address(program_id, stake_pool).0; - let accounts = vec![ + let mut instructions = vec![]; + let mut accounts = vec![ AccountMeta::new(*stake_pool, false), AccountMeta::new(*validator_list_storage, false), - AccountMeta::new_readonly(stake_pool_deposit_authority, false), + ]; + if let Some(stake_pool_deposit_authority) = stake_pool_deposit_authority { + accounts.push(AccountMeta::new_readonly( + *stake_pool_deposit_authority, + true, + )); + instructions.extend_from_slice(&[ + stake::instruction::authorize( + deposit_stake_address, + deposit_stake_withdraw_authority, + stake_pool_deposit_authority, + stake::state::StakeAuthorize::Staker, + None, + ), + stake::instruction::authorize( + deposit_stake_address, + deposit_stake_withdraw_authority, + stake_pool_deposit_authority, + stake::state::StakeAuthorize::Withdrawer, + None, + ), + ]); + } else { + let stake_pool_deposit_authority = + find_deposit_authority_program_address(program_id, stake_pool).0; + accounts.push(AccountMeta::new_readonly( + stake_pool_deposit_authority, + false, + )); + instructions.extend_from_slice(&[ + stake::instruction::authorize( + deposit_stake_address, + deposit_stake_withdraw_authority, + &stake_pool_deposit_authority, + stake::state::StakeAuthorize::Staker, + None, + ), + stake::instruction::authorize( + deposit_stake_address, + deposit_stake_withdraw_authority, + &stake_pool_deposit_authority, + stake::state::StakeAuthorize::Withdrawer, + None, + ), + ]); + }; + + accounts.extend_from_slice(&[ AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), AccountMeta::new(*deposit_stake_address, false), AccountMeta::new(*validator_stake_account, false), @@ -1309,28 +1454,99 @@ pub fn deposit_stake( AccountMeta::new_readonly(sysvar::stake_history::id(), false), AccountMeta::new_readonly(*token_program_id, false), AccountMeta::new_readonly(stake::program::id(), false), - ]; - vec![ - stake::instruction::authorize( - deposit_stake_address, - deposit_stake_withdraw_authority, - &stake_pool_deposit_authority, - stake::state::StakeAuthorize::Staker, - None, - ), - stake::instruction::authorize( - deposit_stake_address, - deposit_stake_withdraw_authority, - &stake_pool_deposit_authority, - stake::state::StakeAuthorize::Withdrawer, - None, - ), - Instruction { - program_id: *program_id, - accounts, - data: StakePoolInstruction::DepositStake.try_to_vec().unwrap(), + ]); + instructions.push( + if let Some(minimum_pool_tokens_out) = minimum_pool_tokens_out { + Instruction { + program_id: *program_id, + accounts, + data: StakePoolInstruction::DepositStakeWithSlippage { + minimum_pool_tokens_out, + } + .try_to_vec() + .unwrap(), + } + } else { + Instruction { + program_id: *program_id, + accounts, + data: StakePoolInstruction::DepositStake.try_to_vec().unwrap(), + } }, - ] + ); + instructions +} + +/// Creates instructions required to deposit into a stake pool, given a stake +/// account owned by the user. +pub fn deposit_stake( + program_id: &Pubkey, + stake_pool: &Pubkey, + validator_list_storage: &Pubkey, + stake_pool_withdraw_authority: &Pubkey, + deposit_stake_address: &Pubkey, + deposit_stake_withdraw_authority: &Pubkey, + validator_stake_account: &Pubkey, + reserve_stake_account: &Pubkey, + pool_tokens_to: &Pubkey, + manager_fee_account: &Pubkey, + referrer_pool_tokens_account: &Pubkey, + pool_mint: &Pubkey, + token_program_id: &Pubkey, +) -> Vec { + deposit_stake_internal( + program_id, + stake_pool, + validator_list_storage, + None, + stake_pool_withdraw_authority, + deposit_stake_address, + deposit_stake_withdraw_authority, + validator_stake_account, + reserve_stake_account, + pool_tokens_to, + manager_fee_account, + referrer_pool_tokens_account, + pool_mint, + token_program_id, + None, + ) +} + +/// Creates instructions to deposit into a stake pool with slippage +pub fn deposit_stake_with_slippage( + program_id: &Pubkey, + stake_pool: &Pubkey, + validator_list_storage: &Pubkey, + stake_pool_withdraw_authority: &Pubkey, + deposit_stake_address: &Pubkey, + deposit_stake_withdraw_authority: &Pubkey, + validator_stake_account: &Pubkey, + reserve_stake_account: &Pubkey, + pool_tokens_to: &Pubkey, + manager_fee_account: &Pubkey, + referrer_pool_tokens_account: &Pubkey, + pool_mint: &Pubkey, + token_program_id: &Pubkey, + minimum_pool_tokens_out: u64, +) -> Vec { + deposit_stake_internal( + program_id, + stake_pool, + validator_list_storage, + None, + stake_pool_withdraw_authority, + deposit_stake_address, + deposit_stake_withdraw_authority, + validator_stake_account, + reserve_stake_account, + pool_tokens_to, + manager_fee_account, + referrer_pool_tokens_account, + pool_mint, + token_program_id, + Some(minimum_pool_tokens_out), + ) } /// Creates instructions required to deposit into a stake pool, given a stake @@ -1352,48 +1568,66 @@ pub fn deposit_stake_with_authority( pool_mint: &Pubkey, token_program_id: &Pubkey, ) -> Vec { - let accounts = vec![ - AccountMeta::new(*stake_pool, false), - AccountMeta::new(*validator_list_storage, false), - AccountMeta::new_readonly(*stake_pool_deposit_authority, true), - AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), - AccountMeta::new(*deposit_stake_address, false), - AccountMeta::new(*validator_stake_account, false), - AccountMeta::new(*reserve_stake_account, false), - AccountMeta::new(*pool_tokens_to, false), - AccountMeta::new(*manager_fee_account, false), - AccountMeta::new(*referrer_pool_tokens_account, false), - AccountMeta::new(*pool_mint, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::stake_history::id(), false), - AccountMeta::new_readonly(*token_program_id, false), - AccountMeta::new_readonly(stake::program::id(), false), - ]; - vec![ - stake::instruction::authorize( - deposit_stake_address, - deposit_stake_withdraw_authority, - stake_pool_deposit_authority, - stake::state::StakeAuthorize::Staker, - None, - ), - stake::instruction::authorize( - deposit_stake_address, - deposit_stake_withdraw_authority, - stake_pool_deposit_authority, - stake::state::StakeAuthorize::Withdrawer, - None, - ), - Instruction { - program_id: *program_id, - accounts, - data: StakePoolInstruction::DepositStake.try_to_vec().unwrap(), - }, - ] + deposit_stake_internal( + program_id, + stake_pool, + validator_list_storage, + Some(stake_pool_deposit_authority), + stake_pool_withdraw_authority, + deposit_stake_address, + deposit_stake_withdraw_authority, + validator_stake_account, + reserve_stake_account, + pool_tokens_to, + manager_fee_account, + referrer_pool_tokens_account, + pool_mint, + token_program_id, + None, + ) +} + +/// Creates instructions required to deposit into a stake pool with slippage, given +/// a stake account owned by the user. The difference with `deposit()` is that a deposit +/// authority must sign this instruction, which is required for private pools. +pub fn deposit_stake_with_authority_and_slippage( + program_id: &Pubkey, + stake_pool: &Pubkey, + validator_list_storage: &Pubkey, + stake_pool_deposit_authority: &Pubkey, + stake_pool_withdraw_authority: &Pubkey, + deposit_stake_address: &Pubkey, + deposit_stake_withdraw_authority: &Pubkey, + validator_stake_account: &Pubkey, + reserve_stake_account: &Pubkey, + pool_tokens_to: &Pubkey, + manager_fee_account: &Pubkey, + referrer_pool_tokens_account: &Pubkey, + pool_mint: &Pubkey, + token_program_id: &Pubkey, + minimum_pool_tokens_out: u64, +) -> Vec { + deposit_stake_internal( + program_id, + stake_pool, + validator_list_storage, + Some(stake_pool_deposit_authority), + stake_pool_withdraw_authority, + deposit_stake_address, + deposit_stake_withdraw_authority, + validator_stake_account, + reserve_stake_account, + pool_tokens_to, + manager_fee_account, + referrer_pool_tokens_account, + pool_mint, + token_program_id, + Some(minimum_pool_tokens_out), + ) } /// Creates instructions required to deposit SOL directly into a stake pool. -pub fn deposit_sol( +fn deposit_sol_internal( program_id: &Pubkey, stake_pool: &Pubkey, stake_pool_withdraw_authority: &Pubkey, @@ -1404,9 +1638,11 @@ pub fn deposit_sol( referrer_pool_tokens_account: &Pubkey, pool_mint: &Pubkey, token_program_id: &Pubkey, - amount: u64, + sol_deposit_authority: Option<&Pubkey>, + lamports_in: u64, + minimum_pool_tokens_out: Option, ) -> Instruction { - let accounts = vec![ + let mut accounts = vec![ AccountMeta::new(*stake_pool, false), AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), AccountMeta::new(*reserve_stake_account, false), @@ -1418,15 +1654,94 @@ pub fn deposit_sol( AccountMeta::new_readonly(system_program::id(), false), AccountMeta::new_readonly(*token_program_id, false), ]; - Instruction { - program_id: *program_id, - accounts, - data: StakePoolInstruction::DepositSol(amount) + if let Some(sol_deposit_authority) = sol_deposit_authority { + accounts.push(AccountMeta::new_readonly(*sol_deposit_authority, true)); + } + if let Some(minimum_pool_tokens_out) = minimum_pool_tokens_out { + Instruction { + program_id: *program_id, + accounts, + data: StakePoolInstruction::DepositSolWithSlippage { + lamports_in, + minimum_pool_tokens_out, + } .try_to_vec() .unwrap(), + } + } else { + Instruction { + program_id: *program_id, + accounts, + data: StakePoolInstruction::DepositSol(lamports_in) + .try_to_vec() + .unwrap(), + } } } +/// Creates instruction to deposit SOL directly into a stake pool. +pub fn deposit_sol( + program_id: &Pubkey, + stake_pool: &Pubkey, + stake_pool_withdraw_authority: &Pubkey, + reserve_stake_account: &Pubkey, + lamports_from: &Pubkey, + pool_tokens_to: &Pubkey, + manager_fee_account: &Pubkey, + referrer_pool_tokens_account: &Pubkey, + pool_mint: &Pubkey, + token_program_id: &Pubkey, + lamports_in: u64, +) -> Instruction { + deposit_sol_internal( + program_id, + stake_pool, + stake_pool_withdraw_authority, + reserve_stake_account, + lamports_from, + pool_tokens_to, + manager_fee_account, + referrer_pool_tokens_account, + pool_mint, + token_program_id, + None, + lamports_in, + None, + ) +} + +/// Creates instruction to deposit SOL directly into a stake pool with slippage constraint. +pub fn deposit_sol_with_slippage( + program_id: &Pubkey, + stake_pool: &Pubkey, + stake_pool_withdraw_authority: &Pubkey, + reserve_stake_account: &Pubkey, + lamports_from: &Pubkey, + pool_tokens_to: &Pubkey, + manager_fee_account: &Pubkey, + referrer_pool_tokens_account: &Pubkey, + pool_mint: &Pubkey, + token_program_id: &Pubkey, + lamports_in: u64, + minimum_pool_tokens_out: u64, +) -> Instruction { + deposit_sol_internal( + program_id, + stake_pool, + stake_pool_withdraw_authority, + reserve_stake_account, + lamports_from, + pool_tokens_to, + manager_fee_account, + referrer_pool_tokens_account, + pool_mint, + token_program_id, + None, + lamports_in, + Some(minimum_pool_tokens_out), + ) +} + /// Creates instruction required to deposit SOL directly into a stake pool. /// The difference with `deposit_sol()` is that a deposit /// authority must sign this instruction. @@ -1442,32 +1757,59 @@ pub fn deposit_sol_with_authority( referrer_pool_tokens_account: &Pubkey, pool_mint: &Pubkey, token_program_id: &Pubkey, - amount: u64, + lamports_in: u64, ) -> Instruction { - let accounts = vec![ - AccountMeta::new(*stake_pool, false), - AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), - AccountMeta::new(*reserve_stake_account, false), - AccountMeta::new(*lamports_from, true), - AccountMeta::new(*pool_tokens_to, false), - AccountMeta::new(*manager_fee_account, false), - AccountMeta::new(*referrer_pool_tokens_account, false), - AccountMeta::new(*pool_mint, false), - AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(*token_program_id, false), - AccountMeta::new_readonly(*sol_deposit_authority, true), - ]; - Instruction { - program_id: *program_id, - accounts, - data: StakePoolInstruction::DepositSol(amount) - .try_to_vec() - .unwrap(), - } + deposit_sol_internal( + program_id, + stake_pool, + stake_pool_withdraw_authority, + reserve_stake_account, + lamports_from, + pool_tokens_to, + manager_fee_account, + referrer_pool_tokens_account, + pool_mint, + token_program_id, + Some(sol_deposit_authority), + lamports_in, + None, + ) } -/// Creates a 'WithdrawStake' instruction. -pub fn withdraw_stake( +/// Creates instruction to deposit SOL directly into a stake pool with slippage constraint. +pub fn deposit_sol_with_authority_and_slippage( + program_id: &Pubkey, + stake_pool: &Pubkey, + sol_deposit_authority: &Pubkey, + stake_pool_withdraw_authority: &Pubkey, + reserve_stake_account: &Pubkey, + lamports_from: &Pubkey, + pool_tokens_to: &Pubkey, + manager_fee_account: &Pubkey, + referrer_pool_tokens_account: &Pubkey, + pool_mint: &Pubkey, + token_program_id: &Pubkey, + lamports_in: u64, + minimum_pool_tokens_out: u64, +) -> Instruction { + deposit_sol_internal( + program_id, + stake_pool, + stake_pool_withdraw_authority, + reserve_stake_account, + lamports_from, + pool_tokens_to, + manager_fee_account, + referrer_pool_tokens_account, + pool_mint, + token_program_id, + Some(sol_deposit_authority), + lamports_in, + Some(minimum_pool_tokens_out), + ) +} + +fn withdraw_stake_internal( program_id: &Pubkey, stake_pool: &Pubkey, validator_list_storage: &Pubkey, @@ -1480,7 +1822,8 @@ pub fn withdraw_stake( manager_fee_account: &Pubkey, pool_mint: &Pubkey, token_program_id: &Pubkey, - amount: u64, + pool_tokens_in: u64, + minimum_lamports_out: Option, ) -> Instruction { let accounts = vec![ AccountMeta::new(*stake_pool, false), @@ -1497,17 +1840,98 @@ pub fn withdraw_stake( AccountMeta::new_readonly(*token_program_id, false), AccountMeta::new_readonly(stake::program::id(), false), ]; - Instruction { - program_id: *program_id, - accounts, - data: StakePoolInstruction::WithdrawStake(amount) + if let Some(minimum_lamports_out) = minimum_lamports_out { + Instruction { + program_id: *program_id, + accounts, + data: StakePoolInstruction::WithdrawStakeWithSlippage { + pool_tokens_in, + minimum_lamports_out, + } .try_to_vec() .unwrap(), + } + } else { + Instruction { + program_id: *program_id, + accounts, + data: StakePoolInstruction::WithdrawStake(pool_tokens_in) + .try_to_vec() + .unwrap(), + } } } -/// Creates instruction required to withdraw SOL directly from a stake pool. -pub fn withdraw_sol( +/// Creates a 'WithdrawStake' instruction. +pub fn withdraw_stake( + program_id: &Pubkey, + stake_pool: &Pubkey, + validator_list_storage: &Pubkey, + stake_pool_withdraw: &Pubkey, + stake_to_split: &Pubkey, + stake_to_receive: &Pubkey, + user_stake_authority: &Pubkey, + user_transfer_authority: &Pubkey, + user_pool_token_account: &Pubkey, + manager_fee_account: &Pubkey, + pool_mint: &Pubkey, + token_program_id: &Pubkey, + pool_tokens_in: u64, +) -> Instruction { + withdraw_stake_internal( + program_id, + stake_pool, + validator_list_storage, + stake_pool_withdraw, + stake_to_split, + stake_to_receive, + user_stake_authority, + user_transfer_authority, + user_pool_token_account, + manager_fee_account, + pool_mint, + token_program_id, + pool_tokens_in, + None, + ) +} + +/// Creates a 'WithdrawStakeWithSlippage' instruction. +pub fn withdraw_stake_with_slippage( + program_id: &Pubkey, + stake_pool: &Pubkey, + validator_list_storage: &Pubkey, + stake_pool_withdraw: &Pubkey, + stake_to_split: &Pubkey, + stake_to_receive: &Pubkey, + user_stake_authority: &Pubkey, + user_transfer_authority: &Pubkey, + user_pool_token_account: &Pubkey, + manager_fee_account: &Pubkey, + pool_mint: &Pubkey, + token_program_id: &Pubkey, + pool_tokens_in: u64, + minimum_lamports_out: u64, +) -> Instruction { + withdraw_stake_internal( + program_id, + stake_pool, + validator_list_storage, + stake_pool_withdraw, + stake_to_split, + stake_to_receive, + user_stake_authority, + user_transfer_authority, + user_pool_token_account, + manager_fee_account, + pool_mint, + token_program_id, + pool_tokens_in, + Some(minimum_lamports_out), + ) +} + +fn withdraw_sol_internal( program_id: &Pubkey, stake_pool: &Pubkey, stake_pool_withdraw_authority: &Pubkey, @@ -1518,9 +1942,11 @@ pub fn withdraw_sol( manager_fee_account: &Pubkey, pool_mint: &Pubkey, token_program_id: &Pubkey, - pool_tokens: u64, + sol_withdraw_authority: Option<&Pubkey>, + pool_tokens_in: u64, + minimum_lamports_out: Option, ) -> Instruction { - let accounts = vec![ + let mut accounts = vec![ AccountMeta::new(*stake_pool, false), AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), AccountMeta::new_readonly(*user_transfer_authority, true), @@ -1534,15 +1960,95 @@ pub fn withdraw_sol( AccountMeta::new_readonly(stake::program::id(), false), AccountMeta::new_readonly(*token_program_id, false), ]; - Instruction { - program_id: *program_id, - accounts, - data: StakePoolInstruction::WithdrawSol(pool_tokens) + if let Some(sol_withdraw_authority) = sol_withdraw_authority { + accounts.push(AccountMeta::new_readonly(*sol_withdraw_authority, true)); + } + if let Some(minimum_lamports_out) = minimum_lamports_out { + Instruction { + program_id: *program_id, + accounts, + data: StakePoolInstruction::WithdrawSolWithSlippage { + pool_tokens_in, + minimum_lamports_out, + } .try_to_vec() .unwrap(), + } + } else { + Instruction { + program_id: *program_id, + accounts, + data: StakePoolInstruction::WithdrawSol(pool_tokens_in) + .try_to_vec() + .unwrap(), + } } } +/// Creates instruction required to withdraw SOL directly from a stake pool. +pub fn withdraw_sol( + program_id: &Pubkey, + stake_pool: &Pubkey, + stake_pool_withdraw_authority: &Pubkey, + user_transfer_authority: &Pubkey, + pool_tokens_from: &Pubkey, + reserve_stake_account: &Pubkey, + lamports_to: &Pubkey, + manager_fee_account: &Pubkey, + pool_mint: &Pubkey, + token_program_id: &Pubkey, + pool_tokens_in: u64, +) -> Instruction { + withdraw_sol_internal( + program_id, + stake_pool, + stake_pool_withdraw_authority, + user_transfer_authority, + pool_tokens_from, + reserve_stake_account, + lamports_to, + manager_fee_account, + pool_mint, + token_program_id, + None, + pool_tokens_in, + None, + ) +} + +/// Creates instruction required to withdraw SOL directly from a stake pool with +/// slippage constraints. +pub fn withdraw_sol_with_slippage( + program_id: &Pubkey, + stake_pool: &Pubkey, + stake_pool_withdraw_authority: &Pubkey, + user_transfer_authority: &Pubkey, + pool_tokens_from: &Pubkey, + reserve_stake_account: &Pubkey, + lamports_to: &Pubkey, + manager_fee_account: &Pubkey, + pool_mint: &Pubkey, + token_program_id: &Pubkey, + pool_tokens_in: u64, + minimum_lamports_out: u64, +) -> Instruction { + withdraw_sol_internal( + program_id, + stake_pool, + stake_pool_withdraw_authority, + user_transfer_authority, + pool_tokens_from, + reserve_stake_account, + lamports_to, + manager_fee_account, + pool_mint, + token_program_id, + None, + pool_tokens_in, + Some(minimum_lamports_out), + ) +} + /// Creates instruction required to withdraw SOL directly from a stake pool. /// The difference with `withdraw_sol()` is that the sol withdraw authority /// must sign this instruction. @@ -1558,30 +2064,59 @@ pub fn withdraw_sol_with_authority( manager_fee_account: &Pubkey, pool_mint: &Pubkey, token_program_id: &Pubkey, - pool_tokens: u64, + pool_tokens_in: u64, ) -> Instruction { - let accounts = vec![ - AccountMeta::new(*stake_pool, false), - AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), - AccountMeta::new_readonly(*user_transfer_authority, true), - AccountMeta::new(*pool_tokens_from, false), - AccountMeta::new(*reserve_stake_account, false), - AccountMeta::new(*lamports_to, false), - AccountMeta::new(*manager_fee_account, false), - AccountMeta::new(*pool_mint, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::stake_history::id(), false), - AccountMeta::new_readonly(stake::program::id(), false), - AccountMeta::new_readonly(*token_program_id, false), - AccountMeta::new_readonly(*sol_withdraw_authority, true), - ]; - Instruction { - program_id: *program_id, - accounts, - data: StakePoolInstruction::WithdrawSol(pool_tokens) - .try_to_vec() - .unwrap(), - } + withdraw_sol_internal( + program_id, + stake_pool, + stake_pool_withdraw_authority, + user_transfer_authority, + pool_tokens_from, + reserve_stake_account, + lamports_to, + manager_fee_account, + pool_mint, + token_program_id, + Some(sol_withdraw_authority), + pool_tokens_in, + None, + ) +} + +/// Creates instruction required to withdraw SOL directly from a stake pool with +/// a slippage constraint. +/// The difference with `withdraw_sol()` is that the sol withdraw authority +/// must sign this instruction. +pub fn withdraw_sol_with_authority_and_slippage( + program_id: &Pubkey, + stake_pool: &Pubkey, + sol_withdraw_authority: &Pubkey, + stake_pool_withdraw_authority: &Pubkey, + user_transfer_authority: &Pubkey, + pool_tokens_from: &Pubkey, + reserve_stake_account: &Pubkey, + lamports_to: &Pubkey, + manager_fee_account: &Pubkey, + pool_mint: &Pubkey, + token_program_id: &Pubkey, + pool_tokens_in: u64, + minimum_lamports_out: u64, +) -> Instruction { + withdraw_sol_internal( + program_id, + stake_pool, + stake_pool_withdraw_authority, + user_transfer_authority, + pool_tokens_from, + reserve_stake_account, + lamports_to, + manager_fee_account, + pool_mint, + token_program_id, + Some(sol_withdraw_authority), + pool_tokens_in, + Some(minimum_lamports_out), + ) } /// Creates a 'set manager' instruction. diff --git a/stake-pool/program/src/processor.rs b/stake-pool/program/src/processor.rs index 33355dc0bec..723e36fb7ba 100644 --- a/stake-pool/program/src/processor.rs +++ b/stake-pool/program/src/processor.rs @@ -2591,7 +2591,11 @@ impl Processor { /// Processes [DepositStake](enum.Instruction.html). #[inline(never)] // needed to avoid stack size violation - fn process_deposit_stake(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + fn process_deposit_stake( + program_id: &Pubkey, + accounts: &[AccountInfo], + minimum_pool_tokens_out: Option, + ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let stake_pool_info = next_account_info(account_info_iter)?; let validator_list_info = next_account_info(account_info_iter)?; @@ -2780,6 +2784,12 @@ impl Processor { return Err(StakePoolError::DepositTooSmall.into()); } + if let Some(minimum_pool_tokens_out) = minimum_pool_tokens_out { + if pool_tokens_user < minimum_pool_tokens_out { + return Err(StakePoolError::ExceededSlippage.into()); + } + } + Self::token_mint_to( stake_pool_info.key, token_program_info.clone(), @@ -2854,6 +2864,7 @@ impl Processor { program_id: &Pubkey, accounts: &[AccountInfo], deposit_lamports: u64, + minimum_pool_tokens_out: Option, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let stake_pool_info = next_account_info(account_info_iter)?; @@ -2933,6 +2944,12 @@ impl Processor { return Err(StakePoolError::DepositTooSmall.into()); } + if let Some(minimum_pool_tokens_out) = minimum_pool_tokens_out { + if pool_tokens_user < minimum_pool_tokens_out { + return Err(StakePoolError::ExceededSlippage.into()); + } + } + Self::sol_transfer( from_user_lamports_info.clone(), reserve_stake_account_info.clone(), @@ -2996,6 +3013,7 @@ impl Processor { program_id: &Pubkey, accounts: &[AccountInfo], pool_tokens: u64, + minimum_lamports_out: Option, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let stake_pool_info = next_account_info(account_info_iter)?; @@ -3070,6 +3088,12 @@ impl Processor { return Err(StakePoolError::WithdrawalTooSmall.into()); } + if let Some(minimum_lamports_out) = minimum_lamports_out { + if withdraw_lamports < minimum_lamports_out { + return Err(StakePoolError::ExceededSlippage.into()); + } + } + let stake_minimum_delegation = stake::tools::get_minimum_delegation()?; let stake_state = try_from_slice_unchecked::(&stake_split_from.data.borrow())?; @@ -3301,6 +3325,7 @@ impl Processor { program_id: &Pubkey, accounts: &[AccountInfo], pool_tokens: u64, + minimum_lamports_out: Option, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let stake_pool_info = next_account_info(account_info_iter)?; @@ -3370,6 +3395,12 @@ impl Processor { return Err(StakePoolError::WithdrawalTooSmall.into()); } + if let Some(minimum_lamports_out) = minimum_lamports_out { + if withdraw_lamports < minimum_lamports_out { + return Err(StakePoolError::ExceededSlippage.into()); + } + } + let new_reserve_lamports = reserve_stake_info .lamports() .saturating_sub(withdraw_lamports); @@ -3840,11 +3871,11 @@ impl Processor { } StakePoolInstruction::DepositStake => { msg!("Instruction: DepositStake"); - Self::process_deposit_stake(program_id, accounts) + Self::process_deposit_stake(program_id, accounts, None) } StakePoolInstruction::WithdrawStake(amount) => { msg!("Instruction: WithdrawStake"); - Self::process_withdraw_stake(program_id, accounts, amount) + Self::process_withdraw_stake(program_id, accounts, amount, None) } StakePoolInstruction::SetFee { fee } => { msg!("Instruction: SetFee"); @@ -3864,11 +3895,11 @@ impl Processor { } StakePoolInstruction::DepositSol(lamports) => { msg!("Instruction: DepositSol"); - Self::process_deposit_sol(program_id, accounts, lamports) + Self::process_deposit_sol(program_id, accounts, lamports, None) } StakePoolInstruction::WithdrawSol(pool_tokens) => { msg!("Instruction: WithdrawSol"); - Self::process_withdraw_sol(program_id, accounts, pool_tokens) + Self::process_withdraw_sol(program_id, accounts, pool_tokens, None) } StakePoolInstruction::CreateTokenMetadata { name, symbol, uri } => { msg!("Instruction: CreateTokenMetadata"); @@ -3894,6 +3925,48 @@ impl Processor { destination_transient_stake_seed, ) } + StakePoolInstruction::DepositStakeWithSlippage { + minimum_pool_tokens_out, + } => { + msg!("Instruction: DepositStakeWithSlippage"); + Self::process_deposit_stake(program_id, accounts, Some(minimum_pool_tokens_out)) + } + StakePoolInstruction::WithdrawStakeWithSlippage { + pool_tokens_in, + minimum_lamports_out, + } => { + msg!("Instruction: WithdrawStakeWithSlippage"); + Self::process_withdraw_stake( + program_id, + accounts, + pool_tokens_in, + Some(minimum_lamports_out), + ) + } + StakePoolInstruction::DepositSolWithSlippage { + lamports_in, + minimum_pool_tokens_out, + } => { + msg!("Instruction: DepositSolWithSlippage"); + Self::process_deposit_sol( + program_id, + accounts, + lamports_in, + Some(minimum_pool_tokens_out), + ) + } + StakePoolInstruction::WithdrawSolWithSlippage { + pool_tokens_in, + minimum_lamports_out, + } => { + msg!("Instruction: WithdrawSolWithSlippage"); + Self::process_withdraw_sol( + program_id, + accounts, + pool_tokens_in, + Some(minimum_lamports_out), + ) + } } } } @@ -3945,6 +4018,7 @@ impl PrintProgramError for StakePoolError { StakePoolError::InvalidMetadataAccount => msg!("Error: Metadata account derived from pool mint account does not match the one passed to program"), StakePoolError::UnsupportedMintExtension => msg!("Error: mint has an unsupported extension"), StakePoolError::UnsupportedFeeAccountExtension => msg!("Error: fee account has an unsupported extension"), + StakePoolError::ExceededSlippage => msg!("Error: instruction exceeds desired slippage limit"), } } } diff --git a/stake-pool/program/tests/deposit.rs b/stake-pool/program/tests/deposit.rs index 585f43ffcb1..5a9f0a37eef 100644 --- a/stake-pool/program/tests/deposit.rs +++ b/stake-pool/program/tests/deposit.rs @@ -799,3 +799,105 @@ async fn fail_with_wrong_mint_for_receiver_acc() { } } } + +#[test_case(spl_token::id(); "token")] +#[test_case(spl_token_2022::id(); "token-2022")] +#[tokio::test] +async fn success_with_slippage(token_program_id: Pubkey) { + let ( + mut context, + stake_pool_accounts, + validator_stake_account, + user, + deposit_stake, + pool_token_account, + stake_lamports, + ) = setup(token_program_id).await; + + let rent = context.banks_client.get_rent().await.unwrap(); + let stake_rent = rent.minimum_balance(std::mem::size_of::()); + + // Save stake pool state before depositing + let pre_stake_pool = get_account( + &mut context.banks_client, + &stake_pool_accounts.stake_pool.pubkey(), + ) + .await; + let pre_stake_pool = + try_from_slice_unchecked::(pre_stake_pool.data.as_slice()).unwrap(); + + let tokens_issued = stake_lamports; // For now tokens are 1:1 to stake + let tokens_issued_user = tokens_issued + - pre_stake_pool + .calc_pool_tokens_sol_deposit_fee(stake_rent) + .unwrap() + - pre_stake_pool + .calc_pool_tokens_stake_deposit_fee(stake_lamports - stake_rent) + .unwrap(); + + let error = stake_pool_accounts + .deposit_stake_with_slippage( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &deposit_stake, + &pool_token_account, + &validator_stake_account.stake_account, + &user, + tokens_issued_user + 1, + ) + .await + .unwrap() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 2, + InstructionError::Custom(StakePoolError::ExceededSlippage as u32) + ) + ); + + let error = stake_pool_accounts + .deposit_stake_with_slippage( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &deposit_stake, + &pool_token_account, + &validator_stake_account.stake_account, + &user, + tokens_issued_user, + ) + .await; + assert!(error.is_none()); + + // Original stake account should be drained + assert!(context + .banks_client + .get_account(deposit_stake) + .await + .expect("get_account") + .is_none()); + + // Stake pool should add its balance to the pool balance + let post_stake_pool = get_account( + &mut context.banks_client, + &stake_pool_accounts.stake_pool.pubkey(), + ) + .await; + let post_stake_pool = + try_from_slice_unchecked::(post_stake_pool.data.as_slice()).unwrap(); + assert_eq!( + post_stake_pool.total_lamports, + pre_stake_pool.total_lamports + stake_lamports + ); + assert_eq!( + post_stake_pool.pool_token_supply, + pre_stake_pool.pool_token_supply + tokens_issued + ); + + // Check minted tokens + let user_token_balance = + get_token_balance(&mut context.banks_client, &pool_token_account).await; + assert_eq!(user_token_balance, tokens_issued_user); +} diff --git a/stake-pool/program/tests/deposit_sol.rs b/stake-pool/program/tests/deposit_sol.rs index e083419caef..5d390f0025e 100644 --- a/stake-pool/program/tests/deposit_sol.rs +++ b/stake-pool/program/tests/deposit_sol.rs @@ -504,3 +504,104 @@ async fn fail_with_invalid_referrer() { ), } } + +#[test_case(spl_token::id(); "token")] +#[test_case(spl_token_2022::id(); "token-2022")] +#[tokio::test] +async fn success_with_slippage(token_program_id: Pubkey) { + let (mut context, stake_pool_accounts, _user, pool_token_account) = + setup(token_program_id).await; + + // Save stake pool state before depositing + let pre_stake_pool = get_account( + &mut context.banks_client, + &stake_pool_accounts.stake_pool.pubkey(), + ) + .await; + let pre_stake_pool = + try_from_slice_unchecked::(pre_stake_pool.data.as_slice()).unwrap(); + + // Save reserve state before depositing + let pre_reserve_lamports = get_account( + &mut context.banks_client, + &stake_pool_accounts.reserve_stake.pubkey(), + ) + .await + .lamports; + + let new_pool_tokens = pre_stake_pool + .calc_pool_tokens_for_deposit(TEST_STAKE_AMOUNT) + .unwrap(); + let pool_tokens_sol_deposit_fee = pre_stake_pool + .calc_pool_tokens_sol_deposit_fee(new_pool_tokens) + .unwrap(); + let tokens_issued = new_pool_tokens - pool_tokens_sol_deposit_fee; + + // Fail with 1 more token in slippage + let error = stake_pool_accounts + .deposit_sol_with_slippage( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &pool_token_account, + TEST_STAKE_AMOUNT, + tokens_issued + 1, + ) + .await + .unwrap() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(error::StakePoolError::ExceededSlippage as u32) + ) + ); + + // Succeed with exact return amount + let error = stake_pool_accounts + .deposit_sol_with_slippage( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &pool_token_account, + TEST_STAKE_AMOUNT, + tokens_issued, + ) + .await; + assert!(error.is_none()); + + // Stake pool should add its balance to the pool balance + let post_stake_pool = get_account( + &mut context.banks_client, + &stake_pool_accounts.stake_pool.pubkey(), + ) + .await; + let post_stake_pool = + try_from_slice_unchecked::(post_stake_pool.data.as_slice()).unwrap(); + assert_eq!( + post_stake_pool.total_lamports, + pre_stake_pool.total_lamports + TEST_STAKE_AMOUNT + ); + assert_eq!( + post_stake_pool.pool_token_supply, + pre_stake_pool.pool_token_supply + new_pool_tokens + ); + + // Check minted tokens + let user_token_balance = + get_token_balance(&mut context.banks_client, &pool_token_account).await; + assert_eq!(user_token_balance, tokens_issued); + + // Check reserve + let post_reserve_lamports = get_account( + &mut context.banks_client, + &stake_pool_accounts.reserve_stake.pubkey(), + ) + .await + .lamports; + assert_eq!( + post_reserve_lamports, + pre_reserve_lamports + TEST_STAKE_AMOUNT + ); +} diff --git a/stake-pool/program/tests/helpers/mod.rs b/stake-pool/program/tests/helpers/mod.rs index 4a6842331c2..74948767f0f 100644 --- a/stake-pool/program/tests/helpers/mod.rs +++ b/stake-pool/program/tests/helpers/mod.rs @@ -967,6 +967,48 @@ impl StakePoolAccounts { Ok(()) } + #[allow(clippy::too_many_arguments)] + pub async fn deposit_stake_with_slippage( + &self, + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + stake: &Pubkey, + pool_account: &Pubkey, + validator_stake_account: &Pubkey, + current_staker: &Keypair, + minimum_pool_tokens_out: u64, + ) -> Option { + let mut instructions = instruction::deposit_stake_with_slippage( + &id(), + &self.stake_pool.pubkey(), + &self.validator_list.pubkey(), + &self.withdraw_authority, + stake, + ¤t_staker.pubkey(), + validator_stake_account, + &self.reserve_stake.pubkey(), + pool_account, + &self.pool_fee_account.pubkey(), + &self.pool_fee_account.pubkey(), + &self.pool_mint.pubkey(), + &self.token_program_id, + minimum_pool_tokens_out, + ); + self.maybe_add_compute_budget_instruction(&mut instructions); + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, current_staker], + *recent_blockhash, + ); + banks_client + .process_transaction(transaction) + .await + .map_err(|e| e.into()) + .err() + } + #[allow(clippy::too_many_arguments)] pub async fn deposit_stake( &self, @@ -1111,6 +1153,88 @@ impl StakePoolAccounts { .err() } + #[allow(clippy::too_many_arguments)] + pub async fn deposit_sol_with_slippage( + &self, + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + pool_account: &Pubkey, + lamports_in: u64, + minimum_pool_tokens_out: u64, + ) -> Option { + let mut instructions = vec![instruction::deposit_sol_with_slippage( + &id(), + &self.stake_pool.pubkey(), + &self.withdraw_authority, + &self.reserve_stake.pubkey(), + &payer.pubkey(), + pool_account, + &self.pool_fee_account.pubkey(), + &self.pool_fee_account.pubkey(), + &self.pool_mint.pubkey(), + &self.token_program_id, + lamports_in, + minimum_pool_tokens_out, + )]; + self.maybe_add_compute_budget_instruction(&mut instructions); + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer], + *recent_blockhash, + ); + banks_client + .process_transaction(transaction) + .await + .map_err(|e| e.into()) + .err() + } + + #[allow(clippy::too_many_arguments)] + pub async fn withdraw_stake_with_slippage( + &self, + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + stake_recipient: &Pubkey, + user_transfer_authority: &Keypair, + pool_account: &Pubkey, + validator_stake_account: &Pubkey, + recipient_new_authority: &Pubkey, + pool_tokens_in: u64, + minimum_lamports_out: u64, + ) -> Option { + let mut instructions = vec![instruction::withdraw_stake_with_slippage( + &id(), + &self.stake_pool.pubkey(), + &self.validator_list.pubkey(), + &self.withdraw_authority, + validator_stake_account, + stake_recipient, + recipient_new_authority, + &user_transfer_authority.pubkey(), + pool_account, + &self.pool_fee_account.pubkey(), + &self.pool_mint.pubkey(), + &self.token_program_id, + pool_tokens_in, + minimum_lamports_out, + )]; + self.maybe_add_compute_budget_instruction(&mut instructions); + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, user_transfer_authority], + *recent_blockhash, + ); + banks_client + .process_transaction(transaction) + .await + .map_err(|e| e.into()) + .err() + } + #[allow(clippy::too_many_arguments)] pub async fn withdraw_stake( &self, @@ -1153,6 +1277,45 @@ impl StakePoolAccounts { .err() } + #[allow(clippy::too_many_arguments)] + pub async fn withdraw_sol_with_slippage( + &self, + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + user: &Keypair, + pool_account: &Pubkey, + amount_in: u64, + minimum_lamports_out: u64, + ) -> Option { + let mut instructions = vec![instruction::withdraw_sol_with_slippage( + &id(), + &self.stake_pool.pubkey(), + &self.withdraw_authority, + &user.pubkey(), + pool_account, + &self.reserve_stake.pubkey(), + &user.pubkey(), + &self.pool_fee_account.pubkey(), + &self.pool_mint.pubkey(), + &self.token_program_id, + amount_in, + minimum_lamports_out, + )]; + self.maybe_add_compute_budget_instruction(&mut instructions); + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, user], + *recent_blockhash, + ); + banks_client + .process_transaction(transaction) + .await + .map_err(|e| e.into()) + .err() + } + #[allow(clippy::too_many_arguments)] pub async fn withdraw_sol( &self, diff --git a/stake-pool/program/tests/withdraw.rs b/stake-pool/program/tests/withdraw.rs index 2c426af077a..4b979ff7fa7 100644 --- a/stake-pool/program/tests/withdraw.rs +++ b/stake-pool/program/tests/withdraw.rs @@ -760,3 +760,81 @@ async fn fail_with_not_enough_tokens() { ) ); } + +#[test_case(spl_token::id(); "token")] +#[test_case(spl_token_2022::id(); "token-2022")] +#[tokio::test] +async fn success_with_slippage(token_program_id: Pubkey) { + let ( + mut context, + stake_pool_accounts, + validator_stake_account, + deposit_info, + user_transfer_authority, + user_stake_recipient, + tokens_to_withdraw, + ) = setup_for_withdraw(token_program_id).await; + + // Save user token balance + let user_token_balance_before = get_token_balance( + &mut context.banks_client, + &deposit_info.pool_account.pubkey(), + ) + .await; + + // first and only deposit, lamports:pool 1:1 + let tokens_withdrawal_fee = stake_pool_accounts.calculate_withdrawal_fee(tokens_to_withdraw); + let received_lamports = tokens_to_withdraw - tokens_withdrawal_fee; + + let new_authority = Pubkey::new_unique(); + let error = stake_pool_accounts + .withdraw_stake_with_slippage( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &user_stake_recipient.pubkey(), + &user_transfer_authority, + &deposit_info.pool_account.pubkey(), + &validator_stake_account.stake_account, + &new_authority, + tokens_to_withdraw, + received_lamports + 1, + ) + .await + .unwrap() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(StakePoolError::ExceededSlippage as u32) + ) + ); + + let error = stake_pool_accounts + .withdraw_stake_with_slippage( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &user_stake_recipient.pubkey(), + &user_transfer_authority, + &deposit_info.pool_account.pubkey(), + &validator_stake_account.stake_account, + &new_authority, + tokens_to_withdraw, + received_lamports, + ) + .await; + assert!(error.is_none()); + + // Check tokens used + let user_token_balance = get_token_balance( + &mut context.banks_client, + &deposit_info.pool_account.pubkey(), + ) + .await; + assert_eq!( + user_token_balance, + user_token_balance_before - tokens_to_withdraw + ); +} diff --git a/stake-pool/program/tests/withdraw_sol.rs b/stake-pool/program/tests/withdraw_sol.rs index 3e029b56ff9..8370dd92414 100644 --- a/stake-pool/program/tests/withdraw_sol.rs +++ b/stake-pool/program/tests/withdraw_sol.rs @@ -323,3 +323,72 @@ async fn fail_without_sol_withdraw_authority_signature() { ) ); } + +#[test_case(spl_token::id(); "token")] +#[test_case(spl_token_2022::id(); "token-2022")] +#[tokio::test] +async fn success_with_slippage(token_program_id: Pubkey) { + let (mut context, stake_pool_accounts, user, pool_token_account, pool_tokens) = + setup(token_program_id).await; + + let amount_received = pool_tokens - stake_pool_accounts.calculate_withdrawal_fee(pool_tokens); + + // Save reserve state before withdrawing + let pre_reserve_lamports = get_account( + &mut context.banks_client, + &stake_pool_accounts.reserve_stake.pubkey(), + ) + .await + .lamports; + + let error = stake_pool_accounts + .withdraw_sol_with_slippage( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &user, + &pool_token_account, + pool_tokens, + amount_received + 1, + ) + .await + .unwrap() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(StakePoolError::ExceededSlippage as u32) + ) + ); + + let error = stake_pool_accounts + .withdraw_sol_with_slippage( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &user, + &pool_token_account, + pool_tokens, + amount_received, + ) + .await; + assert!(error.is_none()); + + // Check burned tokens + let user_token_balance = + get_token_balance(&mut context.banks_client, &pool_token_account).await; + assert_eq!(user_token_balance, 0); + + // Check reserve + let post_reserve_lamports = get_account( + &mut context.banks_client, + &stake_pool_accounts.reserve_stake.pubkey(), + ) + .await + .lamports; + assert_eq!( + post_reserve_lamports, + pre_reserve_lamports - amount_received + ); +}