diff --git a/Cargo.lock b/Cargo.lock index 2c6fd49..13786ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4740,6 +4740,7 @@ dependencies = [ "stakedex_deposit_stake_interface", "stakedex_jup_interface", "stakedex_sdk_common", + "stakedex_withdraw_sol_interface", "stakedex_withdraw_stake_interface", ] @@ -4759,6 +4760,15 @@ dependencies = [ "unstake_interface", ] +[[package]] +name = "stakedex_withdraw_sol_interface" +version = "0.1.0" +dependencies = [ + "borsh 0.9.3", + "serde", + "solana-program", +] + [[package]] name = "stakedex_withdraw_stake_interface" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 341c788..3104c7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,3 +61,4 @@ stakedex_sdk_common = { path = "./common" } stakedex_spl_stake_pool = { path = "./libs/spl_stake_pool" } stakedex_unstake_it = { path = "./libs/unstake_it" } stakedex_withdraw_stake_interface = { path = "./interfaces/stakedex_withdraw_stake_interface" } +stakedex_withdraw_sol_interface = { path = "./interfaces/stakedex_withdraw_sol_interface" } diff --git a/common/src/address/mod.rs b/common/src/address/mod.rs index 1981de6..775d7ea 100644 --- a/common/src/address/mod.rs +++ b/common/src/address/mod.rs @@ -6,6 +6,7 @@ mod spl_deposit_cap_guard; mod spl_stake_pool_like; mod stakedex; mod unstake_it; +mod wrapped_sol; pub use lido::*; pub use marinade::*; @@ -13,3 +14,4 @@ pub use spl_deposit_cap_guard::*; pub use spl_stake_pool_like::*; pub use stakedex::*; pub use unstake_it::*; +pub use wrapped_sol::*; diff --git a/common/src/address/stakedex.rs b/common/src/address/stakedex.rs index 857c7cc..760fbea 100644 --- a/common/src/address/stakedex.rs +++ b/common/src/address/stakedex.rs @@ -4,6 +4,7 @@ pub mod stakedex_program { [ ("sol-bridge-out", b"sol_bridge_out"), ("prefunder", b"prefunder"), + ("wsol-fee-token-account", b"fee", b"\x06\x9b\x88W\xfe\xab\x81\x84\xfbh\x7fcF\x18\xc05\xda\xc49\xdc\x1a\xeb;U\x98\xa0\xf0\x00\x00\x00\x00\x01") ] ); } diff --git a/common/src/address/wrapped_sol.rs b/common/src/address/wrapped_sol.rs new file mode 100644 index 0000000..09c8d7c --- /dev/null +++ b/common/src/address/wrapped_sol.rs @@ -0,0 +1,3 @@ +pub mod wsol { + sanctum_macros::declare_program_keys!("So11111111111111111111111111111111111111112", []); +} diff --git a/common/src/lib.rs b/common/src/lib.rs index 4b61c07..adf87e2 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -7,6 +7,7 @@ mod errs; mod fees; mod init_from_keyed_account; mod pda; +mod withdraw_sol; mod withdraw_stake; pub use address::*; @@ -18,4 +19,5 @@ pub use errs::*; pub use fees::*; pub use init_from_keyed_account::*; pub use pda::*; +pub use withdraw_sol::*; pub use withdraw_stake::*; diff --git a/common/src/withdraw_sol.rs b/common/src/withdraw_sol.rs new file mode 100644 index 0000000..2bf2cca --- /dev/null +++ b/common/src/withdraw_sol.rs @@ -0,0 +1,53 @@ +use anyhow::Result; +use jupiter_amm_interface::Quote; +use rust_decimal::{ + prelude::{FromPrimitive, Zero}, + Decimal, +}; +use solana_program::{instruction::Instruction, pubkey::Pubkey}; + +use crate::{apply_global_fee, wsol, BaseStakePoolAmm}; + +#[derive(Copy, Clone, Debug)] +pub struct WithdrawSolQuote { + pub in_amount: u64, + + /// After subtracting withdraw fees + pub out_amount: u64, + + /// Withdrawal fees, in SOL + pub fee_amount: u64, +} + +pub trait WithdrawSol: BaseStakePoolAmm { + /// This should only include the stake pool's fees, not stakedex's global fees + fn get_withdraw_sol_quote(&self, lst: u64) -> Result; + + fn virtual_ix(&self) -> Result; + + fn accounts_len(&self) -> usize; + + fn convert_quote(&self, withdraw_sol_quote: WithdrawSolQuote) -> Quote { + let aft_global_fees = apply_global_fee(withdraw_sol_quote.out_amount); + let total_fees = withdraw_sol_quote.fee_amount + aft_global_fees.fee; + let final_out_amount = aft_global_fees.remainder; + let before_fees = (final_out_amount + total_fees) as f64; + // Decimal::from_f64() returns None if infinite or NaN (before_fees = 0) + let fee_pct = + Decimal::from_f64((total_fees as f64) / before_fees).unwrap_or_else(Decimal::zero); + Quote { + in_amount: withdraw_sol_quote.in_amount, + out_amount: final_out_amount, + fee_amount: total_fees, + fee_pct, + // since stakedex program levies fee on output mint, + // we count all fees in terms of output mint (wsol) to be consistent + fee_mint: wsol::ID, + ..Quote::default() + } + } + + fn underlying_liquidity(&self) -> Option<&Pubkey> { + None + } +} diff --git a/interfaces/stakedex_interface/idl.json b/interfaces/stakedex_interface/idl.json index 572b0f0..2221e3c 100644 --- a/interfaces/stakedex_interface/idl.json +++ b/interfaces/stakedex_interface/idl.json @@ -531,6 +531,63 @@ "type": "u8", "value": 7 } + }, + { + "name": "WithdrawWrappedSol", + "accounts": [ + { + "name": "user", + "isMut": false, + "isSigner": true, + "desc": "The withdraw authority of src_token_from." + }, + { + "name": "srcTokenFrom", + "isMut": true, + "isSigner": false, + "desc": "The token account to burn and redeem LSTs from" + }, + { + "name": "wsolTo", + "isMut": true, + "isSigner": false, + "desc": "The wSOL token account to receive withdrawn wrapped SOL to" + }, + { + "name": "wsolFeeTokenAccount", + "isMut": true, + "isSigner": false, + "desc": "The dest_token_mint token account collecting fees. PDA. Seeds = ['fee', dest_token_mint.pubkey]" + }, + { + "name": "srcTokenMint", + "isMut": true, + "isSigner": false, + "desc": "Input LST token mint" + }, + { + "name": "wsolMint", + "isMut": false, + "isSigner": false, + "desc": "wSOL token mint" + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "desc": "Tokenkeg program. The withdraw SOL accounts slice follows" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ], + "discriminant": { + "type": "u8", + "value": 8 + } } ], "types": [ diff --git a/interfaces/stakedex_interface/src/instructions.rs b/interfaces/stakedex_interface/src/instructions.rs index a901d73..46e698e 100644 --- a/interfaces/stakedex_interface/src/instructions.rs +++ b/interfaces/stakedex_interface/src/instructions.rs @@ -19,6 +19,7 @@ pub enum StakedexProgramIx { DepositStake, PrefundWithdrawStake(PrefundWithdrawStakeIxArgs), PrefundSwapViaStake(PrefundSwapViaStakeIxArgs), + WithdrawWrappedSol(WithdrawWrappedSolIxArgs), } impl StakedexProgramIx { pub fn deserialize(buf: &[u8]) -> std::io::Result { @@ -43,6 +44,9 @@ impl StakedexProgramIx { PREFUND_SWAP_VIA_STAKE_IX_DISCM => Ok(Self::PrefundSwapViaStake( PrefundSwapViaStakeIxArgs::deserialize(&mut reader)?, )), + WITHDRAW_WRAPPED_SOL_IX_DISCM => Ok(Self::WithdrawWrappedSol( + WithdrawWrappedSolIxArgs::deserialize(&mut reader)?, + )), _ => Err(std::io::Error::new( std::io::ErrorKind::Other, format!("discm {:?} not found", maybe_discm), @@ -71,6 +75,10 @@ impl StakedexProgramIx { writer.write_all(&[PREFUND_SWAP_VIA_STAKE_IX_DISCM])?; args.serialize(&mut writer) } + Self::WithdrawWrappedSol(args) => { + writer.write_all(&[WITHDRAW_WRAPPED_SOL_IX_DISCM])?; + args.serialize(&mut writer) + } } } pub fn try_to_vec(&self) -> std::io::Result> { @@ -2448,3 +2456,280 @@ pub fn prefund_swap_via_stake_verify_account_privileges<'me, 'info>( prefund_swap_via_stake_verify_signer_privileges(accounts)?; Ok(()) } +pub const WITHDRAW_WRAPPED_SOL_IX_ACCOUNTS_LEN: usize = 7; +#[derive(Copy, Clone, Debug)] +pub struct WithdrawWrappedSolAccounts<'me, 'info> { + ///The withdraw authority of src_token_from. + pub user: &'me AccountInfo<'info>, + ///The token account to burn and redeem LSTs from + pub src_token_from: &'me AccountInfo<'info>, + ///The wSOL token account to receive withdrawn wrapped SOL to + pub wsol_to: &'me AccountInfo<'info>, + ///The dest_token_mint token account collecting fees. PDA. Seeds = ['fee', dest_token_mint.pubkey] + pub wsol_fee_token_account: &'me AccountInfo<'info>, + ///Input LST token mint + pub src_token_mint: &'me AccountInfo<'info>, + ///wSOL token mint + pub wsol_mint: &'me AccountInfo<'info>, + ///Tokenkeg program. The withdraw SOL accounts slice follows + pub token_program: &'me AccountInfo<'info>, +} +#[derive(Copy, Clone, Debug)] +pub struct WithdrawWrappedSolKeys { + ///The withdraw authority of src_token_from. + pub user: Pubkey, + ///The token account to burn and redeem LSTs from + pub src_token_from: Pubkey, + ///The wSOL token account to receive withdrawn wrapped SOL to + pub wsol_to: Pubkey, + ///The dest_token_mint token account collecting fees. PDA. Seeds = ['fee', dest_token_mint.pubkey] + pub wsol_fee_token_account: Pubkey, + ///Input LST token mint + pub src_token_mint: Pubkey, + ///wSOL token mint + pub wsol_mint: Pubkey, + ///Tokenkeg program. The withdraw SOL accounts slice follows + pub token_program: Pubkey, +} +impl From> for WithdrawWrappedSolKeys { + fn from(accounts: WithdrawWrappedSolAccounts) -> Self { + Self { + user: *accounts.user.key, + src_token_from: *accounts.src_token_from.key, + wsol_to: *accounts.wsol_to.key, + wsol_fee_token_account: *accounts.wsol_fee_token_account.key, + src_token_mint: *accounts.src_token_mint.key, + wsol_mint: *accounts.wsol_mint.key, + token_program: *accounts.token_program.key, + } + } +} +impl From for [AccountMeta; WITHDRAW_WRAPPED_SOL_IX_ACCOUNTS_LEN] { + fn from(keys: WithdrawWrappedSolKeys) -> Self { + [ + AccountMeta { + pubkey: keys.user, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: keys.src_token_from, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: keys.wsol_to, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: keys.wsol_fee_token_account, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: keys.src_token_mint, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: keys.wsol_mint, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: keys.token_program, + is_signer: false, + is_writable: false, + }, + ] + } +} +impl From<[Pubkey; WITHDRAW_WRAPPED_SOL_IX_ACCOUNTS_LEN]> for WithdrawWrappedSolKeys { + fn from(pubkeys: [Pubkey; WITHDRAW_WRAPPED_SOL_IX_ACCOUNTS_LEN]) -> Self { + Self { + user: pubkeys[0], + src_token_from: pubkeys[1], + wsol_to: pubkeys[2], + wsol_fee_token_account: pubkeys[3], + src_token_mint: pubkeys[4], + wsol_mint: pubkeys[5], + token_program: pubkeys[6], + } + } +} +impl<'info> From> + for [AccountInfo<'info>; WITHDRAW_WRAPPED_SOL_IX_ACCOUNTS_LEN] +{ + fn from(accounts: WithdrawWrappedSolAccounts<'_, 'info>) -> Self { + [ + accounts.user.clone(), + accounts.src_token_from.clone(), + accounts.wsol_to.clone(), + accounts.wsol_fee_token_account.clone(), + accounts.src_token_mint.clone(), + accounts.wsol_mint.clone(), + accounts.token_program.clone(), + ] + } +} +impl<'me, 'info> From<&'me [AccountInfo<'info>; WITHDRAW_WRAPPED_SOL_IX_ACCOUNTS_LEN]> + for WithdrawWrappedSolAccounts<'me, 'info> +{ + fn from(arr: &'me [AccountInfo<'info>; WITHDRAW_WRAPPED_SOL_IX_ACCOUNTS_LEN]) -> Self { + Self { + user: &arr[0], + src_token_from: &arr[1], + wsol_to: &arr[2], + wsol_fee_token_account: &arr[3], + src_token_mint: &arr[4], + wsol_mint: &arr[5], + token_program: &arr[6], + } + } +} +pub const WITHDRAW_WRAPPED_SOL_IX_DISCM: u8 = 8u8; +#[derive(BorshDeserialize, BorshSerialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct WithdrawWrappedSolIxArgs { + pub amount: u64, +} +#[derive(Clone, Debug, PartialEq)] +pub struct WithdrawWrappedSolIxData(pub WithdrawWrappedSolIxArgs); +impl From for WithdrawWrappedSolIxData { + fn from(args: WithdrawWrappedSolIxArgs) -> Self { + Self(args) + } +} +impl WithdrawWrappedSolIxData { + pub fn deserialize(buf: &[u8]) -> std::io::Result { + let mut reader = buf; + let mut maybe_discm_buf = [0u8; 1]; + reader.read_exact(&mut maybe_discm_buf)?; + let maybe_discm = maybe_discm_buf[0]; + if maybe_discm != WITHDRAW_WRAPPED_SOL_IX_DISCM { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "discm does not match. Expected: {:?}. Received: {:?}", + WITHDRAW_WRAPPED_SOL_IX_DISCM, maybe_discm + ), + )); + } + Ok(Self(WithdrawWrappedSolIxArgs::deserialize(&mut reader)?)) + } + pub fn serialize(&self, mut writer: W) -> std::io::Result<()> { + writer.write_all(&[WITHDRAW_WRAPPED_SOL_IX_DISCM])?; + self.0.serialize(&mut writer) + } + pub fn try_to_vec(&self) -> std::io::Result> { + let mut data = Vec::new(); + self.serialize(&mut data)?; + Ok(data) + } +} +pub fn withdraw_wrapped_sol_ix_with_program_id( + program_id: Pubkey, + keys: WithdrawWrappedSolKeys, + args: WithdrawWrappedSolIxArgs, +) -> std::io::Result { + let metas: [AccountMeta; WITHDRAW_WRAPPED_SOL_IX_ACCOUNTS_LEN] = keys.into(); + let data: WithdrawWrappedSolIxData = args.into(); + Ok(Instruction { + program_id, + accounts: Vec::from(metas), + data: data.try_to_vec()?, + }) +} +pub fn withdraw_wrapped_sol_ix( + keys: WithdrawWrappedSolKeys, + args: WithdrawWrappedSolIxArgs, +) -> std::io::Result { + withdraw_wrapped_sol_ix_with_program_id(crate::ID, keys, args) +} +pub fn withdraw_wrapped_sol_invoke_with_program_id( + program_id: Pubkey, + accounts: WithdrawWrappedSolAccounts<'_, '_>, + args: WithdrawWrappedSolIxArgs, +) -> ProgramResult { + let keys: WithdrawWrappedSolKeys = accounts.into(); + let ix = withdraw_wrapped_sol_ix_with_program_id(program_id, keys, args)?; + invoke_instruction(&ix, accounts) +} +pub fn withdraw_wrapped_sol_invoke( + accounts: WithdrawWrappedSolAccounts<'_, '_>, + args: WithdrawWrappedSolIxArgs, +) -> ProgramResult { + withdraw_wrapped_sol_invoke_with_program_id(crate::ID, accounts, args) +} +pub fn withdraw_wrapped_sol_invoke_signed_with_program_id( + program_id: Pubkey, + accounts: WithdrawWrappedSolAccounts<'_, '_>, + args: WithdrawWrappedSolIxArgs, + seeds: &[&[&[u8]]], +) -> ProgramResult { + let keys: WithdrawWrappedSolKeys = accounts.into(); + let ix = withdraw_wrapped_sol_ix_with_program_id(program_id, keys, args)?; + invoke_instruction_signed(&ix, accounts, seeds) +} +pub fn withdraw_wrapped_sol_invoke_signed( + accounts: WithdrawWrappedSolAccounts<'_, '_>, + args: WithdrawWrappedSolIxArgs, + seeds: &[&[&[u8]]], +) -> ProgramResult { + withdraw_wrapped_sol_invoke_signed_with_program_id(crate::ID, accounts, args, seeds) +} +pub fn withdraw_wrapped_sol_verify_account_keys( + accounts: WithdrawWrappedSolAccounts<'_, '_>, + keys: WithdrawWrappedSolKeys, +) -> Result<(), (Pubkey, Pubkey)> { + for (actual, expected) in [ + (accounts.user.key, &keys.user), + (accounts.src_token_from.key, &keys.src_token_from), + (accounts.wsol_to.key, &keys.wsol_to), + ( + accounts.wsol_fee_token_account.key, + &keys.wsol_fee_token_account, + ), + (accounts.src_token_mint.key, &keys.src_token_mint), + (accounts.wsol_mint.key, &keys.wsol_mint), + (accounts.token_program.key, &keys.token_program), + ] { + if actual != expected { + return Err((*actual, *expected)); + } + } + Ok(()) +} +pub fn withdraw_wrapped_sol_verify_writable_privileges<'me, 'info>( + accounts: WithdrawWrappedSolAccounts<'me, 'info>, +) -> Result<(), (&'me AccountInfo<'info>, ProgramError)> { + for should_be_writable in [ + accounts.src_token_from, + accounts.wsol_to, + accounts.wsol_fee_token_account, + accounts.src_token_mint, + ] { + if !should_be_writable.is_writable { + return Err((should_be_writable, ProgramError::InvalidAccountData)); + } + } + Ok(()) +} +pub fn withdraw_wrapped_sol_verify_signer_privileges<'me, 'info>( + accounts: WithdrawWrappedSolAccounts<'me, 'info>, +) -> Result<(), (&'me AccountInfo<'info>, ProgramError)> { + for should_be_signer in [accounts.user] { + if !should_be_signer.is_signer { + return Err((should_be_signer, ProgramError::MissingRequiredSignature)); + } + } + Ok(()) +} +pub fn withdraw_wrapped_sol_verify_account_privileges<'me, 'info>( + accounts: WithdrawWrappedSolAccounts<'me, 'info>, +) -> Result<(), (&'me AccountInfo<'info>, ProgramError)> { + withdraw_wrapped_sol_verify_writable_privileges(accounts)?; + withdraw_wrapped_sol_verify_signer_privileges(accounts)?; + Ok(()) +} diff --git a/interfaces/stakedex_withdraw_sol_interface/.gitignore b/interfaces/stakedex_withdraw_sol_interface/.gitignore new file mode 100644 index 0000000..869df07 --- /dev/null +++ b/interfaces/stakedex_withdraw_sol_interface/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock \ No newline at end of file diff --git a/interfaces/stakedex_withdraw_sol_interface/Cargo.toml b/interfaces/stakedex_withdraw_sol_interface/Cargo.toml new file mode 100644 index 0000000..0a41b4f --- /dev/null +++ b/interfaces/stakedex_withdraw_sol_interface/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "stakedex_withdraw_sol_interface" +version = "0.1.0" +edition = "2021" + +[dependencies.borsh] +workspace = true + +[dependencies.serde] +optional = true +workspace = true + +[dependencies.solana-program] +workspace = true diff --git a/interfaces/stakedex_withdraw_sol_interface/README.md b/interfaces/stakedex_withdraw_sol_interface/README.md new file mode 100644 index 0000000..2297bd9 --- /dev/null +++ b/interfaces/stakedex_withdraw_sol_interface/README.md @@ -0,0 +1,19 @@ +# stakedex_withdraw_sol_interface + +This is a virtual IDL, there's no actual on-chain program, but we use the generated accounts/instructions structs for convenience. + +## Generate + +In workspace root: + +```sh +solores \ + -o ./interfaces \ + --solana-program-vers "workspace=true" \ + --borsh-vers "workspace=true" \ + --thiserror-vers "workspace=true" \ + --num-derive-vers "workspace=true" \ + --num-traits-vers "workspace=true" \ + --serde-vers "workspace=true" \ + interfaces/stakedex_withdraw_sol_interface/idl.json +``` diff --git a/interfaces/stakedex_withdraw_sol_interface/idl.json b/interfaces/stakedex_withdraw_sol_interface/idl.json new file mode 100644 index 0000000..8be7ee5 --- /dev/null +++ b/interfaces/stakedex_withdraw_sol_interface/idl.json @@ -0,0 +1,66 @@ +{ + "version": "0.1.0", + "name": "stakedex_withdraw_sol", + "instructions": [ + { + "name": "SplStakePoolWithdrawSol", + "accounts": [ + { + "name": "splStakePoolProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "withdrawSolSplStakePool", + "isMut": true, + "isSigner": false + }, + { + "name": "withdrawSolWithdrawAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "withdrawSolReserveStake", + "isMut": true, + "isSigner": false + }, + { + "name": "withdrawSolManagerFee", + "isMut": true, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "stakeHistory", + "isMut": false, + "isSigner": false + }, + { + "name": "stakeProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "withdrawSolTokenProgram", + "isMut": false, + "isSigner": false, + "desc": "possible duplicate to account for token-22 stake pools" + } + ], + "args": [], + "discriminant": { + "type": "u8", + "value": 1 + } + } + ], + "metadata": { + "origin": "shank", + "address": "TH1S1SAV1RTUAL1DLoNLY1XACCoUNTSAREUSED11111" + } +} diff --git a/interfaces/stakedex_withdraw_sol_interface/src/instructions.rs b/interfaces/stakedex_withdraw_sol_interface/src/instructions.rs new file mode 100644 index 0000000..f589173 --- /dev/null +++ b/interfaces/stakedex_withdraw_sol_interface/src/instructions.rs @@ -0,0 +1,331 @@ +use solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + instruction::{AccountMeta, Instruction}, + program::{invoke, invoke_signed}, + program_error::ProgramError, + pubkey::Pubkey, +}; +use std::io::Read; +#[derive(Clone, Debug, PartialEq)] +pub enum StakedexWithdrawSolProgramIx { + SplStakePoolWithdrawSol, +} +impl StakedexWithdrawSolProgramIx { + pub fn deserialize(buf: &[u8]) -> std::io::Result { + let mut reader = buf; + let mut maybe_discm_buf = [0u8; 1]; + reader.read_exact(&mut maybe_discm_buf)?; + let maybe_discm = maybe_discm_buf[0]; + match maybe_discm { + SPL_STAKE_POOL_WITHDRAW_SOL_IX_DISCM => Ok(Self::SplStakePoolWithdrawSol), + _ => Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("discm {:?} not found", maybe_discm), + )), + } + } + pub fn serialize(&self, mut writer: W) -> std::io::Result<()> { + match self { + Self::SplStakePoolWithdrawSol => { + writer.write_all(&[SPL_STAKE_POOL_WITHDRAW_SOL_IX_DISCM]) + } + } + } + pub fn try_to_vec(&self) -> std::io::Result> { + let mut data = Vec::new(); + self.serialize(&mut data)?; + Ok(data) + } +} +fn invoke_instruction<'info, A: Into<[AccountInfo<'info>; N]>, const N: usize>( + ix: &Instruction, + accounts: A, +) -> ProgramResult { + let account_info: [AccountInfo<'info>; N] = accounts.into(); + invoke(ix, &account_info) +} +fn invoke_instruction_signed<'info, A: Into<[AccountInfo<'info>; N]>, const N: usize>( + ix: &Instruction, + accounts: A, + seeds: &[&[&[u8]]], +) -> ProgramResult { + let account_info: [AccountInfo<'info>; N] = accounts.into(); + invoke_signed(ix, &account_info, seeds) +} +pub const SPL_STAKE_POOL_WITHDRAW_SOL_IX_ACCOUNTS_LEN: usize = 9; +#[derive(Copy, Clone, Debug)] +pub struct SplStakePoolWithdrawSolAccounts<'me, 'info> { + pub spl_stake_pool_program: &'me AccountInfo<'info>, + pub withdraw_sol_spl_stake_pool: &'me AccountInfo<'info>, + pub withdraw_sol_withdraw_authority: &'me AccountInfo<'info>, + pub withdraw_sol_reserve_stake: &'me AccountInfo<'info>, + pub withdraw_sol_manager_fee: &'me AccountInfo<'info>, + pub clock: &'me AccountInfo<'info>, + pub stake_history: &'me AccountInfo<'info>, + pub stake_program: &'me AccountInfo<'info>, + ///possible duplicate to account for token-22 stake pools + pub withdraw_sol_token_program: &'me AccountInfo<'info>, +} +#[derive(Copy, Clone, Debug)] +pub struct SplStakePoolWithdrawSolKeys { + pub spl_stake_pool_program: Pubkey, + pub withdraw_sol_spl_stake_pool: Pubkey, + pub withdraw_sol_withdraw_authority: Pubkey, + pub withdraw_sol_reserve_stake: Pubkey, + pub withdraw_sol_manager_fee: Pubkey, + pub clock: Pubkey, + pub stake_history: Pubkey, + pub stake_program: Pubkey, + ///possible duplicate to account for token-22 stake pools + pub withdraw_sol_token_program: Pubkey, +} +impl From> for SplStakePoolWithdrawSolKeys { + fn from(accounts: SplStakePoolWithdrawSolAccounts) -> Self { + Self { + spl_stake_pool_program: *accounts.spl_stake_pool_program.key, + withdraw_sol_spl_stake_pool: *accounts.withdraw_sol_spl_stake_pool.key, + withdraw_sol_withdraw_authority: *accounts.withdraw_sol_withdraw_authority.key, + withdraw_sol_reserve_stake: *accounts.withdraw_sol_reserve_stake.key, + withdraw_sol_manager_fee: *accounts.withdraw_sol_manager_fee.key, + clock: *accounts.clock.key, + stake_history: *accounts.stake_history.key, + stake_program: *accounts.stake_program.key, + withdraw_sol_token_program: *accounts.withdraw_sol_token_program.key, + } + } +} +impl From + for [AccountMeta; SPL_STAKE_POOL_WITHDRAW_SOL_IX_ACCOUNTS_LEN] +{ + fn from(keys: SplStakePoolWithdrawSolKeys) -> Self { + [ + AccountMeta { + pubkey: keys.spl_stake_pool_program, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: keys.withdraw_sol_spl_stake_pool, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: keys.withdraw_sol_withdraw_authority, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: keys.withdraw_sol_reserve_stake, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: keys.withdraw_sol_manager_fee, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: keys.clock, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: keys.stake_history, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: keys.stake_program, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: keys.withdraw_sol_token_program, + is_signer: false, + is_writable: false, + }, + ] + } +} +impl From<[Pubkey; SPL_STAKE_POOL_WITHDRAW_SOL_IX_ACCOUNTS_LEN]> for SplStakePoolWithdrawSolKeys { + fn from(pubkeys: [Pubkey; SPL_STAKE_POOL_WITHDRAW_SOL_IX_ACCOUNTS_LEN]) -> Self { + Self { + spl_stake_pool_program: pubkeys[0], + withdraw_sol_spl_stake_pool: pubkeys[1], + withdraw_sol_withdraw_authority: pubkeys[2], + withdraw_sol_reserve_stake: pubkeys[3], + withdraw_sol_manager_fee: pubkeys[4], + clock: pubkeys[5], + stake_history: pubkeys[6], + stake_program: pubkeys[7], + withdraw_sol_token_program: pubkeys[8], + } + } +} +impl<'info> From> + for [AccountInfo<'info>; SPL_STAKE_POOL_WITHDRAW_SOL_IX_ACCOUNTS_LEN] +{ + fn from(accounts: SplStakePoolWithdrawSolAccounts<'_, 'info>) -> Self { + [ + accounts.spl_stake_pool_program.clone(), + accounts.withdraw_sol_spl_stake_pool.clone(), + accounts.withdraw_sol_withdraw_authority.clone(), + accounts.withdraw_sol_reserve_stake.clone(), + accounts.withdraw_sol_manager_fee.clone(), + accounts.clock.clone(), + accounts.stake_history.clone(), + accounts.stake_program.clone(), + accounts.withdraw_sol_token_program.clone(), + ] + } +} +impl<'me, 'info> From<&'me [AccountInfo<'info>; SPL_STAKE_POOL_WITHDRAW_SOL_IX_ACCOUNTS_LEN]> + for SplStakePoolWithdrawSolAccounts<'me, 'info> +{ + fn from(arr: &'me [AccountInfo<'info>; SPL_STAKE_POOL_WITHDRAW_SOL_IX_ACCOUNTS_LEN]) -> Self { + Self { + spl_stake_pool_program: &arr[0], + withdraw_sol_spl_stake_pool: &arr[1], + withdraw_sol_withdraw_authority: &arr[2], + withdraw_sol_reserve_stake: &arr[3], + withdraw_sol_manager_fee: &arr[4], + clock: &arr[5], + stake_history: &arr[6], + stake_program: &arr[7], + withdraw_sol_token_program: &arr[8], + } + } +} +pub const SPL_STAKE_POOL_WITHDRAW_SOL_IX_DISCM: u8 = 1u8; +#[derive(Clone, Debug, PartialEq)] +pub struct SplStakePoolWithdrawSolIxData; +impl SplStakePoolWithdrawSolIxData { + pub fn deserialize(buf: &[u8]) -> std::io::Result { + let mut reader = buf; + let mut maybe_discm_buf = [0u8; 1]; + reader.read_exact(&mut maybe_discm_buf)?; + let maybe_discm = maybe_discm_buf[0]; + if maybe_discm != SPL_STAKE_POOL_WITHDRAW_SOL_IX_DISCM { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "discm does not match. Expected: {:?}. Received: {:?}", + SPL_STAKE_POOL_WITHDRAW_SOL_IX_DISCM, maybe_discm + ), + )); + } + Ok(Self) + } + pub fn serialize(&self, mut writer: W) -> std::io::Result<()> { + writer.write_all(&[SPL_STAKE_POOL_WITHDRAW_SOL_IX_DISCM]) + } + pub fn try_to_vec(&self) -> std::io::Result> { + let mut data = Vec::new(); + self.serialize(&mut data)?; + Ok(data) + } +} +pub fn spl_stake_pool_withdraw_sol_ix_with_program_id( + program_id: Pubkey, + keys: SplStakePoolWithdrawSolKeys, +) -> std::io::Result { + let metas: [AccountMeta; SPL_STAKE_POOL_WITHDRAW_SOL_IX_ACCOUNTS_LEN] = keys.into(); + Ok(Instruction { + program_id, + accounts: Vec::from(metas), + data: SplStakePoolWithdrawSolIxData.try_to_vec()?, + }) +} +pub fn spl_stake_pool_withdraw_sol_ix( + keys: SplStakePoolWithdrawSolKeys, +) -> std::io::Result { + spl_stake_pool_withdraw_sol_ix_with_program_id(crate::ID, keys) +} +pub fn spl_stake_pool_withdraw_sol_invoke_with_program_id( + program_id: Pubkey, + accounts: SplStakePoolWithdrawSolAccounts<'_, '_>, +) -> ProgramResult { + let keys: SplStakePoolWithdrawSolKeys = accounts.into(); + let ix = spl_stake_pool_withdraw_sol_ix_with_program_id(program_id, keys)?; + invoke_instruction(&ix, accounts) +} +pub fn spl_stake_pool_withdraw_sol_invoke( + accounts: SplStakePoolWithdrawSolAccounts<'_, '_>, +) -> ProgramResult { + spl_stake_pool_withdraw_sol_invoke_with_program_id(crate::ID, accounts) +} +pub fn spl_stake_pool_withdraw_sol_invoke_signed_with_program_id( + program_id: Pubkey, + accounts: SplStakePoolWithdrawSolAccounts<'_, '_>, + seeds: &[&[&[u8]]], +) -> ProgramResult { + let keys: SplStakePoolWithdrawSolKeys = accounts.into(); + let ix = spl_stake_pool_withdraw_sol_ix_with_program_id(program_id, keys)?; + invoke_instruction_signed(&ix, accounts, seeds) +} +pub fn spl_stake_pool_withdraw_sol_invoke_signed( + accounts: SplStakePoolWithdrawSolAccounts<'_, '_>, + seeds: &[&[&[u8]]], +) -> ProgramResult { + spl_stake_pool_withdraw_sol_invoke_signed_with_program_id(crate::ID, accounts, seeds) +} +pub fn spl_stake_pool_withdraw_sol_verify_account_keys( + accounts: SplStakePoolWithdrawSolAccounts<'_, '_>, + keys: SplStakePoolWithdrawSolKeys, +) -> Result<(), (Pubkey, Pubkey)> { + for (actual, expected) in [ + ( + accounts.spl_stake_pool_program.key, + &keys.spl_stake_pool_program, + ), + ( + accounts.withdraw_sol_spl_stake_pool.key, + &keys.withdraw_sol_spl_stake_pool, + ), + ( + accounts.withdraw_sol_withdraw_authority.key, + &keys.withdraw_sol_withdraw_authority, + ), + ( + accounts.withdraw_sol_reserve_stake.key, + &keys.withdraw_sol_reserve_stake, + ), + ( + accounts.withdraw_sol_manager_fee.key, + &keys.withdraw_sol_manager_fee, + ), + (accounts.clock.key, &keys.clock), + (accounts.stake_history.key, &keys.stake_history), + (accounts.stake_program.key, &keys.stake_program), + ( + accounts.withdraw_sol_token_program.key, + &keys.withdraw_sol_token_program, + ), + ] { + if actual != expected { + return Err((*actual, *expected)); + } + } + Ok(()) +} +pub fn spl_stake_pool_withdraw_sol_verify_writable_privileges<'me, 'info>( + accounts: SplStakePoolWithdrawSolAccounts<'me, 'info>, +) -> Result<(), (&'me AccountInfo<'info>, ProgramError)> { + for should_be_writable in [ + accounts.withdraw_sol_spl_stake_pool, + accounts.withdraw_sol_reserve_stake, + accounts.withdraw_sol_manager_fee, + ] { + if !should_be_writable.is_writable { + return Err((should_be_writable, ProgramError::InvalidAccountData)); + } + } + Ok(()) +} +pub fn spl_stake_pool_withdraw_sol_verify_account_privileges<'me, 'info>( + accounts: SplStakePoolWithdrawSolAccounts<'me, 'info>, +) -> Result<(), (&'me AccountInfo<'info>, ProgramError)> { + spl_stake_pool_withdraw_sol_verify_writable_privileges(accounts)?; + Ok(()) +} diff --git a/interfaces/stakedex_withdraw_sol_interface/src/lib.rs b/interfaces/stakedex_withdraw_sol_interface/src/lib.rs new file mode 100644 index 0000000..ea25011 --- /dev/null +++ b/interfaces/stakedex_withdraw_sol_interface/src/lib.rs @@ -0,0 +1,3 @@ +solana_program::declare_id!("TH1S1SAV1RTUAL1DLoNLY1XACCoUNTSAREUSED11111"); +pub mod instructions; +pub use instructions::*; diff --git a/jup_interface/src/pool_sol/deposit_withdraw_sol.rs b/jup_interface/src/pool_sol/deposit_withdraw_sol.rs new file mode 100644 index 0000000..bf0a22c --- /dev/null +++ b/jup_interface/src/pool_sol/deposit_withdraw_sol.rs @@ -0,0 +1,175 @@ +use anyhow::{anyhow, Result}; +use jupiter_amm_interface::{ + AccountMap, Amm, AmmContext, KeyedAccount, Quote, QuoteParams, SwapAndAccountMetas, SwapParams, +}; +use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey, system_program}; +use spl_token::native_mint; +use stakedex_interface::{ + StakeWrappedSolKeys, WithdrawWrappedSolKeys, STAKE_WRAPPED_SOL_IX_ACCOUNTS_LEN, + WITHDRAW_WRAPPED_SOL_IX_ACCOUNTS_LEN, +}; +use stakedex_sdk_common::{ + find_deposit_stake_amm_key, find_fee_token_acc, stakedex_program, wsol_bridge_in, DepositSol, + InitFromKeyedAccount, WithdrawSol, TEMPORARY_JUP_AMM_LABEL, +}; + +use crate::jupiter_stakedex_interface::STAKEDEX_ACCOUNT_META; + +// newtype pattern in order to impl external trait (Amm) on external generic (WithdrawSol) +#[derive(Clone)] +pub struct DepositWithdrawSolWrapper(pub T); + +impl Amm for DepositWithdrawSolWrapper +where + T: DepositSol + WithdrawSol + InitFromKeyedAccount + Clone + Send + Sync + 'static, +{ + fn from_keyed_account(keyed_account: &KeyedAccount, amm_context: &AmmContext) -> Result { + T::from_keyed_account(keyed_account, amm_context).map(|t| Self(t)) + } + + fn label(&self) -> String { + TEMPORARY_JUP_AMM_LABEL.to_owned() + } + + // To avoid key clashes with existing stake pools on jup (Marinade), + // we can use a PDA like this + fn key(&self) -> Pubkey { + find_deposit_stake_amm_key(&self.0.main_state_key()).0 + } + + fn get_reserve_mints(&self) -> Vec { + Vec::from([native_mint::ID, self.0.staked_sol_mint()]) + } + + fn get_accounts_to_update(&self) -> Vec { + self.0.get_accounts_to_update() + } + + fn update(&mut self, accounts_map: &AccountMap) -> Result<()> { + self.0.update(accounts_map) + } + + fn quote(&self, quote_params: &QuoteParams) -> Result { + if quote_params.input_mint == native_mint::ID + && quote_params.output_mint == self.0.staked_sol_mint() + { + // deposit case + let deposit_sol_quote = self.0.get_deposit_sol_quote(quote_params.amount)?; + let quote = DepositSol::convert_quote(&self.0, deposit_sol_quote); + Ok(quote) + } else if quote_params.input_mint == self.0.staked_sol_mint() + && quote_params.output_mint == native_mint::ID + { + // withdraw case + let withdraw_sol_quote = self.0.get_withdraw_sol_quote(quote_params.amount)?; + let quote = WithdrawSol::convert_quote(&self.0, withdraw_sol_quote); + Ok(quote) + } else { + Err(anyhow!( + "Cannot handle {} -> {}", + quote_params.input_mint, + quote_params.output_mint + )) + } + } + + fn get_swap_and_account_metas(&self, swap_params: &SwapParams) -> Result { + let mut account_metas = vec![STAKEDEX_ACCOUNT_META.clone()]; + + if swap_params.source_mint == native_mint::ID + && swap_params.destination_mint == self.0.staked_sol_mint() + { + // deposit case + account_metas.extend(<[AccountMeta; STAKE_WRAPPED_SOL_IX_ACCOUNTS_LEN]>::from( + StakeWrappedSolKeys { + user: swap_params.token_transfer_authority, + wsol_from: swap_params.source_token_account, + dest_token_to: swap_params.destination_token_account, + wsol_mint: swap_params.source_mint, + dest_token_mint: swap_params.destination_mint, + token_program: spl_token::ID, + system_program: system_program::ID, + wsol_bridge_in: wsol_bridge_in::ID, + sol_bridge_out: stakedex_program::SOL_BRIDGE_OUT_ID, + dest_token_fee_token_account: find_fee_token_acc(&swap_params.destination_mint) + .0, + }, + )); + + let deposit_sol_virtual_ix = DepositSol::virtual_ix(&self.0)?; + account_metas.extend(deposit_sol_virtual_ix.accounts); + account_metas.push(swap_params.placeholder_account_meta()); + Ok(SwapAndAccountMetas { + swap: todo!(), // TODO: get jup to add a new variant to Swap enum, + account_metas, + }) + } else if swap_params.source_mint == self.0.staked_sol_mint() + && swap_params.destination_mint == native_mint::ID + { + // withdraw case + account_metas.extend(<[AccountMeta; WITHDRAW_WRAPPED_SOL_IX_ACCOUNTS_LEN]>::from( + WithdrawWrappedSolKeys { + user: swap_params.token_transfer_authority, + src_token_from: swap_params.source_token_account, + wsol_to: swap_params.destination_token_account, + wsol_fee_token_account: find_fee_token_acc(&swap_params.destination_mint).0, + src_token_mint: swap_params.source_mint, + wsol_mint: swap_params.destination_mint, + token_program: spl_token::ID, + }, + )); + + let withdraw_sol_virtual_ix = WithdrawSol::virtual_ix(&self.0)?; + account_metas.extend(withdraw_sol_virtual_ix.accounts); + account_metas.push(swap_params.placeholder_account_meta()); + Ok(SwapAndAccountMetas { + swap: todo!(), // TODO: get jup to add a new variant to Swap enum, + account_metas, + }) + } else { + Err(anyhow!( + "Cannot handle {} -> {}", + swap_params.source_mint, + swap_params.destination_mint + )) + } + } + + fn clone_amm(&self) -> Box { + Box::new(self.clone()) + } + + fn program_id(&self) -> Pubkey { + stakedex_interface::ID + } + + fn unidirectional(&self) -> bool { + true + } + + // TODO: for compile time max calculation + // - should this just be all within const + // - or just ditch const altogether since it's either: + // - 1 + STAKE_WRAPPED_SOL_IX_ACCOUNTS_LEN + DepositSol::accounts_len() + // - 1 + WITHDRAW_WRAPPED_SOL_IX_ACCOUNTS_LEN + WithdrawSol::accounts_len(), + // - and never the other way around + fn get_accounts_len(&self) -> usize { + 1 + const { + if STAKE_WRAPPED_SOL_IX_ACCOUNTS_LEN > WITHDRAW_WRAPPED_SOL_IX_ACCOUNTS_LEN { + STAKE_WRAPPED_SOL_IX_ACCOUNTS_LEN + } else { + WITHDRAW_WRAPPED_SOL_IX_ACCOUNTS_LEN + } + } + std::cmp::max( + WithdrawSol::accounts_len(&self.0), + DepositSol::accounts_len(&self.0), + ) + } + + fn program_dependencies(&self) -> Vec<(Pubkey, String)> { + vec![( + self.0.program_id(), + self.0.stake_pool_label().to_lowercase(), + )] + } +} diff --git a/jup_interface/src/pool_sol/mod.rs b/jup_interface/src/pool_sol/mod.rs index 7b46c55..94b98fd 100644 --- a/jup_interface/src/pool_sol/mod.rs +++ b/jup_interface/src/pool_sol/mod.rs @@ -1,6 +1,7 @@ //! Stake pools that accept direct SOL deposits or withdrawals mod deposit_sol; -// TODO: withdraw_sol for SPL +mod deposit_withdraw_sol; pub use deposit_sol::*; +pub use deposit_withdraw_sol::*; diff --git a/libs/spl_stake_pool/Cargo.toml b/libs/spl_stake_pool/Cargo.toml index 21b2a5a..4963c37 100644 --- a/libs/spl_stake_pool/Cargo.toml +++ b/libs/spl_stake_pool/Cargo.toml @@ -17,6 +17,7 @@ stakedex_deposit_sol_interface = { workspace = true } stakedex_deposit_stake_interface = { workspace = true } stakedex_sdk_common = { workspace = true } stakedex_withdraw_stake_interface = { workspace = true } +stakedex_withdraw_sol_interface = { workspace = true } [dev-dependencies] stakedex_jup_interface = { workspace = true } diff --git a/libs/spl_stake_pool/src/lib.rs b/libs/spl_stake_pool/src/lib.rs index 4d2e084..cf35d37 100644 --- a/libs/spl_stake_pool/src/lib.rs +++ b/libs/spl_stake_pool/src/lib.rs @@ -1,4 +1,7 @@ -use std::sync::{atomic::AtomicU64, Arc}; +use std::{ + num::NonZeroU64, + sync::{atomic::AtomicU64, Arc}, +}; use anyhow::{anyhow, Result}; use deposit_cap_guard::{find_spl_deposit_cap_guard_state, DepositCap}; @@ -23,7 +26,7 @@ pub struct SplStakePoolStakedexInitKeys { /// A SPL stake pool with possibly custom program ID. /// Works for different deploys of spl stake pool prog - spl, sanctum spl, sanctum spl multi -#[derive(Clone, Default)] +#[derive(Debug, Clone, Default)] pub struct SplStakePoolStakedex { pub stake_pool_addr: Pubkey, pub stake_pool_program: Pubkey, @@ -157,6 +160,17 @@ impl SplStakePoolStakedex { } } +/// Newtype encapsulating [`SplStakePoolStakedex`] because +/// DepositSol, DepositStake, WithdrawStake does not require fetching reserve stake account +/// for quoting, only WithdrawSol does. +#[derive(Debug, Clone, Default)] +pub struct SplStakePoolStakedexWithWithdrawSol { + pub inner: SplStakePoolStakedex, + // NonZero: reserve should always have at least rent-exempt lamports. + // Initialize with `None`, fetch reserve account to update + pub reserve_stake_lamports: Option, +} + #[cfg(test)] mod tests { use stakedex_jup_interface::DepositSolWrapper; diff --git a/libs/spl_stake_pool/src/stakedex_traits/base.rs b/libs/spl_stake_pool/src/stakedex_traits/base.rs index 6252f9d..0afdef8 100644 --- a/libs/spl_stake_pool/src/stakedex_traits/base.rs +++ b/libs/spl_stake_pool/src/stakedex_traits/base.rs @@ -1,9 +1,12 @@ +use std::num::NonZeroU64; + use anyhow::Result; use jupiter_amm_interface::{AccountMap, AmmContext, KeyedAccount}; use solana_program::pubkey::Pubkey; +use spl_stake_pool::error::StakePoolError; use stakedex_sdk_common::{account_missing_err, BaseStakePoolAmm, InitFromKeyedAccount}; -use crate::SplStakePoolStakedex; +use crate::{SplStakePoolStakedex, SplStakePoolStakedexWithWithdrawSol}; impl InitFromKeyedAccount for SplStakePoolStakedex { /// Initialize from stake pool main account @@ -39,22 +42,27 @@ impl InitFromKeyedAccount for SplStakePoolStakedex { } impl BaseStakePoolAmm for SplStakePoolStakedex { + #[inline] fn program_id(&self) -> Pubkey { self.stake_pool_program } + #[inline] fn stake_pool_label(&self) -> &str { &self.stake_pool_label } + #[inline] fn main_state_key(&self) -> Pubkey { self.stake_pool_addr } + #[inline] fn staked_sol_mint(&self) -> Pubkey { self.stake_pool.pool_mint } + #[inline] fn get_accounts_to_update(&self) -> Vec { let mut res = Vec::from([self.stake_pool_addr, self.stake_pool.validator_list]); if self.is_sol_deposit_capped() || self.is_stake_deposit_capped() { @@ -87,3 +95,53 @@ impl BaseStakePoolAmm for SplStakePoolStakedex { Ok(()) } } + +impl InitFromKeyedAccount for SplStakePoolStakedexWithWithdrawSol { + #[inline] + fn from_keyed_account(keyed_account: &KeyedAccount, amm_context: &AmmContext) -> Result { + Ok(Self { + inner: SplStakePoolStakedex::from_keyed_account(keyed_account, amm_context)?, + reserve_stake_lamports: None, + }) + } +} + +impl BaseStakePoolAmm for SplStakePoolStakedexWithWithdrawSol { + #[inline] + fn program_id(&self) -> Pubkey { + self.inner.program_id() + } + + #[inline] + fn stake_pool_label(&self) -> &str { + self.inner.stake_pool_label() + } + + #[inline] + fn main_state_key(&self) -> Pubkey { + self.inner.main_state_key() + } + + #[inline] + fn staked_sol_mint(&self) -> Pubkey { + self.inner.staked_sol_mint() + } + + #[inline] + fn get_accounts_to_update(&self) -> Vec { + let mut res = self.inner.get_accounts_to_update(); + res.push(self.inner.stake_pool.reserve_stake); + res + } + + fn update(&mut self, account_map: &AccountMap) -> Result<()> { + self.inner.update(account_map)?; + let reserve_stake = account_map + .get(&self.inner.stake_pool.reserve_stake) + .ok_or_else(|| account_missing_err(&self.inner.stake_pool.reserve_stake))?; + let reserve_stake_lamports = + NonZeroU64::new(reserve_stake.lamports).ok_or(StakePoolError::WrongStakeStake)?; + self.reserve_stake_lamports = Some(reserve_stake_lamports); + Ok(()) + } +} diff --git a/libs/spl_stake_pool/src/stakedex_traits/deposit_sol.rs b/libs/spl_stake_pool/src/stakedex_traits/deposit_sol.rs index 3dcb803..6c70406 100644 --- a/libs/spl_stake_pool/src/stakedex_traits/deposit_sol.rs +++ b/libs/spl_stake_pool/src/stakedex_traits/deposit_sol.rs @@ -9,9 +9,31 @@ use stakedex_sdk_common::{DepositSol, DepositSolQuote}; use crate::{ deposit_cap_guard::{to_deposit_cap_guard_ix, DepositCap}, - SplStakePoolStakedex, + SplStakePoolStakedex, SplStakePoolStakedexWithWithdrawSol, }; +impl DepositSol for SplStakePoolStakedexWithWithdrawSol { + #[inline] + fn can_accept_sol_deposits(&self) -> bool { + self.inner.can_accept_sol_deposits() + } + + #[inline] + fn get_deposit_sol_quote_unchecked(&self, lamports: u64) -> Result { + self.inner.get_deposit_sol_quote_unchecked(lamports) + } + + #[inline] + fn virtual_ix(&self) -> Result { + self.inner.virtual_ix() + } + + #[inline] + fn accounts_len(&self) -> usize { + self.inner.accounts_len() + } +} + impl DepositSol for SplStakePoolStakedex { fn can_accept_sol_deposits(&self) -> bool { if self.stake_pool.sol_deposit_authority.is_some() { @@ -90,6 +112,7 @@ impl DepositSol for SplStakePoolStakedex { }) } + #[inline] fn accounts_len(&self) -> usize { SPL_STAKE_POOL_DEPOSIT_SOL_IX_ACCOUNTS_LEN } diff --git a/libs/spl_stake_pool/src/stakedex_traits/deposit_stake.rs b/libs/spl_stake_pool/src/stakedex_traits/deposit_stake.rs index f028df5..526acb6 100644 --- a/libs/spl_stake_pool/src/stakedex_traits/deposit_stake.rs +++ b/libs/spl_stake_pool/src/stakedex_traits/deposit_stake.rs @@ -12,7 +12,7 @@ use stakedex_sdk_common::{ use crate::{ deposit_cap_guard::{to_deposit_cap_guard_ix, DepositCap}, - SplStakePoolStakedex, + SplStakePoolStakedex, SplStakePoolStakedexWithWithdrawSol, }; impl DepositStake for SplStakePoolStakedex { @@ -184,3 +184,33 @@ impl DepositStake for SplStakePoolStakedex { SPL_STAKE_POOL_DEPOSIT_STAKE_IX_ACCOUNTS_LEN } } + +impl DepositStake for SplStakePoolStakedexWithWithdrawSol { + #[inline] + fn can_accept_stake_deposits(&self) -> bool { + self.inner.can_accept_stake_deposits() + } + + #[inline] + fn get_deposit_stake_quote_unchecked( + &self, + withdraw_stake_quote: WithdrawStakeQuote, + ) -> DepositStakeQuote { + self.inner + .get_deposit_stake_quote_unchecked(withdraw_stake_quote) + } + + #[inline] + fn virtual_ix( + &self, + quote: &DepositStakeQuote, + deposit_stake_info: &DepositStakeInfo, + ) -> Result { + self.inner.virtual_ix(quote, deposit_stake_info) + } + + #[inline] + fn accounts_len(&self) -> usize { + self.inner.accounts_len() + } +} diff --git a/libs/spl_stake_pool/src/stakedex_traits/mod.rs b/libs/spl_stake_pool/src/stakedex_traits/mod.rs index 6ce232a..744c103 100644 --- a/libs/spl_stake_pool/src/stakedex_traits/mod.rs +++ b/libs/spl_stake_pool/src/stakedex_traits/mod.rs @@ -1,4 +1,5 @@ mod base; mod deposit_sol; mod deposit_stake; +mod withdraw_sol; mod withdraw_stake; diff --git a/libs/spl_stake_pool/src/stakedex_traits/withdraw_sol.rs b/libs/spl_stake_pool/src/stakedex_traits/withdraw_sol.rs new file mode 100644 index 0000000..b703a46 --- /dev/null +++ b/libs/spl_stake_pool/src/stakedex_traits/withdraw_sol.rs @@ -0,0 +1,97 @@ +use anyhow::Result; +use solana_program::{instruction::Instruction, pubkey::Pubkey, stake, sysvar}; +use spl_stake_pool::{error::StakePoolError, state::StakePool, MINIMUM_RESERVE_LAMPORTS}; +use stakedex_sdk_common::{WithdrawSol, WithdrawSolQuote, STAKE_ACCOUNT_RENT_EXEMPT_LAMPORTS}; +use stakedex_withdraw_sol_interface::{ + spl_stake_pool_withdraw_sol_ix, SplStakePoolWithdrawSolKeys, + SPL_STAKE_POOL_WITHDRAW_SOL_IX_ACCOUNTS_LEN, +}; + +use crate::SplStakePoolStakedexWithWithdrawSol; + +// Adapted from: https://github.com/solana-labs/solana-program-library/blob/17a228bb8e36737209ca5d5375415c70da37c311/stake-pool/program/src/lib.rs#L80-L84 +// Will have to change if network changes rent-exempt parameters +const TOTAL_MIN_RESERVE_LAMPORTS: u64 = + STAKE_ACCOUNT_RENT_EXEMPT_LAMPORTS + MINIMUM_RESERVE_LAMPORTS; + +impl WithdrawSol for SplStakePoolStakedexWithWithdrawSol { + fn get_withdraw_sol_quote(&self, lst: u64) -> Result { + // Interface does not work for pools with permissioned SOL withdrawals + if self.inner.stake_pool.sol_withdraw_authority.is_some() { + return Err(StakePoolError::InvalidSolWithdrawAuthority.into()); + } + if !self.inner.is_updated_this_epoch() { + return Err(StakePoolError::StakeListAndPoolOutOfDate.into()); + } + let quote = get_withdraw_sol_quote_copied(&self.inner.stake_pool, lst)?; + let curr_reserve_lamports: u64 = self + .reserve_stake_lamports + .ok_or(StakePoolError::WrongStakeStake)? + .into(); + // Adapted from: + // https://github.com/solana-labs/solana-program-library/blob/17a228bb8e36737209ca5d5375415c70da37c311/stake-pool/program/src/processor.rs#L3102-L3116 + let new_reserve_lamports = curr_reserve_lamports.saturating_sub(quote.out_amount); + if new_reserve_lamports < TOTAL_MIN_RESERVE_LAMPORTS { + return Err(StakePoolError::SolWithdrawalTooLarge.into()); + } + Ok(quote) + } + + #[inline] + fn virtual_ix(&self) -> Result { + // spl_stake_pool_withdraw_sol_ix works for all spl-stake-pool like + // (spl, sanctum-spl, sanctum-spl-multi) because the accounts interface is the exact same + Ok(spl_stake_pool_withdraw_sol_ix( + SplStakePoolWithdrawSolKeys { + spl_stake_pool_program: self.inner.stake_pool_program, + withdraw_sol_spl_stake_pool: self.inner.stake_pool_addr, + withdraw_sol_withdraw_authority: self.inner.withdraw_authority_addr(), + withdraw_sol_reserve_stake: self.inner.stake_pool.reserve_stake, + withdraw_sol_manager_fee: self.inner.stake_pool.manager_fee_account, + clock: sysvar::clock::ID, + stake_history: sysvar::stake_history::ID, + stake_program: stake::program::ID, + withdraw_sol_token_program: self.inner.stake_pool.token_program_id, + }, + )?) + } + + #[inline] + fn accounts_len(&self) -> usize { + SPL_STAKE_POOL_WITHDRAW_SOL_IX_ACCOUNTS_LEN + } + + fn underlying_liquidity(&self) -> Option<&Pubkey> { + Some(&self.inner.stake_pool.reserve_stake) + } +} + +// Assumes +// - manager fee account is a valid token account (fees will be 0 otherwise) +fn get_withdraw_sol_quote_copied(sp: &StakePool, pool_tokens: u64) -> Result { + // Copied from + // https://github.com/solana-labs/solana-program-library/blob/17a228bb8e36737209ca5d5375415c70da37c311/stake-pool/program/src/processor.rs#L3066-L3094 + let pool_tokens_fee = sp + .calc_pool_tokens_sol_withdrawal_fee(pool_tokens) + .ok_or(StakePoolError::CalculationFailure)?; + let pool_tokens_burnt = pool_tokens + .checked_sub(pool_tokens_fee) + .ok_or(StakePoolError::CalculationFailure)?; + let withdraw_lamports = sp + .calc_lamports_withdraw_amount(pool_tokens_burnt) + .ok_or(StakePoolError::CalculationFailure)?; + if withdraw_lamports == 0 { + return Err(StakePoolError::WithdrawalTooSmall.into()); + } + + // estimate pool_tokens_fee in terms of SOL instead of LST + let est_lamports_fee = sp + // calc_lamports_withdraw_amount() is just pool_tokens * pool_lamports / pool_mint_supply + .calc_lamports_withdraw_amount(pool_tokens_fee) + .ok_or(StakePoolError::CalculationFailure)?; + Ok(WithdrawSolQuote { + in_amount: pool_tokens, + out_amount: withdraw_lamports, + fee_amount: est_lamports_fee, + }) +} diff --git a/libs/spl_stake_pool/src/stakedex_traits/withdraw_stake.rs b/libs/spl_stake_pool/src/stakedex_traits/withdraw_stake.rs index 83343d8..26e7e9b 100644 --- a/libs/spl_stake_pool/src/stakedex_traits/withdraw_stake.rs +++ b/libs/spl_stake_pool/src/stakedex_traits/withdraw_stake.rs @@ -9,7 +9,7 @@ use stakedex_withdraw_stake_interface::{ SPL_STAKE_POOL_WITHDRAW_STAKE_IX_ACCOUNTS_LEN, }; -use crate::SplStakePoolStakedex; +use crate::{SplStakePoolStakedex, SplStakePoolStakedexWithWithdrawSol}; pub struct WithdrawStakeQuoteIter<'a> { pool: &'a SplStakePoolStakedex, @@ -140,3 +140,29 @@ impl WithdrawStakeBase for SplStakePoolStakedex { Some(&self.stake_pool_addr) } } + +impl WithdrawStakeIter for SplStakePoolStakedexWithWithdrawSol { + type Iter<'me> = WithdrawStakeQuoteIter<'me>; + + #[inline] + fn withdraw_stake_quote_iter(&self, withdraw_amount: u64) -> Self::Iter<'_> { + self.inner.withdraw_stake_quote_iter(withdraw_amount) + } +} + +impl WithdrawStakeBase for SplStakePoolStakedexWithWithdrawSol { + #[inline] + fn can_accept_stake_withdrawals(&self) -> bool { + self.inner.can_accept_stake_withdrawals() + } + + #[inline] + fn virtual_ix(&self, quote: &WithdrawStakeQuote) -> Result { + self.inner.virtual_ix(quote) + } + + #[inline] + fn accounts_len(&self) -> usize { + self.inner.accounts_len() + } +} diff --git a/stakedex_sdk/src/lib.rs b/stakedex_sdk/src/lib.rs index 718736c..3cfa58c 100644 --- a/stakedex_sdk/src/lib.rs +++ b/stakedex_sdk/src/lib.rs @@ -10,21 +10,22 @@ use spl_token::native_mint; use stakedex_interface::{ DepositStakeKeys, PrefundSwapViaStakeIxArgs, PrefundSwapViaStakeKeys, PrefundWithdrawStakeIxArgs, PrefundWithdrawStakeKeys, StakeWrappedSolIxArgs, - StakeWrappedSolKeys, SwapViaStakeArgs, + StakeWrappedSolKeys, SwapViaStakeArgs, WithdrawWrappedSolIxArgs, WithdrawWrappedSolKeys, }; use stakedex_jup_interface::{ manual_concat_get_account_metas, prefund_get_account_metas, quote_pool_pair, DepositSolWrapper, - OneWayPoolPair, PrefundRepayParams, TwoWayPoolPair, + DepositWithdrawSolWrapper, OneWayPoolPair, PrefundRepayParams, TwoWayPoolPair, }; use stakedex_lido::LidoStakedex; use stakedex_marinade::MarinadeStakedex; use stakedex_sdk_common::{ - find_fee_token_acc, lido_state, marinade_state, msol, stakedex_program, stsol, - unstake_it_program, wsol_bridge_in, BaseStakePoolAmm, DepositSol, DepositStake, - DepositStakeInfo, DepositStakeQuote, InitFromKeyedAccount, WithdrawStake, WithdrawStakeQuote, - DEPOSIT_STAKE_DST_TOKEN_ACCOUNT_INDEX, + find_fee_token_acc, lido_state, marinade_state, msol, + stakedex_program::{self, WSOL_FEE_TOKEN_ACCOUNT_ID}, + stsol, unstake_it_program, wsol, wsol_bridge_in, BaseStakePoolAmm, DepositSol, DepositStake, + DepositStakeInfo, DepositStakeQuote, InitFromKeyedAccount, WithdrawSol, WithdrawStake, + WithdrawStakeQuote, DEPOSIT_STAKE_DST_TOKEN_ACCOUNT_INDEX, }; -use stakedex_spl_stake_pool::SplStakePoolStakedex; +use stakedex_spl_stake_pool::{SplStakePoolStakedex, SplStakePoolStakedexWithWithdrawSol}; use stakedex_unstake_it::{UnstakeItStakedex, UnstakeItStakedexPrefund}; pub use stakedex_interface::ID as stakedex_program_id; @@ -55,9 +56,10 @@ macro_rules! match_same_stakedex { }; } +/// Collection of all supported stake pools #[derive(Clone, Default)] pub struct Stakedex { - pub spls: Vec, + pub spls: Vec, pub unstakeit: UnstakeItStakedexPrefund, pub marinade: MarinadeStakedex, pub lido: LidoStakedex, @@ -150,7 +152,10 @@ impl Stakedex { get_keyed_account(accounts, &pool) .map_or_else(Err, |mut ka| { ka.params = Some(name.as_str().into()); - SplStakePoolStakedex::from_keyed_account(&ka, amm_context) + Ok(SplStakePoolStakedexWithWithdrawSol { + inner: SplStakePoolStakedex::from_keyed_account(&ka, amm_context)?, + reserve_stake_lamports: None, + }) }) .ok() } @@ -225,31 +230,50 @@ impl Stakedex { pub fn get_deposit_sol_pool(&self, mint: &Pubkey) -> Option<&dyn DepositSol> { Some(match *mint { msol::ID => &self.marinade, - mint => self - .spls - .iter() - .find(|SplStakePoolStakedex { stake_pool, .. }| stake_pool.pool_mint == mint)?, + mint => self.spls.iter().find( + |SplStakePoolStakedexWithWithdrawSol { + inner: SplStakePoolStakedex { stake_pool, .. }, + .. + }| stake_pool.pool_mint == mint, + )?, }) } + pub fn get_withdraw_sol_pool(&self, mint: &Pubkey) -> Option<&dyn WithdrawSol> { + // right now only spls can WithdrawSol + self.spls + .iter() + .find( + |SplStakePoolStakedexWithWithdrawSol { + inner: SplStakePoolStakedex { stake_pool, .. }, + .. + }| stake_pool.pool_mint == *mint, + ) + .map(|sp| sp as &dyn WithdrawSol) + } + pub fn get_deposit_stake_pool(&self, mint: &Pubkey) -> Option<&dyn DepositStake> { Some(match *mint { msol::ID => &self.marinade, native_mint::ID => &self.unstakeit, - mint => self - .spls - .iter() - .find(|SplStakePoolStakedex { stake_pool, .. }| stake_pool.pool_mint == mint)?, + mint => self.spls.iter().find( + |SplStakePoolStakedexWithWithdrawSol { + inner: SplStakePoolStakedex { stake_pool, .. }, + .. + }| stake_pool.pool_mint == mint, + )?, }) } pub fn get_withdraw_stake_pool(&self, mint: &Pubkey) -> Option<&dyn WithdrawStake> { Some(match *mint { stsol::ID => &self.lido, - mint => self - .spls - .iter() - .find(|SplStakePoolStakedex { stake_pool, .. }| stake_pool.pool_mint == mint)?, + mint => self.spls.iter().find( + |SplStakePoolStakedexWithWithdrawSol { + inner: SplStakePoolStakedex { stake_pool, .. }, + .. + }| stake_pool.pool_mint == mint, + )?, }) } @@ -447,6 +471,38 @@ impl Stakedex { Ok(ix) } + pub fn quote_withdraw_wrapped_sol(&self, quote_params: &QuoteParams) -> Result { + let withdraw_from = self + .get_withdraw_sol_pool("e_params.input_mint) + .ok_or_else(|| anyhow!("pool not found for input mint {}", quote_params.input_mint))?; + let withdraw_sol_quote = withdraw_from.get_withdraw_sol_quote(quote_params.amount)?; + let quote = withdraw_from.convert_quote(withdraw_sol_quote); + Ok(quote) + } + + pub fn withdraw_wrapped_sol_ix(&self, swap_params: &SwapParams) -> Result { + let withdraw_from = self + .get_withdraw_sol_pool(&swap_params.source_mint) + .ok_or_else(|| anyhow!("pool not found for src mint {}", swap_params.source_mint))?; + let mut ix = stakedex_interface::withdraw_wrapped_sol_ix( + WithdrawWrappedSolKeys { + user: swap_params.token_transfer_authority, + wsol_mint: wsol::ID, + token_program: spl_token::ID, + src_token_from: swap_params.source_token_account, + wsol_to: swap_params.destination_token_account, + wsol_fee_token_account: WSOL_FEE_TOKEN_ACCOUNT_ID, + src_token_mint: swap_params.source_mint, + }, + WithdrawWrappedSolIxArgs { + amount: swap_params.in_amount, + }, + )?; + let withdraw_sol_virtual_ix = withdraw_from.virtual_ix()?; + ix.accounts.extend(withdraw_sol_virtual_ix.accounts); + Ok(ix) + } + /// input_mint = voter pubkey for deposit stake pub fn quote_deposit_stake(&self, quote_params: &QuoteParams) -> Result { let (deposit_to, dsq) = self.quote_deposit_stake_dsq( @@ -509,7 +565,7 @@ impl Stakedex { pub fn get_amms(self) -> Vec> { #[derive(Clone)] enum Stakedex { - SplStakePool(SplStakePoolStakedex), + SplStakePool(SplStakePoolStakedexWithWithdrawSol), UnstakeIt(UnstakeItStakedexPrefund), Marinade(MarinadeStakedex), Lido(LidoStakedex), @@ -536,7 +592,7 @@ impl Stakedex { for stakedex in stakedexes.iter() { match stakedex { Stakedex::SplStakePool(spl_stake_pool) => { - amms.push(Box::new(DepositSolWrapper(spl_stake_pool.clone()))) + amms.push(Box::new(DepositWithdrawSolWrapper(spl_stake_pool.clone()))) } Stakedex::Marinade(marinade) => { amms.push(Box::new(DepositSolWrapper(marinade.clone()))) @@ -554,16 +610,16 @@ impl Stakedex { for (first_stakedex, second_stakedex) in stakedexes.into_iter().tuple_combinations() { match (first_stakedex, second_stakedex) { (Stakedex::SplStakePool(p1), Stakedex::SplStakePool(p2)) => { - amms.push(Box::new(TwoWayPoolPair::new(p1, p2))); + amms.push(Box::new(TwoWayPoolPair::new(p1.inner, p2.inner))); } match_stakedexes!(SplStakePool, Marinade, withdraw, deposit) => { - amms.push(Box::new(OneWayPoolPair::new(withdraw, deposit))); + amms.push(Box::new(OneWayPoolPair::new(withdraw.inner, deposit))); } match_stakedexes!(SplStakePool, UnstakeIt, withdraw, deposit) => { - amms.push(Box::new(OneWayPoolPair::new(withdraw, deposit))); + amms.push(Box::new(OneWayPoolPair::new(withdraw.inner, deposit))); } match_stakedexes!(Lido, SplStakePool, withdraw, deposit) => { - amms.push(Box::new(OneWayPoolPair::new(withdraw, deposit))); + amms.push(Box::new(OneWayPoolPair::new(withdraw, deposit.inner))); } match_stakedexes!(Lido, UnstakeIt, withdraw, deposit) => { amms.push(Box::new(OneWayPoolPair::new(withdraw, deposit))); diff --git a/stakedex_sdk/tests/test_main.rs b/stakedex_sdk/tests/test_main.rs index 197fa4a..9c53997 100644 --- a/stakedex_sdk/tests/test_main.rs +++ b/stakedex_sdk/tests/test_main.rs @@ -395,9 +395,8 @@ fn setup_swap_via_stake( ) -> (u64, Vec, u64, u64) { let source_balance = RPC .get_token_account_balance(&src_token_acc) - .map_err(|err| { - println!("Could not swap {} to {}", input_mint, output_mint); - err + .inspect_err(|err| { + println!("Could not swap {input_mint} to {output_mint}: {err}"); }) .unwrap();