diff --git a/token-swap/js/cli/token-swap-test.js b/token-swap/js/cli/token-swap-test.js index b18abcf51da..1bf5806e798 100644 --- a/token-swap/js/cli/token-swap-test.js +++ b/token-swap/js/cli/token-swap-test.js @@ -98,6 +98,7 @@ export async function createTokenSwap(): Promise { const connection = await getConnection(); const payer = await newAccountWithLamports(connection, 1000000000); owner = await newAccountWithLamports(connection, 1000000000); + const swapPayer = await newAccountWithLamports(connection, 10000000000); const tokenSwapAccount = new Account(); [authority, nonce] = await PublicKey.findProgramAddress( @@ -151,7 +152,6 @@ export async function createTokenSwap(): Promise { await mintB.mintTo(tokenAccountB, owner, [], currentSwapTokenB); console.log('creating token swap'); - const swapPayer = await newAccountWithLamports(connection, 10000000000); tokenSwap = await TokenSwap.createTokenSwap( connection, swapPayer, diff --git a/token-swap/js/client/token-swap.js b/token-swap/js/client/token-swap.js index 9e61aa410be..c52463b767e 100644 --- a/token-swap/js/client/token-swap.js +++ b/token-swap/js/client/token-swap.js @@ -83,6 +83,9 @@ export const TokenSwapLayout: typeof BufferLayout.Structure = BufferLayout.struc Layout.uint64('hostFeeDenominator'), BufferLayout.u8('curveType'), BufferLayout.blob(32, 'curveParameters'), + BufferLayout.u32('freezeAuthorityOption'), + Layout.publicKey('freezeAuthority'), + BufferLayout.u8('freezeAuthorityBitMask'), ], ); @@ -306,6 +309,7 @@ export class TokenSwap { {pubkey: tokenAccountPool, isSigner: false, isWritable: true}, {pubkey: tokenProgramId, isSigner: false, isWritable: false}, ]; + const commandDataLayout = BufferLayout.struct([ BufferLayout.u8('instruction'), BufferLayout.u8('nonce'), @@ -319,6 +323,9 @@ export class TokenSwap { BufferLayout.nu64('hostFeeDenominator'), BufferLayout.u8('curveType'), BufferLayout.blob(32, 'curveParameters'), + BufferLayout.u32('freezeAuthorityOption'), + Layout.publicKey('freezeAuthority'), + BufferLayout.u8('freezeAuthorityBitMask'), ]); let data = Buffer.alloc(1024); { @@ -335,6 +342,9 @@ export class TokenSwap { hostFeeNumerator, hostFeeDenominator, curveType, + freezeAuthorityOption: 0, + freezeAuthority: undefined, + freezeAuthorityBitMask: 0, }, data, ); diff --git a/token-swap/program/fuzz/src/native_token_swap.rs b/token-swap/program/fuzz/src/native_token_swap.rs index fc20c8998e6..175dc26cd11 100644 --- a/token-swap/program/fuzz/src/native_token_swap.rs +++ b/token-swap/program/fuzz/src/native_token_swap.rs @@ -15,7 +15,9 @@ use spl_token_swap::{ use spl_token::instruction::approve; -use solana_program::{bpf_loader, entrypoint::ProgramResult, pubkey::Pubkey, system_program}; +use solana_program::{ + bpf_loader, entrypoint::ProgramResult, program_option::COption, pubkey::Pubkey, system_program, +}; pub struct NativeTokenSwap { pub user_account: NativeAccountData, @@ -89,6 +91,8 @@ impl NativeTokenSwap { nonce, fees.clone(), swap_curve.clone(), + COption::None, + 0, ) .unwrap(); diff --git a/token-swap/program/src/curve/base.rs b/token-swap/program/src/curve/base.rs index dd106b78451..fb5bb1405cf 100644 --- a/token-swap/program/src/curve/base.rs +++ b/token-swap/program/src/curve/base.rs @@ -148,7 +148,6 @@ impl Default for SwapCurve { /// Clone takes advantage of pack / unpack to get around the difficulty of /// cloning dynamic objects. /// Note that this is only to be used for testing. -#[cfg(any(test, feature = "fuzz"))] impl Clone for SwapCurve { fn clone(&self) -> Self { let mut packed_self = [0u8; Self::LEN]; diff --git a/token-swap/program/src/error.rs b/token-swap/program/src/error.rs index e8f3ed762a4..8e340a156f7 100644 --- a/token-swap/program/src/error.rs +++ b/token-swap/program/src/error.rs @@ -91,6 +91,22 @@ pub enum SwapError { /// The operation cannot be performed on the given curve #[error("The operation cannot be performed on the given curve")] UnsupportedCurveOperation, + + /// Attempted to access an invalid bit in the freeze authority bitmask + #[error("Attempted to access an invalid bit in the freeze authority bitmask")] + InvalidBitMaskOperation, + + /// This action has been frozen by the Freeze Authority + #[error("This action has been frozen by the Freeze Authority")] + FrozenAction, + + /// Unauthorized to freeze + #[error("Unauthorized to freeze")] + UnauthorizedToFreeze, + + /// Attempted to set bit mask on Swap V1 + #[error("Attempted to set bit mask on Swap V1")] + SwapV1UnsupportedAction, } impl From for ProgramError { fn from(e: SwapError) -> Self { diff --git a/token-swap/program/src/instruction.rs b/token-swap/program/src/instruction.rs index 3bb389d408c..be832b20010 100644 --- a/token-swap/program/src/instruction.rs +++ b/token-swap/program/src/instruction.rs @@ -7,6 +7,7 @@ use crate::error::SwapError; use solana_program::{ instruction::{AccountMeta, Instruction}, program_error::ProgramError, + program_option::COption, program_pack::Pack, pubkey::Pubkey, }; @@ -27,6 +28,15 @@ pub struct Initialize { /// swap curve info for pool, including CurveType and anything /// else that may be required pub swap_curve: SwapCurve, + /// The freeze authority of the swap. + pub freeze_authority: COption, + /// bits, from right to left - 1 disables, 0 enables the actions: + /// 0. process_swap, + /// 1. process_deposit_all_token_types, + /// 2. process_withdraw_all_token_types, + /// 3. process_deposit_single_token_type_exact_amount_in, + /// 4. process_withdraw_single_token_type_exact_amount_out, + pub freeze_authority_bit_mask: u8, } /// Swap instruction data @@ -92,6 +102,20 @@ pub struct WithdrawSingleTokenTypeExactAmountOut { pub maximum_pool_token_amount: u64, } +/// SetFreezeAuthorityBitMask instruction data +#[cfg_attr(feature = "fuzz", derive(Arbitrary))] +#[repr(C)] +#[derive(Clone, Debug, PartialEq)] +pub struct SetFreezeAuthorityBitMask { + /// bits, from right to left - 1 disables, 0 enables the actions: + /// 0. process_swap, + /// 1. process_deposit_all_token_types, + /// 2. process_withdraw_all_token_types, + /// 3. process_deposit_single_token_type_exact_amount_in, + /// 4. process_withdraw_single_token_type_exact_amount_out, + pub freeze_authority_bit_mask: u8, +} + /// Instructions supported by the token swap program. #[repr(C)] #[derive(Debug, PartialEq)] @@ -107,7 +131,8 @@ pub enum SwapInstruction { /// Must be empty, not owned by swap authority /// 6. `[writable]` Pool Token Account to deposit the initial pool token /// supply. Must be empty, not owned by swap authority. - /// 7. '[]` Token program id + /// 7. '[]` Freeze authority. Optional. + /// 8. '[]` Token program id Initialize(Initialize), /// Swap the tokens in the pool. @@ -187,6 +212,11 @@ pub enum SwapInstruction { /// 8. `[writable]` Fee account, to receive withdrawal fees /// 9. '[]` Token program id WithdrawSingleTokenTypeExactAmountOut(WithdrawSingleTokenTypeExactAmountOut), + + /// Update the freeze authority bit mask. + /// 0. `[writable]` Token-swap + /// 1. `[]` Freeze authority (must be signer) + SetFreezeAuthorityBitMask(SetFreezeAuthorityBitMask), } impl SwapInstruction { @@ -199,11 +229,16 @@ impl SwapInstruction { if rest.len() >= Fees::LEN { let (fees, rest) = rest.split_at(Fees::LEN); let fees = Fees::unpack_unchecked(fees)?; - let swap_curve = SwapCurve::unpack_unchecked(rest)?; + let (swap_curve_data, rest) = rest.split_at(SwapCurve::LEN); + let swap_curve = SwapCurve::unpack_unchecked(swap_curve_data)?; + let (freeze_authority, rest) = Self::unpack_pubkey_option(rest)?; + let freeze_authority_bit_mask = rest[0]; Self::Initialize(Initialize { nonce, fees, swap_curve, + freeze_authority, + freeze_authority_bit_mask, }) } else { return Err(SwapError::InvalidInstruction.into()); @@ -253,6 +288,9 @@ impl SwapInstruction { maximum_pool_token_amount, }) } + 6 => Self::SetFreezeAuthorityBitMask(SetFreezeAuthorityBitMask { + freeze_authority_bit_mask: rest[0], + }), _ => return Err(SwapError::InvalidInstruction.into()), }) } @@ -271,6 +309,28 @@ impl SwapInstruction { } } + fn unpack_pubkey_option(input: &[u8]) -> Result<(COption, &[u8]), ProgramError> { + match input.split_first() { + Option::Some((&0, rest)) => Ok((COption::None, rest)), + Option::Some((&1, rest)) if rest.len() >= 32 => { + let (key, rest) = rest.split_at(32); + let pk = Pubkey::new(key); + Ok((COption::Some(pk), rest)) + } + _ => Err(SwapError::InvalidInstruction.into()), + } + } + + fn pack_pubkey_option(value: &COption, buf: &mut Vec) { + match *value { + COption::Some(ref key) => { + buf.push(1); + buf.extend_from_slice(&key.to_bytes()); + } + COption::None => buf.push(0), + } + } + /// Packs a [SwapInstruction](enum.SwapInstruction.html) into a byte buffer. pub fn pack(&self) -> Vec { let mut buf = Vec::with_capacity(size_of::()); @@ -279,6 +339,8 @@ impl SwapInstruction { nonce, fees, swap_curve, + freeze_authority, + freeze_authority_bit_mask, }) => { buf.push(0); buf.push(*nonce); @@ -288,6 +350,8 @@ impl SwapInstruction { let mut swap_curve_slice = [0u8; SwapCurve::LEN]; Pack::pack_into_slice(swap_curve, &mut swap_curve_slice[..]); buf.extend_from_slice(&swap_curve_slice); + Self::pack_pubkey_option(freeze_authority, &mut buf); + buf.push(*freeze_authority_bit_mask); } Self::Swap(Swap { amount_in, @@ -335,6 +399,13 @@ impl SwapInstruction { buf.extend_from_slice(&destination_token_amount.to_le_bytes()); buf.extend_from_slice(&maximum_pool_token_amount.to_le_bytes()); } + + Self::SetFreezeAuthorityBitMask(SetFreezeAuthorityBitMask { + freeze_authority_bit_mask, + }) => { + buf.push(6); + buf.extend_from_slice(&freeze_authority_bit_mask.to_le_bytes()); + } } buf } @@ -354,11 +425,15 @@ pub fn initialize( nonce: u8, fees: Fees, swap_curve: SwapCurve, + freeze_authority: COption, + freeze_authority_bit_mask: u8, ) -> Result { let init_data = SwapInstruction::Initialize(Initialize { nonce, fees, swap_curve, + freeze_authority, + freeze_authority_bit_mask, }); let data = init_data.pack(); @@ -569,6 +644,28 @@ pub fn swap( }) } +/// Creates a set_freeze_authority_bit_mask instruction +/// Creates a 'swap' instruction. +pub fn set_freeze_authority_bit_mask( + program_id: &Pubkey, + swap_pubkey: &Pubkey, + freeze_authority_pubkey: &Pubkey, + instruction: SetFreezeAuthorityBitMask, +) -> Result { + let data = SwapInstruction::SetFreezeAuthorityBitMask(instruction).pack(); + + let accounts = vec![ + AccountMeta::new(*swap_pubkey, false), + AccountMeta::new_readonly(*freeze_authority_pubkey, true), + ]; + + Ok(Instruction { + program_id: *program_id, + accounts, + data, + }) +} + /// Unpacks a reference from a bytes buffer. /// TODO actually pack / unpack instead of relying on normal memory layout. pub fn unpack(input: &[u8]) -> Result<&T, ProgramError> { @@ -618,6 +715,8 @@ mod tests { nonce, fees, swap_curve, + freeze_authority: COption::None, + freeze_authority_bit_mask: 0, }); let packed = check.pack(); let mut expect = vec![0u8, nonce]; @@ -631,7 +730,7 @@ mod tests { expect.extend_from_slice(&host_fee_denominator.to_le_bytes()); expect.push(curve_type as u8); expect.extend_from_slice(&.to_le_bytes()); - expect.extend_from_slice(&[0u8; 24]); + expect.extend_from_slice(&[0u8; 26]); assert_eq!(packed, expect); let unpacked = SwapInstruction::unpack(&expect).unwrap(); assert_eq!(unpacked, check); diff --git a/token-swap/program/src/processor.rs b/token-swap/program/src/processor.rs index 571d7eae31c..4d6d95b6767 100644 --- a/token-swap/program/src/processor.rs +++ b/token-swap/program/src/processor.rs @@ -1,6 +1,10 @@ //! Program state processor -use crate::constraints::{SwapConstraints, SWAP_CONSTRAINTS}; +use crate::{ + constraints::{SwapConstraints, SWAP_CONSTRAINTS}, + instruction::SetFreezeAuthorityBitMask, + state::SwapV2, +}; use crate::{ curve::{ base::SwapCurve, @@ -12,7 +16,7 @@ use crate::{ DepositAllTokenTypes, DepositSingleTokenTypeExactAmountIn, Initialize, Swap, SwapInstruction, WithdrawAllTokenTypes, WithdrawSingleTokenTypeExactAmountOut, }, - state::{SwapState, SwapV1, SwapVersion}, + state::{SwapState, SwapVersion}, }; use num_traits::FromPrimitive; use solana_program::{ @@ -149,6 +153,43 @@ impl Processor { signers, ) } + fn get_bit_at(input: u8, n: usize) -> Result { + if n < 8 { + Ok(input & (1 << n) != 0) + } else { + Err(()) + } + } + + fn check_allowed_to_freeze( + token_swap: &dyn SwapState, + freeze_authority_info: &AccountInfo, + ) -> ProgramResult { + match token_swap.freeze_authority() { + COption::Some(key) => { + if key == *freeze_authority_info.key && freeze_authority_info.is_signer { + Ok(()) + } else { + Err(SwapError::UnauthorizedToFreeze.into()) + } + } + COption::None => Err(SwapError::UnauthorizedToFreeze.into()), + } + } + + fn check_allowed_to_use(token_swap: &dyn SwapState, bit_position: usize) -> ProgramResult { + let bitmask: u8 = token_swap.freeze_authority_bit_mask(); + match Self::get_bit_at(bitmask, bit_position) { + Ok(frozen) => { + if frozen { + Err(SwapError::FrozenAction.into()) + } else { + Ok(()) + } + } + Err(_) => Err(SwapError::InvalidBitMaskOperation.into()), + } + } #[allow(clippy::too_many_arguments)] fn check_accounts( @@ -203,6 +244,7 @@ impl Processor { } /// Processes an [Initialize](enum.Instruction.html). + #[allow(clippy::too_many_arguments)] pub fn process_initialize( program_id: &Pubkey, nonce: u8, @@ -210,6 +252,8 @@ impl Processor { swap_curve: SwapCurve, accounts: &[AccountInfo], swap_constraints: &Option, + freeze_authority: COption, + freeze_authority_bit_mask: u8, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let swap_info = next_account_info(account_info_iter)?; @@ -305,7 +349,7 @@ impl Processor { to_u64(initial_amount)?, )?; - let obj = SwapVersion::SwapV1(SwapV1 { + let obj = SwapVersion::SwapV2(SwapV2 { is_initialized: true, nonce, token_program_id, @@ -317,6 +361,8 @@ impl Processor { pool_fee_account: *fee_account_info.key, fees, swap_curve, + freeze_authority, + freeze_authority_bit_mask, }); SwapVersion::pack(obj, &mut swap_info.data.borrow_mut())?; Ok(()) @@ -345,6 +391,7 @@ impl Processor { return Err(ProgramError::IncorrectProgramId); } let token_swap = SwapVersion::unpack(&swap_info.data.borrow())?; + Self::check_allowed_to_use(token_swap.as_ref(), 0)?; if *authority_info.key != Self::authority_id(program_id, swap_info.key, token_swap.nonce())? { @@ -512,6 +559,8 @@ impl Processor { let token_program_info = next_account_info(account_info_iter)?; let token_swap = SwapVersion::unpack(&swap_info.data.borrow())?; + Self::check_allowed_to_use(token_swap.as_ref(), 1)?; + let calculator = &token_swap.swap_curve().calculator; if !calculator.allows_deposits() { return Err(SwapError::UnsupportedCurveOperation.into()); @@ -615,6 +664,8 @@ impl Processor { let token_program_info = next_account_info(account_info_iter)?; let token_swap = SwapVersion::unpack(&swap_info.data.borrow())?; + Self::check_allowed_to_use(token_swap.as_ref(), 2)?; + Self::check_accounts( token_swap.as_ref(), program_id, @@ -739,6 +790,8 @@ impl Processor { let token_program_info = next_account_info(account_info_iter)?; let token_swap = SwapVersion::unpack(&swap_info.data.borrow())?; + Self::check_allowed_to_use(token_swap.as_ref(), 3)?; + let source_account = Self::unpack_token_account(source_info, &token_swap.token_program_id())?; let swap_token_a = @@ -854,6 +907,9 @@ impl Processor { let token_program_info = next_account_info(account_info_iter)?; let token_swap = SwapVersion::unpack(&swap_info.data.borrow())?; + + Self::check_allowed_to_use(token_swap.as_ref(), 4)?; + let destination_account = Self::unpack_token_account(destination_info, &token_swap.token_program_id())?; let swap_token_a = @@ -992,6 +1048,39 @@ impl Processor { Ok(()) } + /// Processes a [SetFreezeAuthorityBitMask](enum.Instruction.html). + pub fn process_set_freeze_authority_bit_mask( + _: &Pubkey, + freeze_authority_bit_mask: u8, + accounts: &[AccountInfo], + ) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let swap_info = next_account_info(account_info_iter)?; + let freeze_authority_info = next_account_info(account_info_iter)?; + let token_swap = SwapVersion::unpack(&swap_info.data.borrow())?; + + Self::check_allowed_to_freeze(token_swap.as_ref(), freeze_authority_info)?; + + let clone = SwapVersion::SwapV2(SwapV2 { + is_initialized: true, + nonce: token_swap.nonce(), + token_program_id: *token_swap.token_program_id(), + token_a: *token_swap.token_a_account(), + token_b: *token_swap.token_b_account(), + pool_mint: *token_swap.pool_mint(), + token_a_mint: *token_swap.token_a_mint(), + token_b_mint: *token_swap.token_b_mint(), + pool_fee_account: *token_swap.pool_fee_account(), + fees: token_swap.fees().clone(), + swap_curve: token_swap.swap_curve().clone(), + freeze_authority: token_swap.freeze_authority(), + freeze_authority_bit_mask, + }); + SwapVersion::pack(clone, &mut swap_info.data.borrow_mut())?; + + Ok(()) + } + /// Processes an [Instruction](enum.Instruction.html). pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { Self::process_with_constraints(program_id, accounts, input, &SWAP_CONSTRAINTS) @@ -1010,6 +1099,8 @@ impl Processor { nonce, fees, swap_curve, + freeze_authority, + freeze_authority_bit_mask, }) => { msg!("Instruction: Init"); Self::process_initialize( @@ -1019,6 +1110,8 @@ impl Processor { swap_curve, accounts, swap_constraints, + freeze_authority, + freeze_authority_bit_mask, ) } SwapInstruction::Swap(Swap { @@ -1084,6 +1177,16 @@ impl Processor { accounts, ) } + SwapInstruction::SetFreezeAuthorityBitMask(SetFreezeAuthorityBitMask { + freeze_authority_bit_mask, + }) => { + msg!("Instruction: SetFreezeAuthorityBitMask"); + Self::process_set_freeze_authority_bit_mask( + program_id, + freeze_authority_bit_mask, + accounts, + ) + } } } } @@ -1152,6 +1255,18 @@ impl PrintProgramError for SwapError { SwapError::UnsupportedCurveOperation => { msg!("Error: The operation cannot be performed on the given curve") } + SwapError::InvalidBitMaskOperation => { + msg!("Error: Attempted to access an invalid bit in the freeze authority bitmask") + } + SwapError::FrozenAction => { + msg!("Error: This action has been frozen by the Freeze Authority") + } + SwapError::UnauthorizedToFreeze => { + msg!("Error: Unauthorized to freeze") + } + SwapError::SwapV1UnsupportedAction => { + msg!("Error: Attempted to set bit mask on Swap V1") + } } } } @@ -1174,8 +1289,9 @@ mod tests { constant_product::ConstantProductCurve, offset::OffsetCurve, }, instruction::{ - deposit_all_token_types, deposit_single_token_type_exact_amount_in, initialize, swap, - withdraw_all_token_types, withdraw_single_token_type_exact_amount_out, + deposit_all_token_types, deposit_single_token_type_exact_amount_in, initialize, + set_freeze_authority_bit_mask, swap, withdraw_all_token_types, + withdraw_single_token_type_exact_amount_out, }, }; use solana_program::{instruction::Instruction, program_stubs, rent::Rent}; @@ -1264,6 +1380,7 @@ mod tests { token_b_account: Account, token_b_mint_key: Pubkey, token_b_mint_account: Account, + freeze_authority: COption, } impl SwapAccountInfo { @@ -1273,8 +1390,14 @@ mod tests { swap_curve: SwapCurve, token_a_amount: u64, token_b_amount: u64, + with_freeze: bool, ) -> Self { let swap_key = Pubkey::new_unique(); + + let freeze_authority = match with_freeze { + true => COption::Some(Pubkey::new_unique()), + false => COption::None, + }; let swap_account = Account::new(0, SwapVersion::LATEST_LEN, &SWAP_PROGRAM_ID); let (authority_key, nonce) = Pubkey::find_program_address(&[&swap_key.to_bytes()[..]], &SWAP_PROGRAM_ID); @@ -1339,6 +1462,7 @@ mod tests { token_b_account, token_b_mint_key, token_b_mint_account, + freeze_authority, } } @@ -1357,6 +1481,8 @@ mod tests { self.nonce, self.fees.clone(), self.swap_curve.clone(), + self.freeze_authority, + 0, ) .unwrap(), vec![ @@ -1511,6 +1637,27 @@ mod tests { Ok(()) } + pub fn set_freeze_authority_bit_mask( + &mut self, + freeze_authority_bit_mask: u8, + ) -> ProgramResult { + match self.freeze_authority { + COption::Some(key) => do_process_instruction( + set_freeze_authority_bit_mask( + &SWAP_PROGRAM_ID, + &TOKEN_PROGRAM_ID, + &key, + SetFreezeAuthorityBitMask { + freeze_authority_bit_mask, + }, + ) + .unwrap(), + vec![&mut self.swap_account, &mut Account::default()], + ), + COption::None => Err(SwapError::InvalidFreezeAuthority.into()), + } + } + #[allow(clippy::too_many_arguments)] pub fn deposit_all_token_types( &mut self, @@ -1999,8 +2146,14 @@ mod tests { calculator: Box::new(ConstantProductCurve {}), }; - let mut accounts = - SwapAccountInfo::new(&user_key, fees, swap_curve, token_a_amount, token_b_amount); + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + false, + ); // wrong nonce for authority_key { @@ -2453,6 +2606,8 @@ mod tests { accounts.nonce, accounts.fees.clone(), accounts.swap_curve.clone(), + COption::None, + 0, ) .unwrap(), vec![ @@ -2508,8 +2663,14 @@ mod tests { curve_type: CurveType::ConstantPrice, calculator: Box::new(ConstantPriceCurve { token_b_price }), }; - let mut accounts = - SwapAccountInfo::new(&user_key, fees, swap_curve, token_a_amount, token_b_amount); + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + false, + ); assert_eq!( Err(SwapError::InvalidCurve.into()), accounts.initialize_swap() @@ -2533,8 +2694,14 @@ mod tests { curve_type: CurveType::ConstantPrice, calculator: Box::new(ConstantPriceCurve { token_b_price }), }; - let mut accounts = - SwapAccountInfo::new(&user_key, fees, swap_curve, token_a_amount, token_b_amount); + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + false, + ); accounts.initialize_swap().unwrap(); } @@ -2555,8 +2722,14 @@ mod tests { curve_type: CurveType::Offset, calculator: Box::new(OffsetCurve { token_b_offset }), }; - let mut accounts = - SwapAccountInfo::new(&user_key, fees, swap_curve, token_a_amount, token_b_amount); + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + false, + ); assert_eq!( Err(SwapError::InvalidCurve.into()), accounts.initialize_swap() @@ -2580,8 +2753,14 @@ mod tests { curve_type: CurveType::Offset, calculator: Box::new(OffsetCurve { token_b_offset }), }; - let mut accounts = - SwapAccountInfo::new(&user_key, fees, swap_curve, token_a_amount, token_b_amount); + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + false, + ); accounts.initialize_swap().unwrap(); } @@ -2622,6 +2801,7 @@ mod tests { swap_curve, token_a_amount, token_b_amount, + false, ); assert_eq!( Err(SwapError::InvalidOwner.into()), @@ -2639,6 +2819,8 @@ mod tests { accounts.nonce, accounts.fees.clone(), accounts.swap_curve.clone(), + COption::None, + 0, ) .unwrap(), vec![ @@ -2694,6 +2876,7 @@ mod tests { swap_curve, token_a_amount, token_b_amount, + false, ); assert_eq!( Err(SwapError::InvalidFee.into()), @@ -2711,6 +2894,8 @@ mod tests { accounts.nonce, accounts.fees.clone(), accounts.swap_curve.clone(), + COption::None, + 0, ) .unwrap(), vec![ @@ -2764,6 +2949,7 @@ mod tests { swap_curve, token_a_amount, token_b_amount, + false, ); do_process_instruction_with_fee_constraints( initialize( @@ -2779,6 +2965,8 @@ mod tests { accounts.nonce, accounts.fees, accounts.swap_curve.clone(), + COption::None, + 0, ) .unwrap(), vec![ @@ -2858,8 +3046,14 @@ mod tests { calculator: Box::new(ConstantProductCurve {}), }; - let mut accounts = - SwapAccountInfo::new(&user_key, fees, swap_curve, token_a_amount, token_b_amount); + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + false, + ); // depositing 10% of the current pool amount in token A and B means // that our pool tokens will be worth 1 / 10 of the current pool amount @@ -3437,6 +3631,134 @@ mod tests { pool_account.amount + swap_pool_account.amount ); } + + // adding freeze authority and turning off deposit throws FrozenAction + { + let fees = Fees { + trade_fee_numerator, + trade_fee_denominator, + owner_trade_fee_numerator, + owner_trade_fee_denominator, + owner_withdraw_fee_numerator, + owner_withdraw_fee_denominator, + host_fee_numerator, + host_fee_denominator, + }; + + let curve_type = CurveType::ConstantProduct; + let swap_curve = SwapCurve { + curve_type, + calculator: Box::new(ConstantProductCurve {}), + }; + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + true, + ); + + accounts.initialize_swap().unwrap(); + accounts.set_freeze_authority_bit_mask(1 << 1).unwrap(); + + let ( + token_a_key, + mut token_a_account, + token_b_key, + mut token_b_account, + pool_key, + mut pool_account, + ) = accounts.setup_token_accounts(&user_key, &depositor_key, deposit_a, deposit_b, 0); + assert_eq!( + Err(SwapError::FrozenAction.into()), + accounts.deposit_all_token_types( + &depositor_key, + &token_a_key, + &mut token_a_account, + &token_b_key, + &mut token_b_account, + &pool_key, + &mut pool_account, + pool_amount.try_into().unwrap(), + deposit_a, + deposit_b, + ) + ); + } + + // adding freeze authority and leaving on deposit works fine + { + let fees = Fees { + trade_fee_numerator, + trade_fee_denominator, + owner_trade_fee_numerator, + owner_trade_fee_denominator, + owner_withdraw_fee_numerator, + owner_withdraw_fee_denominator, + host_fee_numerator, + host_fee_denominator, + }; + + let curve_type = CurveType::ConstantProduct; + let swap_curve = SwapCurve { + curve_type, + calculator: Box::new(ConstantProductCurve {}), + }; + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + true, + ); + + accounts.initialize_swap().unwrap(); + + let ( + token_a_key, + mut token_a_account, + token_b_key, + mut token_b_account, + pool_key, + mut pool_account, + ) = accounts.setup_token_accounts(&user_key, &depositor_key, deposit_a, deposit_b, 0); + accounts + .deposit_all_token_types( + &depositor_key, + &token_a_key, + &mut token_a_account, + &token_b_key, + &mut token_b_account, + &pool_key, + &mut pool_account, + pool_amount.try_into().unwrap(), + deposit_a, + deposit_b, + ) + .unwrap(); + + let swap_token_a = + spl_token::state::Account::unpack(&accounts.token_a_account.data).unwrap(); + assert_eq!(swap_token_a.amount, deposit_a + token_a_amount); + let swap_token_b = + spl_token::state::Account::unpack(&accounts.token_b_account.data).unwrap(); + assert_eq!(swap_token_b.amount, deposit_b + token_b_amount); + let token_a = spl_token::state::Account::unpack(&token_a_account.data).unwrap(); + assert_eq!(token_a.amount, 0); + let token_b = spl_token::state::Account::unpack(&token_b_account.data).unwrap(); + assert_eq!(token_b.amount, 0); + let pool_account = spl_token::state::Account::unpack(&pool_account.data).unwrap(); + let swap_pool_account = + spl_token::state::Account::unpack(&accounts.pool_token_account.data).unwrap(); + let pool_mint = + spl_token::state::Mint::unpack(&accounts.pool_mint_account.data).unwrap(); + assert_eq!( + pool_mint.supply, + pool_account.amount + swap_pool_account.amount + ); + } } #[test] @@ -3478,8 +3800,14 @@ mod tests { let minimum_token_a_amount = initial_a / 40; let minimum_token_b_amount = initial_b / 40; - let mut accounts = - SwapAccountInfo::new(&user_key, fees, swap_curve, token_a_amount, token_b_amount); + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + false, + ); // swap not initialized { @@ -4244,28 +4572,196 @@ mod tests { TryInto::::try_into(results.token_b_amount).unwrap() ); } - } - #[test] - fn test_deposit_one_exact_in() { - let user_key = Pubkey::new_unique(); - let depositor_key = Pubkey::new_unique(); - let trade_fee_numerator = 1; - let trade_fee_denominator = 2; - let owner_trade_fee_numerator = 1; - let owner_trade_fee_denominator = 10; - let owner_withdraw_fee_numerator = 1; - let owner_withdraw_fee_denominator = 5; - let host_fee_numerator = 20; - let host_fee_denominator = 100; + // add freeze authority and freeze withdrawal throws FrozenAction error + { + let fees = Fees { + trade_fee_numerator, + trade_fee_denominator, + owner_trade_fee_numerator, + owner_trade_fee_denominator, + owner_withdraw_fee_numerator, + owner_withdraw_fee_denominator, + host_fee_numerator, + host_fee_denominator, + }; - let fees = Fees { - trade_fee_numerator, - trade_fee_denominator, - owner_trade_fee_numerator, - owner_trade_fee_denominator, - owner_withdraw_fee_numerator, - owner_withdraw_fee_denominator, + let curve_type = CurveType::ConstantProduct; + let swap_curve = SwapCurve { + curve_type, + calculator: Box::new(ConstantProductCurve {}), + }; + + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + true, + ); + accounts.initialize_swap().unwrap(); + accounts.set_freeze_authority_bit_mask(1 << 2).unwrap(); + + let ( + token_a_key, + mut token_a_account, + token_b_key, + mut token_b_account, + _pool_key, + mut _pool_account, + ) = accounts.setup_token_accounts(&user_key, &withdrawer_key, 0, 0, 0); + + let pool_fee_key = accounts.pool_fee_key; + let mut pool_fee_account = accounts.pool_fee_account.clone(); + let fee_account = spl_token::state::Account::unpack(&pool_fee_account.data).unwrap(); + let pool_fee_amount = fee_account.amount; + + assert_eq!( + Err(SwapError::FrozenAction.into()), + accounts.withdraw_all_token_types( + &user_key, + &pool_fee_key, + &mut pool_fee_account, + &token_a_key, + &mut token_a_account, + &token_b_key, + &mut token_b_account, + pool_fee_amount, + 0, + 0, + ) + ) + } + // add freeze authority does not break withdrawal + { + let fees = Fees { + trade_fee_numerator, + trade_fee_denominator, + owner_trade_fee_numerator, + owner_trade_fee_denominator, + owner_withdraw_fee_numerator, + owner_withdraw_fee_denominator, + host_fee_numerator, + host_fee_denominator, + }; + + let curve_type = CurveType::ConstantProduct; + let swap_curve = SwapCurve { + curve_type, + calculator: Box::new(ConstantProductCurve {}), + }; + + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + true, + ); + accounts.initialize_swap().unwrap(); + + let ( + token_a_key, + mut token_a_account, + token_b_key, + mut token_b_account, + pool_key, + mut pool_account, + ) = accounts.setup_token_accounts( + &user_key, + &withdrawer_key, + initial_a, + initial_b, + initial_pool.try_into().unwrap(), + ); + + accounts + .withdraw_all_token_types( + &withdrawer_key, + &pool_key, + &mut pool_account, + &token_a_key, + &mut token_a_account, + &token_b_key, + &mut token_b_account, + withdraw_amount.try_into().unwrap(), + minimum_token_a_amount, + minimum_token_b_amount, + ) + .unwrap(); + + let swap_token_a = + spl_token::state::Account::unpack(&accounts.token_a_account.data).unwrap(); + let swap_token_b = + spl_token::state::Account::unpack(&accounts.token_b_account.data).unwrap(); + let pool_mint = + spl_token::state::Mint::unpack(&accounts.pool_mint_account.data).unwrap(); + let withdraw_fee = accounts.fees.owner_withdraw_fee(withdraw_amount).unwrap(); + let results = accounts + .swap_curve + .calculator + .pool_tokens_to_trading_tokens( + withdraw_amount - withdraw_fee, + pool_mint.supply.try_into().unwrap(), + swap_token_a.amount.try_into().unwrap(), + swap_token_b.amount.try_into().unwrap(), + RoundDirection::Floor, + ) + .unwrap(); + assert_eq!( + swap_token_a.amount, + token_a_amount - to_u64(results.token_a_amount).unwrap() + ); + assert_eq!( + swap_token_b.amount, + token_b_amount - to_u64(results.token_b_amount).unwrap() + ); + let token_a = spl_token::state::Account::unpack(&token_a_account.data).unwrap(); + assert_eq!( + token_a.amount, + initial_a + to_u64(results.token_a_amount).unwrap() + ); + let token_b = spl_token::state::Account::unpack(&token_b_account.data).unwrap(); + assert_eq!( + token_b.amount, + initial_b + to_u64(results.token_b_amount).unwrap() + ); + let pool_account = spl_token::state::Account::unpack(&pool_account.data).unwrap(); + assert_eq!( + pool_account.amount, + to_u64(initial_pool - withdraw_amount).unwrap() + ); + let fee_account = + spl_token::state::Account::unpack(&accounts.pool_fee_account.data).unwrap(); + assert_eq!( + fee_account.amount, + TryInto::::try_into(withdraw_fee).unwrap() + ); + } + } + + #[test] + fn test_deposit_one_exact_in() { + let user_key = Pubkey::new_unique(); + let depositor_key = Pubkey::new_unique(); + let trade_fee_numerator = 1; + let trade_fee_denominator = 2; + let owner_trade_fee_numerator = 1; + let owner_trade_fee_denominator = 10; + let owner_withdraw_fee_numerator = 1; + let owner_withdraw_fee_denominator = 5; + let host_fee_numerator = 20; + let host_fee_denominator = 100; + + let fees = Fees { + trade_fee_numerator, + trade_fee_denominator, + owner_trade_fee_numerator, + owner_trade_fee_denominator, + owner_withdraw_fee_numerator, + owner_withdraw_fee_denominator, host_fee_numerator, host_fee_denominator, }; @@ -4278,8 +4774,14 @@ mod tests { calculator: Box::new(ConstantProductCurve {}), }; - let mut accounts = - SwapAccountInfo::new(&user_key, fees, swap_curve, token_a_amount, token_b_amount); + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + false, + ); let deposit_a = token_a_amount / 10; let deposit_b = token_b_amount / 10; @@ -4757,6 +5259,143 @@ mod tests { pool_account.amount + swap_pool_account.amount ); } + + // freeze authority throws FrozenAction error when bit mask turned on + { + let fees = Fees { + trade_fee_numerator, + trade_fee_denominator, + owner_trade_fee_numerator, + owner_trade_fee_denominator, + owner_withdraw_fee_numerator, + owner_withdraw_fee_denominator, + host_fee_numerator, + host_fee_denominator, + }; + + let curve_type = CurveType::ConstantProduct; + let swap_curve = SwapCurve { + curve_type, + calculator: Box::new(ConstantProductCurve {}), + }; + + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + true, + ); + accounts.initialize_swap().unwrap(); + accounts.set_freeze_authority_bit_mask(1 << 3).unwrap(); + + let ( + token_a_key, + mut token_a_account, + _token_b_key, + mut _token_b_account, + pool_key, + mut pool_account, + ) = accounts.setup_token_accounts(&user_key, &depositor_key, deposit_a, deposit_b, 0); + assert_eq!( + Err(SwapError::FrozenAction.into()), + accounts.deposit_single_token_type_exact_amount_in( + &depositor_key, + &token_a_key, + &mut token_a_account, + &pool_key, + &mut pool_account, + deposit_a, + pool_amount, + ) + ); + } + + // freeze authority correctly deposits when no mask set + { + let fees = Fees { + trade_fee_numerator, + trade_fee_denominator, + owner_trade_fee_numerator, + owner_trade_fee_denominator, + owner_withdraw_fee_numerator, + owner_withdraw_fee_denominator, + host_fee_numerator, + host_fee_denominator, + }; + + let curve_type = CurveType::ConstantProduct; + let swap_curve = SwapCurve { + curve_type, + calculator: Box::new(ConstantProductCurve {}), + }; + + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + true, + ); + accounts.initialize_swap().unwrap(); + + let ( + token_a_key, + mut token_a_account, + token_b_key, + mut token_b_account, + pool_key, + mut pool_account, + ) = accounts.setup_token_accounts(&user_key, &depositor_key, deposit_a, deposit_b, 0); + accounts + .deposit_single_token_type_exact_amount_in( + &depositor_key, + &token_a_key, + &mut token_a_account, + &pool_key, + &mut pool_account, + deposit_a, + pool_amount, + ) + .unwrap(); + + let swap_token_a = + spl_token::state::Account::unpack(&accounts.token_a_account.data).unwrap(); + assert_eq!(swap_token_a.amount, deposit_a + token_a_amount); + + let token_a = spl_token::state::Account::unpack(&token_a_account.data).unwrap(); + assert_eq!(token_a.amount, 0); + + accounts + .deposit_single_token_type_exact_amount_in( + &depositor_key, + &token_b_key, + &mut token_b_account, + &pool_key, + &mut pool_account, + deposit_b, + pool_amount, + ) + .unwrap(); + let swap_token_b = + spl_token::state::Account::unpack(&accounts.token_b_account.data).unwrap(); + assert_eq!(swap_token_b.amount, deposit_b + token_b_amount); + + let token_b = spl_token::state::Account::unpack(&token_b_account.data).unwrap(); + assert_eq!(token_b.amount, 0); + + let pool_account = spl_token::state::Account::unpack(&pool_account.data).unwrap(); + let swap_pool_account = + spl_token::state::Account::unpack(&accounts.pool_token_account.data).unwrap(); + let pool_mint = + spl_token::state::Mint::unpack(&accounts.pool_mint_account.data).unwrap(); + assert_eq!( + pool_mint.supply, + pool_account.amount + swap_pool_account.amount + ); + } } #[test] @@ -4798,8 +5437,14 @@ mod tests { let destination_a_amount = initial_a / 40; let destination_b_amount = initial_b / 40; - let mut accounts = - SwapAccountInfo::new(&user_key, fees, swap_curve, token_a_amount, token_b_amount); + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + false, + ); // swap not initialized { @@ -5408,6 +6053,162 @@ mod tests { let token_a = spl_token::state::Account::unpack(&token_a_account.data).unwrap(); assert_eq!(token_a.amount, initial_a + fee_a_amount); } + + // freeze authority throws FrozenAction error when bit mask flipped + { + let fees = Fees { + trade_fee_numerator, + trade_fee_denominator, + owner_trade_fee_numerator, + owner_trade_fee_denominator, + owner_withdraw_fee_numerator, + owner_withdraw_fee_denominator, + host_fee_numerator, + host_fee_denominator, + }; + + let curve_type = CurveType::ConstantProduct; + let swap_curve = SwapCurve { + curve_type, + calculator: Box::new(ConstantProductCurve {}), + }; + + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + true, + ); + accounts.initialize_swap().unwrap(); + // Let's add 1 here to test possibility of having additional bits flipped + accounts + .set_freeze_authority_bit_mask((1 << 4) + 1) + .unwrap(); + + let ( + token_a_key, + mut token_a_account, + _token_b_key, + _token_b_account, + pool_key, + mut pool_account, + ) = accounts.setup_token_accounts( + &user_key, + &withdrawer_key, + initial_a, + initial_b, + initial_pool.try_into().unwrap(), + ); + assert_eq!( + Err(SwapError::FrozenAction.into()), + accounts.withdraw_single_token_type_exact_amount_out( + &withdrawer_key, + &pool_key, + &mut pool_account, + &token_a_key, + &mut token_a_account, + destination_a_amount, + maximum_pool_token_amount, + ) + ); + } + + // freeze authority has correct withdrawal + { + let fees = Fees { + trade_fee_numerator, + trade_fee_denominator, + owner_trade_fee_numerator, + owner_trade_fee_denominator, + owner_withdraw_fee_numerator, + owner_withdraw_fee_denominator, + host_fee_numerator, + host_fee_denominator, + }; + + let curve_type = CurveType::ConstantProduct; + let swap_curve = SwapCurve { + curve_type, + calculator: Box::new(ConstantProductCurve {}), + }; + + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + true, + ); + accounts.initialize_swap().unwrap(); + let ( + token_a_key, + mut token_a_account, + _token_b_key, + _token_b_account, + pool_key, + mut pool_account, + ) = accounts.setup_token_accounts( + &user_key, + &withdrawer_key, + initial_a, + initial_b, + initial_pool.try_into().unwrap(), + ); + + let swap_token_a = + spl_token::state::Account::unpack(&accounts.token_a_account.data).unwrap(); + let swap_token_b = + spl_token::state::Account::unpack(&accounts.token_b_account.data).unwrap(); + let pool_mint = + spl_token::state::Mint::unpack(&accounts.pool_mint_account.data).unwrap(); + + let pool_token_amount = accounts + .swap_curve + .trading_tokens_to_pool_tokens( + destination_a_amount.try_into().unwrap(), + (swap_token_a.amount - destination_a_amount) + .try_into() + .unwrap(), + swap_token_b.amount.try_into().unwrap(), + pool_mint.supply.try_into().unwrap(), + TradeDirection::AtoB, + RoundDirection::Ceiling, + &accounts.fees, + ) + .unwrap(); + let withdraw_fee = accounts.fees.owner_withdraw_fee(pool_token_amount).unwrap(); + + accounts + .withdraw_single_token_type_exact_amount_out( + &withdrawer_key, + &pool_key, + &mut pool_account, + &token_a_key, + &mut token_a_account, + destination_a_amount, + maximum_pool_token_amount, + ) + .unwrap(); + + let swap_token_a = + spl_token::state::Account::unpack(&accounts.token_a_account.data).unwrap(); + + assert_eq!(swap_token_a.amount, token_a_amount - destination_a_amount); + let token_a = spl_token::state::Account::unpack(&token_a_account.data).unwrap(); + assert_eq!(token_a.amount, initial_a + destination_a_amount); + + let pool_account = spl_token::state::Account::unpack(&pool_account.data).unwrap(); + assert_eq!( + pool_account.amount, + to_u64(initial_pool - pool_token_amount - withdraw_fee).unwrap() + ); + let fee_account = + spl_token::state::Account::unpack(&accounts.pool_fee_account.data).unwrap(); + assert_eq!(fee_account.amount, to_u64(withdraw_fee).unwrap()); + } } fn check_valid_swap_curve( @@ -5431,6 +6232,7 @@ mod tests { swap_curve.clone(), token_a_amount, token_b_amount, + false, ); let initial_a = token_a_amount / 5; let initial_b = token_b_amount / 5; @@ -5737,6 +6539,7 @@ mod tests { swap_curve, token_a_amount, token_b_amount, + false, ); // initialize swap @@ -5754,6 +6557,8 @@ mod tests { accounts.nonce, accounts.fees.clone(), accounts.swap_curve.clone(), + COption::None, + 0, ) .unwrap(), vec![ @@ -5870,8 +6675,14 @@ mod tests { curve_type, calculator: Box::new(ConstantProductCurve {}), }; - let mut accounts = - SwapAccountInfo::new(&user_key, fees, swap_curve, token_a_amount, token_b_amount); + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + false, + ); let initial_a = token_a_amount / 5; let initial_b = token_b_amount / 5; @@ -6500,6 +7311,239 @@ mod tests { } } + #[test] + fn test_freeze_authority_can_freeze_swap() { + let trade_fee_numerator = 1; + let trade_fee_denominator = 10; + let owner_trade_fee_numerator = 1; + let owner_trade_fee_denominator = 30; + let owner_withdraw_fee_numerator = 1; + let owner_withdraw_fee_denominator = 30; + let host_fee_numerator = 10; + let host_fee_denominator = 100; + + let token_a_amount = 1_000_000_000; + let token_b_amount = 0; + let fees = Fees { + trade_fee_numerator, + trade_fee_denominator, + owner_trade_fee_numerator, + owner_trade_fee_denominator, + owner_withdraw_fee_numerator, + owner_withdraw_fee_denominator, + host_fee_numerator, + host_fee_denominator, + }; + + let token_b_offset = 2_000_000; + let swap_curve = SwapCurve { + curve_type: CurveType::Offset, + calculator: Box::new(OffsetCurve { token_b_offset }), + }; + let user_key = Pubkey::new_unique(); + let swapper_key = Pubkey::new_unique(); + + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + true, + ); + + accounts.initialize_swap().unwrap(); + accounts.set_freeze_authority_bit_mask(1 << 0).unwrap(); + + let swap_token_a_key = accounts.token_a_key; + let swap_token_b_key = accounts.token_b_key; + let initial_a = 500_000; + let initial_b = 1_000; + + let ( + token_a_key, + mut token_a_account, + token_b_key, + mut token_b_account, + _pool_key, + _pool_account, + ) = accounts.setup_token_accounts(&user_key, &swapper_key, initial_a, initial_b, 0); + + // swap a to b way, fails, there's no liquidity + let a_to_b_amount = initial_a; + let minimum_token_b_amount = 0; + + assert_eq!( + Err(SwapError::FrozenAction.into()), + accounts.swap( + &swapper_key, + &token_a_key, + &mut token_a_account, + &swap_token_a_key, + &swap_token_b_key, + &token_b_key, + &mut token_b_account, + a_to_b_amount, + minimum_token_b_amount, + ) + ); + } + + #[test] + fn test_freeze_authority_does_not_affect_swap() { + let trade_fee_numerator = 1; + let trade_fee_denominator = 10; + let owner_trade_fee_numerator = 1; + let owner_trade_fee_denominator = 30; + let owner_withdraw_fee_numerator = 1; + let owner_withdraw_fee_denominator = 30; + let host_fee_numerator = 10; + let host_fee_denominator = 100; + + let token_a_amount = 1_000_000_000; + let token_b_amount = 0; + let fees = Fees { + trade_fee_numerator, + trade_fee_denominator, + owner_trade_fee_numerator, + owner_trade_fee_denominator, + owner_withdraw_fee_numerator, + owner_withdraw_fee_denominator, + host_fee_numerator, + host_fee_denominator, + }; + + let token_b_offset = 2_000_000; + let swap_curve = SwapCurve { + curve_type: CurveType::Offset, + calculator: Box::new(OffsetCurve { token_b_offset }), + }; + let user_key = Pubkey::new_unique(); + let swapper_key = Pubkey::new_unique(); + + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + true, + ); + + accounts.initialize_swap().unwrap(); + + let swap_token_a_key = accounts.token_a_key; + let swap_token_b_key = accounts.token_b_key; + let initial_a = 500_000; + let initial_b = 1_000; + + let ( + token_a_key, + mut token_a_account, + token_b_key, + mut token_b_account, + _pool_key, + _pool_account, + ) = accounts.setup_token_accounts(&user_key, &swapper_key, initial_a, initial_b, 0); + + // swap a to b way, fails, there's no liquidity + let a_to_b_amount = initial_a; + let minimum_token_b_amount = 0; + + assert_eq!( + Err(SwapError::ZeroTradingTokens.into()), + accounts.swap( + &swapper_key, + &token_a_key, + &mut token_a_account, + &swap_token_a_key, + &swap_token_b_key, + &token_b_key, + &mut token_b_account, + a_to_b_amount, + minimum_token_b_amount, + ) + ); + + // swap b to a, succeeds at offset price + let b_to_a_amount = initial_b; + let minimum_token_a_amount = 0; + accounts + .swap( + &swapper_key, + &token_b_key, + &mut token_b_account, + &swap_token_b_key, + &swap_token_a_key, + &token_a_key, + &mut token_a_account, + b_to_a_amount, + minimum_token_a_amount, + ) + .unwrap(); + + // try a to b again, succeeds due to new liquidity + accounts + .swap( + &swapper_key, + &token_a_key, + &mut token_a_account, + &swap_token_a_key, + &swap_token_b_key, + &token_b_key, + &mut token_b_account, + a_to_b_amount, + minimum_token_b_amount, + ) + .unwrap(); + + // try a to b again, fails due to no more liquidity + assert_eq!( + Err(SwapError::ZeroTradingTokens.into()), + accounts.swap( + &swapper_key, + &token_a_key, + &mut token_a_account, + &swap_token_a_key, + &swap_token_b_key, + &token_b_key, + &mut token_b_account, + a_to_b_amount, + minimum_token_b_amount, + ) + ); + + // Try to deposit, fails because deposits are not allowed for offset + // curve swaps + { + let initial_a = 100; + let initial_b = 100; + let pool_amount = 100; + let ( + token_a_key, + mut token_a_account, + token_b_key, + mut token_b_account, + pool_key, + mut pool_account, + ) = accounts.setup_token_accounts(&user_key, &swapper_key, initial_a, initial_b, 0); + assert_eq!( + Err(SwapError::UnsupportedCurveOperation.into()), + accounts.deposit_all_token_types( + &swapper_key, + &token_a_key, + &mut token_a_account, + &token_b_key, + &mut token_b_account, + &pool_key, + &mut pool_account, + pool_amount, + initial_a, + initial_b, + ) + ); + } + } #[test] fn test_overdraw_offset_curve() { let trade_fee_numerator = 1; @@ -6532,8 +7576,14 @@ mod tests { let user_key = Pubkey::new_unique(); let swapper_key = Pubkey::new_unique(); - let mut accounts = - SwapAccountInfo::new(&user_key, fees, swap_curve, token_a_amount, token_b_amount); + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + false, + ); accounts.initialize_swap().unwrap(); @@ -6683,8 +7733,14 @@ mod tests { let user_key = Pubkey::new_unique(); let withdrawer_key = Pubkey::new_unique(); - let mut accounts = - SwapAccountInfo::new(&user_key, fees, swap_curve, token_a_amount, token_b_amount); + let mut accounts = SwapAccountInfo::new( + &user_key, + fees, + swap_curve, + token_a_amount, + token_b_amount, + false, + ); accounts.initialize_swap().unwrap(); diff --git a/token-swap/program/src/state.rs b/token-swap/program/src/state.rs index f928b581976..31c3d2a49cd 100644 --- a/token-swap/program/src/state.rs +++ b/token-swap/program/src/state.rs @@ -5,6 +5,7 @@ use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; use enum_dispatch::enum_dispatch; use solana_program::{ program_error::ProgramError, + program_option::COption, program_pack::{IsInitialized, Pack, Sealed}, pubkey::Pubkey, }; @@ -37,12 +38,25 @@ pub trait SwapState { fn fees(&self) -> &Fees; /// Curve associated with swap fn swap_curve(&self) -> &SwapCurve; + + /// Freeze authority + fn freeze_authority(&self) -> COption; + + /// bits, from right to left - 1 disables, 0 enables the actions: + /// 0. process_swap, + /// 1. process_deposit_all_token_types, + /// 2. process_withdraw_all_token_types, + /// 3. process_deposit_single_token_type_exact_amount_in, + /// 4. process_withdraw_single_token_type_exact_amount_out, + fn freeze_authority_bit_mask(&self) -> u8; } /// All versions of SwapState #[enum_dispatch(SwapState)] pub enum SwapVersion { /// Latest version, used for all new swaps + SwapV2, + /// Deprecated version, used for some existing swaps SwapV1, } @@ -51,11 +65,15 @@ pub enum SwapVersion { /// special implementations are provided here impl SwapVersion { /// Size of the latest version of the SwapState - pub const LATEST_LEN: usize = 1 + SwapV1::LEN; // add one for the version enum + pub const LATEST_LEN: usize = 1 + SwapV2::LEN; // add one for the version enum /// Pack a swap into a byte array, based on its version pub fn pack(src: Self, dst: &mut [u8]) -> Result<(), ProgramError> { match src { + Self::SwapV2(swap_info) => { + dst[0] = 2; + SwapV2::pack(swap_info, &mut dst[1..]) + } Self::SwapV1(swap_info) => { dst[0] = 1; SwapV1::pack(swap_info, &mut dst[1..]) @@ -70,6 +88,7 @@ impl SwapVersion { .split_first() .ok_or(ProgramError::InvalidAccountData)?; match version { + 2 => Ok(Box::new(SwapV2::unpack(rest)?)), 1 => Ok(Box::new(SwapV1::unpack(rest)?)), _ => Err(ProgramError::UninitializedAccount), } @@ -84,6 +103,194 @@ impl SwapVersion { } } } +/// Program states. +#[repr(C)] +#[derive(Debug, Default, PartialEq)] +pub struct SwapV2 { + /// Initialized state. + pub is_initialized: bool, + /// Nonce used in program address. + /// The program address is created deterministically with the nonce, + /// swap program id, and swap account pubkey. This program address has + /// authority over the swap's token A account, token B account, and pool + /// token mint. + pub nonce: u8, + + /// Program ID of the tokens being exchanged. + pub token_program_id: Pubkey, + + /// Token A + pub token_a: Pubkey, + /// Token B + pub token_b: Pubkey, + + /// Pool tokens are issued when A or B tokens are deposited. + /// Pool tokens can be withdrawn back to the original A or B token. + pub pool_mint: Pubkey, + + /// Mint information for token A + pub token_a_mint: Pubkey, + /// Mint information for token B + pub token_b_mint: Pubkey, + + /// Pool token account to receive trading and / or withdrawal fees + pub pool_fee_account: Pubkey, + + /// All fee information + pub fees: Fees, + + /// Swap curve parameters, to be unpacked and used by the SwapCurve, which + /// calculates swaps, deposits, and withdrawals + pub swap_curve: SwapCurve, + + /// Freeze authority + pub freeze_authority: COption, + + /// bits, from left to right - 1 disables, 0 enables the actions: + /// 0. process_swap, + /// 1. process_deposit_all_token_types, + /// 2. process_withdraw_all_token_types, + /// 3. process_deposit_single_token_type_exact_amount_in, + /// 4. process_withdraw_single_token_type_exact_amount_out, + pub freeze_authority_bit_mask: u8, +} + +impl SwapState for SwapV2 { + fn is_initialized(&self) -> bool { + self.is_initialized + } + + fn nonce(&self) -> u8 { + self.nonce + } + + fn token_program_id(&self) -> &Pubkey { + &self.token_program_id + } + + fn token_a_account(&self) -> &Pubkey { + &self.token_a + } + + fn token_b_account(&self) -> &Pubkey { + &self.token_b + } + + fn pool_mint(&self) -> &Pubkey { + &self.pool_mint + } + + fn token_a_mint(&self) -> &Pubkey { + &self.token_a_mint + } + + fn token_b_mint(&self) -> &Pubkey { + &self.token_b_mint + } + + fn pool_fee_account(&self) -> &Pubkey { + &self.pool_fee_account + } + + fn fees(&self) -> &Fees { + &self.fees + } + + fn swap_curve(&self) -> &SwapCurve { + &self.swap_curve + } + + fn freeze_authority(&self) -> COption { + self.freeze_authority + } + + fn freeze_authority_bit_mask(&self) -> u8 { + self.freeze_authority_bit_mask + } +} + +impl Sealed for SwapV2 {} +impl IsInitialized for SwapV2 { + fn is_initialized(&self) -> bool { + self.is_initialized + } +} + +impl Pack for SwapV2 { + const LEN: usize = 360; + + fn pack_into_slice(&self, output: &mut [u8]) { + let output = array_mut_ref![output, 0, 360]; + let ( + is_initialized, + nonce, + token_program_id, + token_a, + token_b, + pool_mint, + token_a_mint, + token_b_mint, + pool_fee_account, + fees, + swap_curve, + freeze_authority, + freeze_authority_bit_mask, + ) = mut_array_refs![output, 1, 1, 32, 32, 32, 32, 32, 32, 32, 64, 33, 36, 1]; + is_initialized[0] = self.is_initialized as u8; + nonce[0] = self.nonce; + token_program_id.copy_from_slice(self.token_program_id.as_ref()); + token_a.copy_from_slice(self.token_a.as_ref()); + token_b.copy_from_slice(self.token_b.as_ref()); + pool_mint.copy_from_slice(self.pool_mint.as_ref()); + token_a_mint.copy_from_slice(self.token_a_mint.as_ref()); + token_b_mint.copy_from_slice(self.token_b_mint.as_ref()); + pool_fee_account.copy_from_slice(self.pool_fee_account.as_ref()); + self.fees.pack_into_slice(&mut fees[..]); + self.swap_curve.pack_into_slice(&mut swap_curve[..]); + pack_coption_key(&self.freeze_authority, freeze_authority); + *freeze_authority_bit_mask = self.freeze_authority_bit_mask.to_le_bytes(); + } + + /// Unpacks a byte buffer into a [SwapV2](struct.SwapV2.html). + fn unpack_from_slice(input: &[u8]) -> Result { + let input = array_ref![input, 0, 360]; + #[allow(clippy::ptr_offset_with_cast)] + let ( + is_initialized, + nonce, + token_program_id, + token_a, + token_b, + pool_mint, + token_a_mint, + token_b_mint, + pool_fee_account, + fees, + swap_curve, + freeze_authority, + freeze_authority_bit_mask, + ) = array_refs![input, 1, 1, 32, 32, 32, 32, 32, 32, 32, 64, 33, 36, 1]; + Ok(Self { + is_initialized: match is_initialized { + [0] => false, + [1] => true, + _ => return Err(ProgramError::InvalidAccountData), + }, + nonce: nonce[0], + token_program_id: Pubkey::new_from_array(*token_program_id), + token_a: Pubkey::new_from_array(*token_a), + token_b: Pubkey::new_from_array(*token_b), + pool_mint: Pubkey::new_from_array(*pool_mint), + token_a_mint: Pubkey::new_from_array(*token_a_mint), + token_b_mint: Pubkey::new_from_array(*token_b_mint), + pool_fee_account: Pubkey::new_from_array(*pool_fee_account), + fees: Fees::unpack_from_slice(fees)?, + swap_curve: SwapCurve::unpack_from_slice(swap_curve)?, + freeze_authority: unpack_coption_key(freeze_authority)?, + freeze_authority_bit_mask: u8::from_le_bytes(*freeze_authority_bit_mask), + }) + } +} /// Program states. #[repr(C)] @@ -121,7 +328,7 @@ pub struct SwapV1 { /// All fee information pub fees: Fees, - /// Swap curve parameters, to be unpacked and used by the SwapCurve, which + /// Swap curve parameters,to be unpacked and used by the SwapCurve, which /// calculates swaps, deposits, and withdrawals pub swap_curve: SwapCurve, } @@ -170,6 +377,14 @@ impl SwapState for SwapV1 { fn swap_curve(&self) -> &SwapCurve { &self.swap_curve } + + fn freeze_authority(&self) -> COption { + COption::None + } + + fn freeze_authority_bit_mask(&self) -> u8 { + 0_u8 + } } impl Sealed for SwapV1 {} @@ -247,6 +462,28 @@ impl Pack for SwapV1 { } } +fn pack_coption_key(src: &COption, dst: &mut [u8; 36]) { + let (tag, body) = mut_array_refs![dst, 4, 32]; + match src { + COption::Some(key) => { + *tag = [1, 0, 0, 0]; + body.copy_from_slice(key.as_ref()); + } + COption::None => { + *tag = [0; 4]; + } + } +} + +fn unpack_coption_key(src: &[u8; 36]) -> Result, ProgramError> { + let (tag, body) = array_refs![src, 4, 32]; + match *tag { + [0, 0, 0, 0] => Ok(COption::None), + [1, 0, 0, 0] => Ok(COption::Some(Pubkey::new_from_array(*body))), + _ => Err(ProgramError::InvalidAccountData), + } +} + #[cfg(test)] mod tests { use super::*; @@ -286,7 +523,7 @@ mod tests { curve_type, calculator, }; - let swap_info = SwapVersion::SwapV1(SwapV1 { + let swap_info = SwapVersion::SwapV2(SwapV2 { is_initialized: true, nonce: TEST_NONCE, token_program_id: TEST_TOKEN_PROGRAM_ID, @@ -298,6 +535,8 @@ mod tests { pool_fee_account: TEST_POOL_FEE_ACCOUNT, fees: TEST_FEES, swap_curve: swap_curve.clone(), + freeze_authority: COption::None, + freeze_authority_bit_mask: 0, }); let mut packed = [0u8; SwapVersion::LATEST_LEN];