diff --git a/src/program-rust/src/error.rs b/src/program-rust/src/error.rs index c159a1d..10979f9 100644 --- a/src/program-rust/src/error.rs +++ b/src/program-rust/src/error.rs @@ -219,6 +219,9 @@ pub enum ProtocolError { #[error("invalid crema swap account data")] InvalidCremaSwapAccountData, + #[error("invalid whirlpool info account data")] + InvalidWhirlpoolInfoAccountData, + #[error("overflow")] Overflow, } @@ -332,6 +335,9 @@ impl PrintProgramError for ProtocolError { ProtocolError::InvalidPoolMint => { msg!("Error: InvalidPoolMint") } + ProtocolError::InvalidWhirlpoolInfoAccountData => { + msg!("Error: InvalidWhirlpoolInfoAccountData") + } ProtocolError::Overflow => { msg!("Error: Overflow") } diff --git a/src/program-rust/src/exchanger/whirlpool/instruction.rs b/src/program-rust/src/exchanger/whirlpool/instruction.rs index 19774b6..e1481fa 100644 --- a/src/program-rust/src/exchanger/whirlpool/instruction.rs +++ b/src/program-rust/src/exchanger/whirlpool/instruction.rs @@ -219,8 +219,8 @@ mod test { sqrt_price_limit: 79226673515401279992447579055, exact_input: true, a_to_b: false, - }).pack(); + }) + .pack(); assert_eq!(data1, data2) } } - diff --git a/src/program-rust/src/instruction.rs b/src/program-rust/src/instruction.rs index 7b10e9e..252b597 100644 --- a/src/program-rust/src/instruction.rs +++ b/src/program-rust/src/instruction.rs @@ -1,6 +1,7 @@ //! Instruction types +#![allow(clippy::ptr_offset_with_cast)] -use crate::error::ProtocolError; +use crate::{error::ProtocolError, parser::whirlpool}; use arrayref::{array_ref, array_refs}; use solana_program::program_error::ProgramError; use std::num::NonZeroU64; @@ -24,6 +25,8 @@ pub enum ExchangerType { AldrinExchange, /// CropperFinance CropperFinance, + /// Whirlpool + Whirlpool, } impl ExchangerType { @@ -37,6 +40,7 @@ impl ExchangerType { 5 => Some(ExchangerType::CremaFinance), 6 => Some(ExchangerType::AldrinExchange), 7 => Some(ExchangerType::CropperFinance), + 8 => Some(ExchangerType::Whirlpool), _ => None, } } @@ -58,6 +62,8 @@ pub struct SwapInstruction { pub expect_amount_out: NonZeroU64, /// Minimum amount of DESTINATION token to output, prevents excessive slippage pub minimum_amount_out: NonZeroU64, + /// sqrt_price_limit + pub sqrt_price_limit: Option, } /// Swap instruction data @@ -65,6 +71,8 @@ pub struct SwapInstruction { pub struct SwapInInstruction { /// amount of tokens to swap pub amount_in: NonZeroU64, + /// sqrt_price_limit + pub sqrt_price_limit: Option, } /// Swap instruction data @@ -74,13 +82,8 @@ pub struct SwapOutInstruction { pub expect_amount_out: NonZeroU64, /// Minimum amount of DESTINATION token to output, prevents excessive slippage pub minimum_amount_out: NonZeroU64, -} - -/// Swap instruction data -#[derive(Clone, Debug, PartialEq)] -pub struct SwapOutSlimInstruction { - /// Minimum amount of DESTINATION token to output, prevents excessive slippage - pub minimum_amount_out: NonZeroU64, + /// sqrt_price_limit + pub sqrt_price_limit: Option, } // Instructions supported by the 1sol protocol program @@ -381,7 +384,7 @@ pub enum ProtocolInstruction { /// 17. `[writable]` raydium pc_vault account. /// 18. `[]` raydium vault_signer account. /// 19. `[]` raydium program id. - SwapRaydiumOut2(SwapOutSlimInstruction), + SwapRaydiumOut2(SwapOutInstruction), /// Swap direct by CremaFinance /// @@ -535,6 +538,61 @@ pub enum ProtocolInstruction { /// 12. `[writable]` AldrinExchange Pool fee account. /// 13. '[]` AldrinExchange program id. SwapCropperFinanceOut(SwapOutInstruction), + + /// Swap direct by Whirlpool + /// + /// 0. `[writable]` User token SOURCE Account, (coin_wallet) + /// 1. `[writable]` User token DESTINATION Account to swap INTO. Must be the DESTINATION token. + /// 2. `[signer]` User token SOURCE account OWNER (or Authority) account. + /// 3. '[]` Token program id + /// 4. `[writable]` fee token account + /// + /// 5. `[writable]` Whirlpool pool account. + /// 6. `[writable]` Whirlpool token vault a. + /// 7. `[writable]` Whirlpool token vault b. + /// 8. `[writable]` Whirlpool TickArray 0. + /// 9. `[writable]` Whirlpool TickArray 1. + /// 10. `[writable]` Whirlpool TickArray 2. + /// 11. `[]` Whirlpool Oracle Account. + /// 12. '[]` Whirlpool program id. + SwapWhirlpool(SwapInstruction), + + /// SwapIn by Whirlpool + /// + /// 0. `[writable]` User token SOURCE Account, (coin_wallet). + /// 1. `[writable]` User token DESTINATION Account to swap INTO. Must be the DESTINATION token. + /// 2. `[signer]` User token SOURCE account OWNER (or Authority) account. + /// 3. '[writable]` Protocol SwapInfo account + /// 4. '[]` Token program id. + /// + /// 5. `[writable]` Whirlpool pool account. + /// 6. `[writable]` Whirlpool token vault a. + /// 7. `[writable]` Whirlpool token vault b. + /// 8. `[writable]` Whirlpool TickArray 0. + /// 9. `[writable]` Whirlpool TickArray 1. + /// 10. `[writable]` Whirlpool TickArray 2. + /// 11. `[]` Whirlpool Oracle Account. + /// 12. '[]` Whirlpool program id. + SwapInWhirlpool(SwapInInstruction), + + /// SwapOut by Whirlpool + /// + /// 0. `[writable]` User token SOURCE Account, (coin_wallet). + /// 1. `[writable]` User token DESTINATION Account to swap INTO. Must be the DESTINATION token. + /// 2. `[signer]` User token SOURCE account OWNER (or Authority) account. + /// 3. '[writable]` SwapInfo account + /// 4. '[]` Token program id. + /// 5. `[writable]` fee token account. + /// + /// 6. `[writable]` Whirlpool pool account. + /// 7. `[writable]` Whirlpool token vault a. + /// 8. `[writable]` Whirlpool token vault b. + /// 9. `[writable]` Whirlpool TickArray 0. + /// 10. `[writable]` Whirlpool TickArray 1. + /// 11. `[writable]` Whirlpool TickArray 2. + /// 12. `[]` Whirlpool Oracle Account. + /// 13. '[]` Whirlpool program id. + SwapOutWhirlpool(SwapOutInstruction), } impl ProtocolInstruction { @@ -542,51 +600,90 @@ impl ProtocolInstruction { pub fn unpack(input: &[u8]) -> Result { let (&tag, rest) = input.split_first().ok_or(ProtocolError::InvalidInput)?; Ok(match tag { - 3 => Self::SwapSplTokenSwap(SwapInstruction::unpack(rest)?), - 4 => Self::SwapSerumDex(SwapInstruction::unpack(rest)?), + 3 => Self::SwapSplTokenSwap(SwapInstruction::unpack(rest, ExchangerType::SplTokenSwap)?), + 4 => Self::SwapSerumDex(SwapInstruction::unpack(rest, ExchangerType::SerumDex)?), 5 => return Err(ProtocolError::InvalidInstruction.into()), - 6 => Self::SwapStableSwap(SwapInstruction::unpack(rest)?), + 6 => Self::SwapStableSwap(SwapInstruction::unpack(rest, ExchangerType::StableSwap)?), 8 => return Err(ProtocolError::InvalidInstruction.into()), - 9 => Self::SwapRaydiumSwap(SwapInstruction::unpack(rest)?), + 9 => Self::SwapRaydiumSwap(SwapInstruction::unpack(rest, ExchangerType::RaydiumSwap)?), 10 => Self::InitializeSwapInfo, 11 => Self::SetupSwapInfo, - 12 => Self::SwapSplTokenSwapIn(SwapInInstruction::unpack(rest)?), - 13 => Self::SwapSplTokenSwapOut(SwapOutInstruction::unpack(rest)?), - 14 => Self::SwapSerumDexIn(SwapInInstruction::unpack(rest)?), - 15 => Self::SwapSerumDexOut(SwapOutInstruction::unpack(rest)?), - 16 => Self::SwapStableSwapIn(SwapInInstruction::unpack(rest)?), - 17 => Self::SwapStableSwapOut(SwapOutInstruction::unpack(rest)?), - 18 => Self::SwapRaydiumIn(SwapInInstruction::unpack(rest)?), - 19 => Self::SwapRaydiumOut(SwapOutInstruction::unpack(rest)?), - 20 => Self::SwapRaydiumIn2(SwapInInstruction::unpack(rest)?), - 21 => Self::SwapRaydiumOut2(SwapOutSlimInstruction::unpack(rest)?), - 22 => Self::SwapCremaFinance(SwapInstruction::unpack(rest)?), - 23 => Self::SwapCremaFinanceIn(SwapInInstruction::unpack(rest)?), - 24 => Self::SwapCremaFinanceOut(SwapOutInstruction::unpack(rest)?), - 25 => Self::SwapAldrinExchange(SwapInstruction::unpack(rest)?), - 26 => Self::SwapAldrinExchangeIn(SwapInInstruction::unpack(rest)?), - 27 => Self::SwapAldrinExchangeOut(SwapOutInstruction::unpack(rest)?), - 28 => Self::SwapCropperFinance(SwapInstruction::unpack(rest)?), - 29 => Self::SwapCropperFinanceIn(SwapInInstruction::unpack(rest)?), - 30 => Self::SwapCropperFinanceOut(SwapOutInstruction::unpack(rest)?), + 12 => Self::SwapSplTokenSwapIn(SwapInInstruction::unpack( + rest, + ExchangerType::SplTokenSwap, + )?), + 13 => Self::SwapSplTokenSwapOut(SwapOutInstruction::unpack( + rest, + ExchangerType::SplTokenSwap, + )?), + 14 => Self::SwapSerumDexIn(SwapInInstruction::unpack(rest, ExchangerType::SerumDex)?), + 15 => Self::SwapSerumDexOut(SwapOutInstruction::unpack(rest, ExchangerType::SerumDex)?), + 16 => Self::SwapStableSwapIn(SwapInInstruction::unpack(rest, ExchangerType::StableSwap)?), + 17 => Self::SwapStableSwapOut(SwapOutInstruction::unpack(rest, ExchangerType::StableSwap)?), + // Self::SwapRaydiumIn(SwapInInstruction::unpack(rest)?) + 18 => return Err(ProtocolError::InvalidInstruction.into()), + //, Self::SwapRaydiumOut(SwapOutInstruction::unpack(rest)?), + 19 => return Err(ProtocolError::InvalidInstruction.into()), + 20 => Self::SwapRaydiumIn2(SwapInInstruction::unpack(rest, ExchangerType::RaydiumSwap)?), + 21 => Self::SwapRaydiumOut2(SwapOutInstruction::unpack( + rest, + ExchangerType::RaydiumSwap, + )?), + 22 => Self::SwapCremaFinance(SwapInstruction::unpack(rest, ExchangerType::CremaFinance)?), + 23 => Self::SwapCremaFinanceIn(SwapInInstruction::unpack( + rest, + ExchangerType::CremaFinance, + )?), + 24 => Self::SwapCremaFinanceOut(SwapOutInstruction::unpack( + rest, + ExchangerType::CremaFinance, + )?), + 25 => Self::SwapAldrinExchange(SwapInstruction::unpack( + rest, + ExchangerType::AldrinExchange, + )?), + 26 => Self::SwapAldrinExchangeIn(SwapInInstruction::unpack( + rest, + ExchangerType::AldrinExchange, + )?), + 27 => Self::SwapAldrinExchangeOut(SwapOutInstruction::unpack( + rest, + ExchangerType::AldrinExchange, + )?), + 28 => Self::SwapCropperFinance(SwapInstruction::unpack( + rest, + ExchangerType::CropperFinance, + )?), + 29 => Self::SwapCropperFinanceIn(SwapInInstruction::unpack( + rest, + ExchangerType::CropperFinance, + )?), + 30 => Self::SwapCropperFinanceOut(SwapOutInstruction::unpack( + rest, + ExchangerType::CropperFinance, + )?), 31 => Self::CloseSwapInfo, + 32 => Self::SwapWhirlpool(SwapInstruction::unpack(rest, ExchangerType::Whirlpool)?), + 33 => Self::SwapInWhirlpool(SwapInInstruction::unpack(rest, ExchangerType::Whirlpool)?), + 34 => Self::SwapOutWhirlpool(SwapOutInstruction::unpack(rest, ExchangerType::Whirlpool)?), _ => return Err(ProtocolError::InvalidInstruction.into()), }) } } impl SwapInstruction { - const DATA_LEN: usize = 24; + const MIN_DATA_LEN: usize = 24; // size = 1 or 3 // flag[0/1], [account_size], [amount_in], [minium_amount_out] - fn unpack(input: &[u8]) -> Result { - if input.len() < SwapInstruction::DATA_LEN { + fn unpack(input: &[u8], exchanger_type: ExchangerType) -> Result { + if input.len() < SwapInstruction::MIN_DATA_LEN { return Err(ProtocolError::InvalidInput.into()); } - let arr_data = array_ref![input, 0, SwapInstruction::DATA_LEN]; + let (fixed_arr, other_arr) = array_refs![input, SwapInstruction::MIN_DATA_LEN; ..;]; let (&amount_in_arr, &expect_amount_out_arr, &minimum_amount_out_arr) = - array_refs![arr_data, 8, 8, 8]; + array_refs![fixed_arr, 8, 8, 8]; + let amount_in = NonZeroU64::new(u64::from_le_bytes(amount_in_arr)).ok_or(ProtocolError::InvalidInput)?; let expect_amount_out = NonZeroU64::new(u64::from_le_bytes(expect_amount_out_arr)) @@ -596,69 +693,90 @@ impl SwapInstruction { if expect_amount_out.get() < minimum_amount_out.get() || expect_amount_out.get() == 0 { return Err(ProtocolError::InvalidExpectAmountOut.into()); } + let sqrt_price_limit = match exchanger_type { + ExchangerType::Whirlpool => Some(whirlpool::WhirlpoolArgs::unpack_input(other_arr)?), + _ => None, + }; Ok(SwapInstruction { amount_in, expect_amount_out, minimum_amount_out, + sqrt_price_limit, }) } } impl SwapInInstruction { - const DATA_LEN: usize = 8; + const MIN_DATA_LEN: usize = 8; // size = 1 or 3 // flag[0/1], [account_size], [amount_in], [minium_amount_out] - fn unpack(input: &[u8]) -> Result { - if input.len() < SwapInInstruction::DATA_LEN { + fn unpack(input: &[u8], exchanger_type: ExchangerType) -> Result { + if input.len() < SwapInInstruction::MIN_DATA_LEN { return Err(ProtocolError::InvalidInput.into()); } - let &amount_in_arr = array_ref![input, 0, SwapInInstruction::DATA_LEN]; + let (fixed_arr, other_arr) = array_refs![input, SwapInInstruction::MIN_DATA_LEN; ..;]; + let &amount_in_arr = array_ref![fixed_arr, 0, 8]; let amount_in = NonZeroU64::new(u64::from_le_bytes(amount_in_arr)).ok_or(ProtocolError::InvalidInput)?; - Ok(Self { amount_in }) + + let sqrt_price_limit = match exchanger_type { + ExchangerType::Whirlpool => Some(whirlpool::WhirlpoolArgs::unpack_input(other_arr)?), + _ => None, + }; + Ok(Self { + amount_in, + sqrt_price_limit, + }) } } impl SwapOutInstruction { - const DATA_LEN: usize = 16; - // size = 1 or 3 // flag[0/1], [account_size], [amount_in], [minium_amount_out] - fn unpack(input: &[u8]) -> Result { - if input.len() < SwapOutInstruction::DATA_LEN { - return Err(ProtocolError::InvalidInput.into()); - } - let arr_data = array_ref![input, 0, SwapOutInstruction::DATA_LEN]; - let (&expect_amount_out_arr, &minimum_amount_out_arr) = array_refs![arr_data, 8, 8]; - let expect_amount_out = NonZeroU64::new(u64::from_le_bytes(expect_amount_out_arr)) - .ok_or(ProtocolError::InvalidInput)?; - let minimum_amount_out = NonZeroU64::new(u64::from_le_bytes(minimum_amount_out_arr)) - .ok_or(ProtocolError::InvalidInput)?; - if expect_amount_out.get() < minimum_amount_out.get() || expect_amount_out.get() == 0 { - return Err(ProtocolError::InvalidExpectAmountOut.into()); - } + fn unpack(input: &[u8], exchanger_type: ExchangerType) -> Result { + let (expect_amount_out, minimum_amount_out, other_arr) = match exchanger_type { + ExchangerType::RaydiumSwapSlim => { + if input.len() < 8 { + return Err(ProtocolError::InvalidInput.into()); + } + let (fixed_arr, other_arr) = array_refs![input, 8; ..;]; + let &minimum_amount_out = array_ref![fixed_arr, 0, 8]; + let minimum_amount_out = NonZeroU64::new(u64::from_le_bytes(minimum_amount_out)) + .ok_or(ProtocolError::InvalidInput)?; + (minimum_amount_out, minimum_amount_out, other_arr) + } + _ => { + if input.len() < 16 { + return Err(ProtocolError::InvalidInput.into()); + } + let (fixed_arr, other_arr) = array_refs![input, 16; ..;]; + let (&expect_amount_out_arr, &minimum_amount_out_arr) = array_refs![fixed_arr, 8, 8]; + let expect_amount_out = NonZeroU64::new(u64::from_le_bytes(expect_amount_out_arr)) + .ok_or(ProtocolError::InvalidInput)?; + let minimum_amount_out = NonZeroU64::new(u64::from_le_bytes(minimum_amount_out_arr)) + .ok_or(ProtocolError::InvalidInput)?; + if expect_amount_out.get() < minimum_amount_out.get() || expect_amount_out.get() == 0 { + return Err(ProtocolError::InvalidExpectAmountOut.into()); + } + (expect_amount_out, minimum_amount_out, other_arr) + } + }; + + let sqrt_price_limit = if exchanger_type == ExchangerType::Whirlpool { + Some(whirlpool::WhirlpoolArgs::unpack_input(other_arr)?) + } else { + None + }; + Ok(Self { expect_amount_out, minimum_amount_out, + sqrt_price_limit, }) } } -impl SwapOutSlimInstruction { - const DATA_LEN: usize = 8; - - fn unpack(input: &[u8]) -> Result { - if input.len() < SwapOutSlimInstruction::DATA_LEN { - return Err(ProtocolError::InvalidInput.into()); - } - let &minimum_amount_out_arr = array_ref![input, 0, SwapOutSlimInstruction::DATA_LEN]; - let minimum_amount_out = NonZeroU64::new(u64::from_le_bytes(minimum_amount_out_arr)) - .ok_or(ProtocolError::InvalidInput)?; - Ok(Self { minimum_amount_out }) - } -} - #[cfg(test)] mod tests { use super::*; @@ -668,15 +786,16 @@ mod tests { let amount_in = 120000u64; let minimum_amount_out = 1080222u64; let expect_amount_out = 1090000u64; - let mut buf = Vec::with_capacity(SwapInstruction::DATA_LEN); + let mut buf = Vec::with_capacity(SwapInstruction::MIN_DATA_LEN); buf.extend_from_slice(&amount_in.to_le_bytes()); buf.extend_from_slice(&expect_amount_out.to_le_bytes()); buf.extend_from_slice(&minimum_amount_out.to_le_bytes()); // buf.insert(, element) - let i = SwapInstruction::unpack(&buf[..]).unwrap(); + let i = SwapInstruction::unpack(&buf[..], ExchangerType::SplTokenSwap).unwrap(); assert_eq!(i.amount_in.get(), amount_in); assert_eq!(i.expect_amount_out.get(), expect_amount_out); assert_eq!(i.minimum_amount_out.get(), minimum_amount_out); + assert_eq!(i.sqrt_price_limit, None); } } diff --git a/src/program-rust/src/parser/mod.rs b/src/program-rust/src/parser/mod.rs index 4bc1dc5..ca771a7 100644 --- a/src/program-rust/src/parser/mod.rs +++ b/src/program-rust/src/parser/mod.rs @@ -6,6 +6,7 @@ pub mod raydium; pub mod serum_dex; pub mod spl_token_swap; pub mod stable_swap; +pub mod whirlpool; #[macro_export] macro_rules! declare_validated_account_wrapper { diff --git a/src/program-rust/src/parser/whirlpool.rs b/src/program-rust/src/parser/whirlpool.rs new file mode 100644 index 0000000..3274845 --- /dev/null +++ b/src/program-rust/src/parser/whirlpool.rs @@ -0,0 +1,185 @@ +use super::base::TokenAccount; +use crate::{ + declare_validated_account_wrapper, + error::{ProtocolError, ProtocolResult}, +}; +use arrayref::array_ref; +use solana_program::{account_info::AccountInfo, pubkey::Pubkey}; + +declare_validated_account_wrapper!(WhirlpoolInfo, |account: &AccountInfo| { + let account_data = account + .try_borrow_data() + .map_err(|_| ProtocolError::BorrowAccountDataError)?; + if account_data.len() != 653 { + return Err(ProtocolError::InvalidCremaSwapAccountData); + } + Ok(()) +}); + +impl<'a, 'b: 'a> WhirlpoolInfo<'a, 'b> { + #[allow(dead_code)] + pub fn bump(self) -> ProtocolResult { + Ok( + self + .inner() + .try_borrow_data() + .map_err(|_| ProtocolError::BorrowAccountDataError)?[30], + ) + } + + pub fn token_a_account(self) -> ProtocolResult { + let data = self + .inner() + .try_borrow_data() + .map_err(|_| ProtocolError::BorrowAccountDataError)?; + Ok(Pubkey::new_from_array(*array_ref![data, 133, 32])) + } + + pub fn token_b_account(self) -> ProtocolResult { + let data = self + .inner() + .try_borrow_data() + .map_err(|_| ProtocolError::BorrowAccountDataError)?; + Ok(Pubkey::new_from_array(*array_ref![data, 213, 32])) + } + + pub fn token_a_mint(self) -> ProtocolResult { + let data = self + .inner() + .try_borrow_data() + .map_err(|_| ProtocolError::BorrowAccountDataError)?; + Ok(Pubkey::new_from_array(*array_ref![data, 101, 32])) + } + + pub fn token_b_mint(self) -> ProtocolResult { + let data = self + .inner() + .try_borrow_data() + .map_err(|_| ProtocolError::BorrowAccountDataError)?; + Ok(Pubkey::new_from_array(*array_ref![data, 181, 32])) + } +} + +#[derive(Copy, Clone)] +pub struct WhirlpoolArgs<'a, 'b: 'a> { + pub pool: WhirlpoolInfo<'a, 'b>, + pub token_a_account: TokenAccount<'a, 'b>, + pub token_b_account: TokenAccount<'a, 'b>, + pub tick_array_0: &'a AccountInfo<'b>, + pub tick_array_1: &'a AccountInfo<'b>, + pub tick_array_2: &'a AccountInfo<'b>, + pub oracle: &'a AccountInfo<'b>, + pub program_id: &'a AccountInfo<'b>, +} + +impl<'a, 'b: 'a> WhirlpoolArgs<'a, 'b> { + const MIN_ACCOUNTS: usize = 8; + + pub fn with_parsed_args(accounts: &'a [AccountInfo<'b>]) -> ProtocolResult { + if accounts.len() != Self::MIN_ACCOUNTS { + return Err(ProtocolError::InvalidAccountsLength); + } + let &[ + ref pool_acc, + ref token_a_account, + ref token_b_account, + ref tick_array_0, + ref tick_array_1, + ref tick_array_2, + ref oracle, + ref program_id, + ]: &'a[AccountInfo<'b>; WhirlpoolArgs::MIN_ACCOUNTS] = array_ref![accounts, 0, WhirlpoolArgs::MIN_ACCOUNTS]; + + let pool = WhirlpoolInfo::new(pool_acc)?; + if !program_id.executable || *pool_acc.owner != *program_id.key { + return Err(ProtocolError::InvalidProgramAddress); + } + + let token_1_account = TokenAccount::new(token_a_account)?; + let token_2_account = TokenAccount::new(token_b_account)?; + + let pool_token_a = pool.token_a_account()?; + let pool_token_b = pool.token_b_account()?; + + // auto invert vault token account + let (token_a_account, token_b_account) = if *token_1_account.pubkey() == pool_token_a + && *token_2_account.pubkey() == pool_token_b + { + (token_1_account, token_2_account) + } else if *token_1_account.pubkey() == pool_token_b && *token_2_account.pubkey() == pool_token_a + { + (token_2_account, token_1_account) + } else { + return Err(ProtocolError::InvalidTokenMint); + }; + + // validate_authority_pubkey( + // authority.key, + // program_id.key, + // &swap_info_acc.key.to_bytes(), + // swap_info.nonce()?, + // )?; + + Ok(Self { + pool, + token_a_account, + token_b_account, + tick_array_0, + tick_array_1, + tick_array_2, + oracle, + program_id, + }) + } + + pub fn unpack_input(input: &[u8]) -> ProtocolResult { + if input.len() < 16 { + return Err(ProtocolError::InvalidInput); + } + let &sqrt_price_limit = array_ref![input, 0, 16]; + Ok(u128::from_le_bytes(sqrt_price_limit)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use solana_program::account_info::IntoAccountInfo; + use solana_sdk::account::Account; + use std::str::FromStr; + + #[test] + fn test_whirlpool_info() { + let pubkey = Pubkey::from_str("4fuUiYxTQ6QCrdSq9ouBYcTM7bqSwYTSyLueGZLTy4T4").unwrap(); + let program_id = Pubkey::from_str("whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc").unwrap(); + let account_data = "6mtVVAzzExTMtcc3avxFq9Ku8rwbiyuFe4fCqF3YaBFk4qBvzVWz5cbEWmgSqWqREa4MQUzikiPtU9nqRMA3taKkZgedKuAL +ntnGX9SLW2fBkui1M6ms2EoEyWcAFk2g5U2NQs116o6Dd5XX1PB9Wdr462MFWPKZZcaDW96hXb8kbjP6GG1hZoTZsLvVkFjM9RoqR81kcncEDPNrKv96b3Rh +WFgGZVMobs7kQh9jPeRNmzBynjDpSvp6328ubJD5jwSyjV1U2ecNVpwS8NtfFJxxN5KsLCtCvJk1WFJ1YJbWaS9WZigdcx7WRHquU6BpVPi7T9daJb4GHhP3 +1hVsVLTX3i2M8UMPHCUXTRyeHLBDLaiM1GY85C7MdRMpy5YLbbvEvzQ1w3CTz9HmcRRE3SX3SLKNb7YFviYLurx1YPHXnFafEdf9wGQX7qrnYbKP9FqD2SsJ +fAdbp8XaN345172gxfjQk574EWxvanVwqYyyzZoYtYDXV2BKDdkjUUBGRBmfugHeiAEZsThxkEQdpKSUfcTcHfAgohF6E1fXHXC9rkmfB6pHbZNeyAuvkDLN +FTY6QQAQVzCdzqKuxijaapH15JRaaWABY78MijEpvuBsT3QcsrL78adXw8mWfgGWk6tGS23GdveVYv5kSGaFccmrH76CZoJimUNpztCXDPfuBXSYYMnfTsFc +U7BjCt9v4waWk6m64oSjjcy9Rn3fBhmFD8LejB84TFs21uutoYMNetXXpgpu9rRdjSNiyRrH1RAgnRHb6cR5FpjDoFsPygQB1nKUcgGrSXP9PafVCSn6zpJE +N6NCg4jeSqKAij88mkmmPeCRVhFKiN9HLZXc6LXDnNikbR6bxizcUNQ7joRcRDRKU7H7oVLCv8Yf"; + let mut test_account = Account { + lamports: 5435760, + data: bs58::decode(account_data.replace('\n', "")) + .into_vec() + .unwrap(), + owner: program_id, + executable: false, + rent_epoch: 302, + }; + let account_info = (&pubkey, &mut test_account).into_account_info(); + assert!(*account_info.key == pubkey); + let pool = WhirlpoolInfo::new(&account_info).unwrap(); + assert_eq!(pool.bump().unwrap(), 254); + assert_eq!( + pool.token_a_account().unwrap(), + Pubkey::from_str("4oY1eVHJrt7ywuFoQnAZwto4qcQip1QhYMAhD11PU4QL").unwrap() + ); + assert_eq!( + pool.token_b_account().unwrap(), + Pubkey::from_str("4dSG9tKHZR4CAictyEnH9XuGZyKapodWXq5xyg7uFwE9").unwrap() + ); + } +} diff --git a/src/program-rust/src/processor.rs b/src/program-rust/src/processor.rs index 3001526..ca3b29f 100644 --- a/src/program-rust/src/processor.rs +++ b/src/program-rust/src/processor.rs @@ -8,11 +8,10 @@ use crate::{ exchanger::{ aldrin, crema, cropper, raydium, serum_dex::{self, matching::Side as DexSide}, - spl_token_swap, stable_swap, + spl_token_swap, stable_swap, whirlpool, }, instruction::{ ExchangerType, ProtocolInstruction, SwapInInstruction, SwapInstruction, SwapOutInstruction, - SwapOutSlimInstruction, }, parser::{ aldrin::AldrinPoolArgs, @@ -23,6 +22,7 @@ use crate::{ serum_dex::SerumDexArgs, spl_token_swap::SplTokenSwapArgs, stable_swap::StableSwapArgs, + whirlpool::WhirlpoolArgs, }, spl_token, state::{Status, SwapInfo}, @@ -161,6 +161,15 @@ impl Processor { accounts, ExchangerType::CropperFinance, ), + ProtocolInstruction::SwapWhirlpool(data) => { + Self::process_single_step_swap(program_id, &data, accounts, ExchangerType::Whirlpool) + } + ProtocolInstruction::SwapInWhirlpool(data) => { + Self::process_single_step_swap_in(program_id, &data, accounts, ExchangerType::Whirlpool) + } + ProtocolInstruction::SwapOutWhirlpool(data) => { + Self::process_single_step_swap_out(program_id, &data, accounts, ExchangerType::Whirlpool) + } } } @@ -386,6 +395,19 @@ impl Processor { &spl_token_program, other_accounts, ), + ExchangerType::Whirlpool => Self::process_step_whirlpool( + program_id, + data.amount_in.get(), + data.minimum_amount_out.get(), + data + .sqrt_price_limit + .ok_or(ProtocolError::InvalidInstruction)?, + &user_args.token_source_account, + &user_args.token_destination_account, + user_args.source_account_owner, + &spl_token_program, + other_accounts, + ), }?; let from_amount_after = user_args.token_source_account.balance()?; let to_amount_after = user_args.token_destination_account.balance()?; @@ -561,6 +583,19 @@ impl Processor { &spl_token_program, other_accounts, ), + ExchangerType::Whirlpool => Self::process_step_whirlpool( + program_id, + data.amount_in.get(), + u64::MIN + 1, + data + .sqrt_price_limit + .ok_or(ProtocolError::InvalidInstruction)?, + &user_args.token_source_account, + &user_args.token_destination_account, + user_args.source_account_owner, + &spl_token_program, + other_accounts, + ), }?; let from_amount_after = user_args.token_source_account.balance()?; @@ -691,7 +726,7 @@ impl Processor { &spl_token_program, other_accounts, ), - ExchangerType::RaydiumSwapSlim => Self::process_step_raydium_slim( + ExchangerType::SerumDex => Self::process_step_serumdex( program_id, amount_in, amount_out, @@ -701,7 +736,7 @@ impl Processor { &spl_token_program, other_accounts, ), - ExchangerType::SerumDex => Self::process_step_serumdex( + ExchangerType::CremaFinance => Self::process_step_crema_finance( program_id, amount_in, amount_out, @@ -711,7 +746,7 @@ impl Processor { &spl_token_program, other_accounts, ), - ExchangerType::CremaFinance => Self::process_step_crema_finance( + ExchangerType::AldrinExchange => Self::process_step_aldrin_exchange( program_id, amount_in, amount_out, @@ -721,7 +756,7 @@ impl Processor { &spl_token_program, other_accounts, ), - ExchangerType::AldrinExchange => Self::process_step_aldrin_exchange( + ExchangerType::CropperFinance => Self::process_step_cropper_finance( program_id, amount_in, amount_out, @@ -731,16 +766,22 @@ impl Processor { &spl_token_program, other_accounts, ), - ExchangerType::CropperFinance => Self::process_step_cropper_finance( + ExchangerType::Whirlpool => Self::process_step_whirlpool( program_id, amount_in, amount_out, + data + .sqrt_price_limit + .ok_or(ProtocolError::InvalidInstruction)?, &user_args.token_source_account, &user_args.token_destination_account, user_args.source_account_owner, &spl_token_program, other_accounts, ), + _ => { + return Err(ProtocolError::InvalidInstruction.into()); + } }?; let from_amount_after = user_args.token_source_account.balance()?; @@ -795,7 +836,7 @@ impl Processor { pub fn process_single_step_swap_out_slim( program_id: &Pubkey, - data: &SwapOutSlimInstruction, + data: &SwapOutInstruction, accounts: &[AccountInfo], exchanger: ExchangerType, ) -> ProgramResult { @@ -867,36 +908,6 @@ impl Processor { ); match exchanger { - ExchangerType::SplTokenSwap => Self::process_step_tokenswap( - program_id, - amount_in, - amount_out, - &user_args.token_source_account, - &user_args.token_destination_account, - user_args.source_account_owner, - &spl_token_program, - other_accounts, - ), - ExchangerType::StableSwap => Self::process_step_stableswap( - program_id, - amount_in, - amount_out, - &user_args.token_source_account, - &user_args.token_destination_account, - user_args.source_account_owner, - &spl_token_program, - other_accounts, - ), - ExchangerType::RaydiumSwap => Self::process_step_raydium( - program_id, - amount_in, - amount_out, - &user_args.token_source_account, - &user_args.token_destination_account, - user_args.source_account_owner, - &spl_token_program, - other_accounts, - ), ExchangerType::RaydiumSwapSlim => Self::process_step_raydium_slim( program_id, amount_in, @@ -907,46 +918,9 @@ impl Processor { &spl_token_program, other_accounts, ), - ExchangerType::SerumDex => Self::process_step_serumdex( - program_id, - amount_in, - amount_out, - &user_args.token_source_account, - &user_args.token_destination_account, - user_args.source_account_owner, - &spl_token_program, - other_accounts, - ), - ExchangerType::CremaFinance => Self::process_step_crema_finance( - program_id, - amount_in, - amount_out, - &user_args.token_source_account, - &user_args.token_destination_account, - user_args.source_account_owner, - &spl_token_program, - other_accounts, - ), - ExchangerType::AldrinExchange => Self::process_step_aldrin_exchange( - program_id, - amount_in, - amount_out, - &user_args.token_source_account, - &user_args.token_destination_account, - user_args.source_account_owner, - &spl_token_program, - other_accounts, - ), - ExchangerType::CropperFinance => Self::process_step_cropper_finance( - program_id, - amount_in, - amount_out, - &user_args.token_source_account, - &user_args.token_destination_account, - user_args.source_account_owner, - &spl_token_program, - other_accounts, - ), + _ => { + return Err(ProtocolError::InvalidInstruction.into()); + } }?; let from_amount_after = user_args.token_source_account.balance()?; @@ -1584,6 +1558,78 @@ impl Processor { Ok(()) } + /// Step swap in spl-token-swap + #[allow(clippy::too_many_arguments, unused_variables)] + fn process_step_whirlpool<'a, 'b: 'a>( + program_id: &Pubkey, + amount_in: u64, + minimum_amount_out: u64, + sqrt_price_limit: u128, + source_token_account: &TokenAccount<'a, 'b>, + destination_token_account: &TokenAccount<'a, 'b>, + source_account_authority: &'a AccountInfo<'b>, + spl_token_program: &SplTokenProgram<'a, 'b>, + accounts: &'a [AccountInfo<'b>], + ) -> ProgramResult { + sol_log_compute_units(); + + let pool_args = WhirlpoolArgs::with_parsed_args(accounts)?; + let amount_in = Self::get_amount_in(amount_in, source_token_account.balance()?); + + msg!( + "swap using whirlpool, amount_in: {}, minimum_amount_out: {}", + amount_in, + minimum_amount_out, + ); + let pool_token_a_mint = pool_args.pool.token_a_mint()?; + let pool_token_b_mint = pool_args.pool.token_b_mint()?; + let source_token_mint = source_token_account.mint()?; + let destination_token_mint = destination_token_account.mint()?; + + let (user_token_a_account, user_token_b_account, a_to_b) = + if source_token_mint == pool_token_a_mint && destination_token_mint == pool_token_b_mint { + (source_token_account, destination_token_account, true) + } else if source_token_mint == pool_token_b_mint + && destination_token_mint == pool_token_a_mint + { + (destination_token_account, source_token_account, false) + } else { + return Err(ProtocolError::InvalidTokenAccount.into()); + }; + + let swap_accounts = vec![ + pool_args.program_id.clone(), + spl_token_program.inner().clone(), + ]; + + let instruction = whirlpool::instruction::swap( + pool_args.program_id.key, + spl_token_program.inner().key, + source_account_authority.key, + pool_args.pool.pubkey(), + user_token_a_account.inner().key, + pool_args.token_a_account.pubkey(), + user_token_b_account.inner().key, + pool_args.token_b_account.pubkey(), + pool_args.tick_array_0.key, + pool_args.tick_array_1.key, + pool_args.tick_array_2.key, + pool_args.oracle.key, + amount_in, + minimum_amount_out, + 1, + true, + a_to_b, + ); + + msg!("invoke whirlpool swap"); + + sol_log_compute_units(); + invoke(&instruction, &swap_accounts)?; + sol_log_compute_units(); + Ok(()) + } + fn get_amount_in(amount_in: u64, source_token_balance: u64) -> u64 { if source_token_balance < amount_in { source_token_balance