From bbc5f57dccabe7effca934694c71b5f7e5b2d142 Mon Sep 17 00:00:00 2001 From: Jon Cinque Date: Fri, 2 Oct 2020 10:40:33 +0200 Subject: [PATCH] token-swap: Add slippage to swap / withdraw / deposit (#560) * token-swap: Add slippage to swap / withdraw / deposit * Update JS snake_case -> camelCase * Run prettier --- token-swap/js/cli/token-swap-test.js | 22 ++- token-swap/js/client/token-swap.js | 55 ++++-- token-swap/js/module.d.ts | 23 ++- token-swap/js/module.flow.js | 22 ++- token-swap/program/src/error.rs | 3 + token-swap/program/src/instruction.rs | 151 +++++++++++---- token-swap/program/src/processor.rs | 261 +++++++++++++++++++++++--- 7 files changed, 446 insertions(+), 91 deletions(-) diff --git a/token-swap/js/cli/token-swap-test.js b/token-swap/js/cli/token-swap-test.js index 94743c5d4f3258..ca5f0e4c7b34a4 100644 --- a/token-swap/js/cli/token-swap-test.js +++ b/token-swap/js/cli/token-swap-test.js @@ -37,8 +37,9 @@ let tokenAccountB: PublicKey; // Initial amount in each swap token const BASE_AMOUNT = 1000; -// Amount passed to instructions -const USER_AMOUNT = 100; +// Amount passed to swap instruction +const SWAP_AMOUNT_IN = 100; +const SWAP_AMOUNT_OUT = 69; // Pool token amount minted on init const DEFAULT_POOL_TOKEN_AMOUNT = 1000000000; // Pool token amount to withdraw / deposit @@ -253,6 +254,8 @@ export async function deposit(): Promise { newAccountPool, tokenProgramId, POOL_TOKEN_AMOUNT, + tokenA, + tokenB, ); let info; @@ -302,6 +305,8 @@ export async function withdraw(): Promise { userAccountB, tokenProgramId, POOL_TOKEN_AMOUNT, + tokenA, + tokenB, ); //const poolMintInfo = await tokenPool.getMintInfo(); @@ -323,8 +328,8 @@ export async function withdraw(): Promise { export async function swap(): Promise { console.log('Creating swap token a account'); let userAccountA = await mintA.createAccount(owner.publicKey); - await mintA.mintTo(userAccountA, owner, [], USER_AMOUNT); - await mintA.approve(userAccountA, authority, owner, [], USER_AMOUNT); + await mintA.mintTo(userAccountA, owner, [], SWAP_AMOUNT_IN); + await mintA.approve(userAccountA, authority, owner, [], SWAP_AMOUNT_IN); console.log('Creating swap token b account'); let userAccountB = await mintB.createAccount(owner.publicKey); const [tokenProgramId] = await GetPrograms(connection); @@ -337,18 +342,19 @@ export async function swap(): Promise { tokenAccountB, userAccountB, tokenProgramId, - USER_AMOUNT, + SWAP_AMOUNT_IN, + SWAP_AMOUNT_OUT, ); await sleep(500); let info; info = await mintA.getAccountInfo(userAccountA); assert(info.amount.toNumber() == 0); info = await mintA.getAccountInfo(tokenAccountA); - assert(info.amount.toNumber() == BASE_AMOUNT + USER_AMOUNT); + assert(info.amount.toNumber() == BASE_AMOUNT + SWAP_AMOUNT_IN); info = await mintB.getAccountInfo(tokenAccountB); - assert(info.amount.toNumber() == 931); + assert(info.amount.toNumber() == BASE_AMOUNT - SWAP_AMOUNT_OUT); info = await mintB.getAccountInfo(userAccountB); - assert(info.amount.toNumber() == 69); + assert(info.amount.toNumber() == SWAP_AMOUNT_OUT); info = await tokenPool.getAccountInfo(tokenAccountPool); assert( info.amount.toNumber() == DEFAULT_POOL_TOKEN_AMOUNT - POOL_TOKEN_AMOUNT, diff --git a/token-swap/js/client/token-swap.js b/token-swap/js/client/token-swap.js index da569dab246307..4daf6dbf9ba5e0 100644 --- a/token-swap/js/client/token-swap.js +++ b/token-swap/js/client/token-swap.js @@ -345,7 +345,8 @@ export class TokenSwap { swapDestination: PublicKey, destination: PublicKey, tokenProgramId: PublicKey, - amount: number | Numberu64, + amountIn: number | Numberu64, + minimumAmountOut: number | Numberu64, ): Promise { return await sendAndConfirmTransaction( 'swap', @@ -360,7 +361,8 @@ export class TokenSwap { destination, this.programId, tokenProgramId, - amount, + amountIn, + minimumAmountOut, ), ), this.payer, @@ -376,18 +378,21 @@ export class TokenSwap { destination: PublicKey, swapProgramId: PublicKey, tokenProgramId: PublicKey, - amount: number | Numberu64, + amountIn: number | Numberu64, + minimumAmountOut: number | Numberu64, ): TransactionInstruction { const dataLayout = BufferLayout.struct([ BufferLayout.u8('instruction'), - Layout.uint64('amount'), + Layout.uint64('amountIn'), + Layout.uint64('minimumAmountOut'), ]); const data = Buffer.alloc(dataLayout.span); dataLayout.encode( { instruction: 1, // Swap instruction - amount: new Numberu64(amount).toBuffer(), + amountIn: new Numberu64(amountIn).toBuffer(), + minimumAmountOut: new Numberu64(minimumAmountOut).toBuffer(), }, data, ); @@ -430,7 +435,9 @@ export class TokenSwap { poolToken: PublicKey, poolAccount: PublicKey, tokenProgramId: PublicKey, - amount: number | Numberu64, + poolTokenAmount: number | Numberu64, + maximumTokenA: number | Numberu64, + maximumTokenB: number | Numberu64, ): Promise { return await sendAndConfirmTransaction( 'deposit', @@ -447,7 +454,9 @@ export class TokenSwap { poolAccount, this.programId, tokenProgramId, - amount, + poolTokenAmount, + maximumTokenA, + maximumTokenB, ), ), this.payer, @@ -465,18 +474,24 @@ export class TokenSwap { poolAccount: PublicKey, swapProgramId: PublicKey, tokenProgramId: PublicKey, - amount: number | Numberu64, + poolTokenAmount: number | Numberu64, + maximumTokenA: number | Numberu64, + maximumTokenB: number | Numberu64, ): TransactionInstruction { const dataLayout = BufferLayout.struct([ BufferLayout.u8('instruction'), - Layout.uint64('amount'), + Layout.uint64('poolTokenAmount'), + Layout.uint64('maximumTokenA'), + Layout.uint64('maximumTokenB'), ]); const data = Buffer.alloc(dataLayout.span); dataLayout.encode( { instruction: 2, // Deposit instruction - amount: new Numberu64(amount).toBuffer(), + poolTokenAmount: new Numberu64(poolTokenAmount).toBuffer(), + maximumTokenA: new Numberu64(maximumTokenA).toBuffer(), + maximumTokenB: new Numberu64(maximumTokenB).toBuffer(), }, data, ); @@ -521,7 +536,9 @@ export class TokenSwap { userAccountA: PublicKey, userAccountB: PublicKey, tokenProgramId: PublicKey, - amount: number | Numberu64, + poolTokenAmount: number | Numberu64, + minimumTokenA: number | Numberu64, + minimumTokenB: number | Numberu64, ): Promise { return await sendAndConfirmTransaction( 'withdraw', @@ -538,7 +555,9 @@ export class TokenSwap { userAccountB, this.programId, tokenProgramId, - amount, + poolTokenAmount, + minimumTokenA, + minimumTokenB, ), ), this.payer, @@ -556,18 +575,24 @@ export class TokenSwap { userAccountB: PublicKey, swapProgramId: PublicKey, tokenProgramId: PublicKey, - amount: number | Numberu64, + poolTokenAmount: number | Numberu64, + minimumTokenA: number | Numberu64, + minimumTokenB: number | Numberu64, ): TransactionInstruction { const dataLayout = BufferLayout.struct([ BufferLayout.u8('instruction'), - Layout.uint64('amount'), + Layout.uint64('poolTokenAmount'), + Layout.uint64('minimumTokenA'), + Layout.uint64('minimumTokenB'), ]); const data = Buffer.alloc(dataLayout.span); dataLayout.encode( { instruction: 3, // Withdraw instruction - amount: new Numberu64(amount).toBuffer(), + poolTokenAmount: new Numberu64(poolTokenAmount).toBuffer(), + minimumTokenA: new Numberu64(minimumTokenA).toBuffer(), + minimumTokenB: new Numberu64(minimumTokenB).toBuffer(), }, data, ); diff --git a/token-swap/js/module.d.ts b/token-swap/js/module.d.ts index 5680b96e6b8152..f821c1f664ef77 100644 --- a/token-swap/js/module.d.ts +++ b/token-swap/js/module.d.ts @@ -71,6 +71,7 @@ declare module '@solana/spl-token-swap' { ): Promise; getInfo(): Promise; + swap( authority: PublicKey, source: PublicKey, @@ -78,7 +79,8 @@ declare module '@solana/spl-token-swap' { swapDestination: PublicKey, destination: PublicKey, tokenProgramId: PublicKey, - amount: number | Numberu64, + amountIn: number | Numberu64, + minimumAmountOut: number | Numberu64, ): Promise; static swapInstruction( @@ -90,7 +92,8 @@ declare module '@solana/spl-token-swap' { destination: PublicKey, swapProgramId: PublicKey, tokenProgramId: PublicKey, - amount: number | Numberu64, + amountIn: number | Numberu64, + minimumAmountOut: number | Numberu64, ): TransactionInstruction; deposit( @@ -102,7 +105,9 @@ declare module '@solana/spl-token-swap' { poolToken: PublicKey, poolAccount: PublicKey, tokenProgramId: PublicKey, - amount: number | Numberu64, + poolTokenAmount: number | Numberu64, + maximumTokenA: number | Numberu64, + maximumTokenB: number | Numberu64, ): Promise; static depositInstruction( @@ -116,7 +121,9 @@ declare module '@solana/spl-token-swap' { poolAccount: PublicKey, swapProgramId: PublicKey, tokenProgramId: PublicKey, - amount: number | Numberu64, + poolTokenAmount: number | Numberu64, + maximumTokenA: number | Numberu64, + maximumTokenB: number | Numberu64, ): TransactionInstruction; withdraw( @@ -128,7 +135,9 @@ declare module '@solana/spl-token-swap' { userAccountA: PublicKey, userAccountB: PublicKey, tokenProgramId: PublicKey, - amount: number | Numberu64, + poolTokenAmount: number | Numberu64, + minimumTokenA: number | Numberu64, + minimumTokenB: number | Numberu64, ): Promise; static withdrawInstruction( @@ -142,7 +151,9 @@ declare module '@solana/spl-token-swap' { userAccountB: PublicKey, swapProgramId: PublicKey, tokenProgramId: PublicKey, - amount: number | Numberu64, + poolTokenAmount: number | Numberu64, + minimumTokenA: number | Numberu64, + minimumTokenB: number | Numberu64, ): TransactionInstruction; } } diff --git a/token-swap/js/module.flow.js b/token-swap/js/module.flow.js index d713b39797acc6..ebe75e0b9d0361 100644 --- a/token-swap/js/module.flow.js +++ b/token-swap/js/module.flow.js @@ -75,7 +75,8 @@ declare module '@solana/spl-token-swap' { swapDestination: PublicKey, destination: PublicKey, tokenProgramId: PublicKey, - amount: number | Numberu64, + amountIn: number | Numberu64, + minimumAmountOut: number | Numberu64, ): Promise; static swapInstruction( @@ -87,7 +88,8 @@ declare module '@solana/spl-token-swap' { destination: PublicKey, swapProgramId: PublicKey, tokenProgramId: PublicKey, - amount: number | Numberu64, + amountIn: number | Numberu64, + minimumAmountOut: number | Numberu64, ): TransactionInstruction; deposit( @@ -99,7 +101,9 @@ declare module '@solana/spl-token-swap' { poolToken: PublicKey, poolAccount: PublicKey, tokenProgramId: PublicKey, - amount: number | Numberu64, + poolTokenAmount: number | Numberu64, + maximumTokenA: number | Numberu64, + maximumTokenB: number | Numberu64, ): Promise; static depositInstruction( @@ -113,7 +117,9 @@ declare module '@solana/spl-token-swap' { poolAccount: PublicKey, swapProgramId: PublicKey, tokenProgramId: PublicKey, - amount: number | Numberu64, + poolTokenAmount: number | Numberu64, + maximumTokenA: number | Numberu64, + maximumTokenB: number | Numberu64, ): TransactionInstruction; withdraw( @@ -125,7 +131,9 @@ declare module '@solana/spl-token-swap' { userAccountA: PublicKey, userAccountB: PublicKey, tokenProgramId: PublicKey, - amount: number | Numberu64, + poolTokenAmount: number | Numberu64, + minimumTokenA: number | Numberu64, + minimumTokenB: number | Numberu64, ): Promise; static withdrawInstruction( @@ -139,7 +147,9 @@ declare module '@solana/spl-token-swap' { userAccountB: PublicKey, swapProgramId: PublicKey, tokenProgramId: PublicKey, - amount: number | Numberu64, + poolTokenAmount: number | Numberu64, + minimumTokenA: number | Numberu64, + minimumTokenB: number | Numberu64, ): TransactionInstruction; } } diff --git a/token-swap/program/src/error.rs b/token-swap/program/src/error.rs index 59c4d3e393add4..9f682c96ce484e 100644 --- a/token-swap/program/src/error.rs +++ b/token-swap/program/src/error.rs @@ -55,6 +55,9 @@ pub enum SwapError { /// Swap input token accounts have the same mint #[error("Swap input token accounts have the same mint")] RepeatedMint, + /// Swap instruction exceeds desired slippage limit + #[error("Swap instruction exceeds desired slippage limit")] + ExceededSlippage, } 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 a01ec39c86eb3e..6632834cf09049 100644 --- a/token-swap/program/src/instruction.rs +++ b/token-swap/program/src/instruction.rs @@ -44,7 +44,9 @@ pub enum SwapInstruction { /// 6. '[]` Token program id Swap { /// SOURCE amount to transfer, output to DESTINATION is based on the exchange rate - amount: u64, + amount_in: u64, + /// Minimum amount of DESTINATION token to output, prevents excessive slippage + minimum_amount_out: u64, }, /// Deposit some tokens into the pool. The output is a "pool" token representing ownership @@ -62,7 +64,11 @@ pub enum SwapInstruction { Deposit { /// Pool token amount to transfer. token_a and token_b amount are set by /// the current exchange rate and size of the pool - amount: u64, + pool_token_amount: u64, + /// Maximum token A amount to deposit, prevents excessive slippage + maximum_token_a_amount: u64, + /// Maximum token B amount to deposit, prevents excessive slippage + maximum_token_b_amount: u64, }, /// Withdraw the token from the pool at the current ratio. @@ -79,7 +85,11 @@ pub enum SwapInstruction { Withdraw { /// Amount of pool tokens to burn. User receives an output of token a /// and b based on the percentage of the pool tokens that are returned. - amount: u64, + pool_token_amount: u64, + /// Minimum amount of token A to receive, prevents excessive slippage + minimum_token_a_amount: u64, + /// Minimum amount of token B to receive, prevents excessive slippage + minimum_token_b_amount: u64, }, } @@ -98,13 +108,32 @@ impl SwapInstruction { nonce, } } - 1 | 2 | 3 => { - let (amount, _rest) = Self::unpack_u64(rest)?; - match tag { - 1 => Self::Swap { amount }, - 2 => Self::Deposit { amount }, - 3 => Self::Withdraw { amount }, - _ => unreachable!(), + 1 => { + let (amount_in, rest) = Self::unpack_u64(rest)?; + let (minimum_amount_out, _rest) = Self::unpack_u64(rest)?; + Self::Swap { + amount_in, + minimum_amount_out, + } + } + 2 => { + let (pool_token_amount, rest) = Self::unpack_u64(rest)?; + let (maximum_token_a_amount, rest) = Self::unpack_u64(rest)?; + let (maximum_token_b_amount, _rest) = Self::unpack_u64(rest)?; + Self::Deposit { + pool_token_amount, + maximum_token_a_amount, + maximum_token_b_amount, + } + } + 3 => { + let (pool_token_amount, rest) = Self::unpack_u64(rest)?; + let (minimum_token_a_amount, rest) = Self::unpack_u64(rest)?; + let (minimum_token_b_amount, _rest) = Self::unpack_u64(rest)?; + Self::Withdraw { + pool_token_amount, + minimum_token_a_amount, + minimum_token_b_amount, } } _ => return Err(SwapError::InvalidInstruction.into()), @@ -139,17 +168,33 @@ impl SwapInstruction { buf.extend_from_slice(&fee_denominator.to_le_bytes()); buf.push(nonce); } - Self::Swap { amount } => { + Self::Swap { + amount_in, + minimum_amount_out, + } => { buf.push(1); - buf.extend_from_slice(&amount.to_le_bytes()); + buf.extend_from_slice(&amount_in.to_le_bytes()); + buf.extend_from_slice(&minimum_amount_out.to_le_bytes()); } - Self::Deposit { amount } => { + Self::Deposit { + pool_token_amount, + maximum_token_a_amount, + maximum_token_b_amount, + } => { buf.push(2); - buf.extend_from_slice(&amount.to_le_bytes()); + buf.extend_from_slice(&pool_token_amount.to_le_bytes()); + buf.extend_from_slice(&maximum_token_a_amount.to_le_bytes()); + buf.extend_from_slice(&maximum_token_b_amount.to_le_bytes()); } - Self::Withdraw { amount } => { + Self::Withdraw { + pool_token_amount, + minimum_token_a_amount, + minimum_token_b_amount, + } => { buf.push(3); - buf.extend_from_slice(&amount.to_le_bytes()); + buf.extend_from_slice(&pool_token_amount.to_le_bytes()); + buf.extend_from_slice(&minimum_token_a_amount.to_le_bytes()); + buf.extend_from_slice(&minimum_token_b_amount.to_le_bytes()); } } buf @@ -206,9 +251,16 @@ pub fn deposit( swap_token_b_pubkey: &Pubkey, pool_mint_pubkey: &Pubkey, destination_pubkey: &Pubkey, - amount: u64, + pool_token_amount: u64, + maximum_token_a_amount: u64, + maximum_token_b_amount: u64, ) -> Result { - let data = SwapInstruction::Deposit { amount }.pack(); + let data = SwapInstruction::Deposit { + pool_token_amount, + maximum_token_a_amount, + maximum_token_b_amount, + } + .pack(); let accounts = vec![ AccountMeta::new(*swap_pubkey, false), @@ -241,9 +293,16 @@ pub fn withdraw( swap_token_b_pubkey: &Pubkey, destination_token_a_pubkey: &Pubkey, destination_token_b_pubkey: &Pubkey, - amount: u64, + pool_token_amount: u64, + minimum_token_a_amount: u64, + minimum_token_b_amount: u64, ) -> Result { - let data = SwapInstruction::Withdraw { amount }.pack(); + let data = SwapInstruction::Withdraw { + pool_token_amount, + minimum_token_a_amount, + minimum_token_b_amount, + } + .pack(); let accounts = vec![ AccountMeta::new(*swap_pubkey, false), @@ -274,9 +333,14 @@ pub fn swap( swap_source_pubkey: &Pubkey, swap_destination_pubkey: &Pubkey, destination_pubkey: &Pubkey, - amount: u64, + amount_in: u64, + minimum_amount_out: u64, ) -> Result { - let data = SwapInstruction::Swap { amount }.pack(); + let data = SwapInstruction::Swap { + amount_in, + minimum_amount_out, + } + .pack(); let accounts = vec![ AccountMeta::new(*swap_pubkey, false), @@ -330,29 +394,50 @@ mod tests { let unpacked = SwapInstruction::unpack(&expect).unwrap(); assert_eq!(unpacked, check); - let amount = 2; - let check = SwapInstruction::Swap { amount }; + let amount_in: u64 = 2; + let minimum_amount_out: u64 = 10; + let check = SwapInstruction::Swap { + amount_in, + minimum_amount_out, + }; let packed = check.pack(); - let mut expect = vec![1, 2]; - expect.extend_from_slice(&[0u8; 7]); + let mut expect = vec![1]; + expect.extend_from_slice(&amount_in.to_le_bytes()); + expect.extend_from_slice(&minimum_amount_out.to_le_bytes()); assert_eq!(packed, expect); let unpacked = SwapInstruction::unpack(&expect).unwrap(); assert_eq!(unpacked, check); - let amount = 5; - let check = SwapInstruction::Deposit { amount }; + let pool_token_amount: u64 = 5; + let maximum_token_a_amount: u64 = 10; + let maximum_token_b_amount: u64 = 20; + let check = SwapInstruction::Deposit { + pool_token_amount, + maximum_token_a_amount, + maximum_token_b_amount, + }; let packed = check.pack(); - let mut expect = vec![2, 5]; - expect.extend_from_slice(&[0u8; 7]); + let mut expect = vec![2]; + expect.extend_from_slice(&pool_token_amount.to_le_bytes()); + expect.extend_from_slice(&maximum_token_a_amount.to_le_bytes()); + expect.extend_from_slice(&maximum_token_b_amount.to_le_bytes()); assert_eq!(packed, expect); let unpacked = SwapInstruction::unpack(&expect).unwrap(); assert_eq!(unpacked, check); - let amount: u64 = 1212438012089; - let check = SwapInstruction::Withdraw { amount }; + let pool_token_amount: u64 = 1212438012089; + let minimum_token_a_amount: u64 = 102198761982612; + let minimum_token_b_amount: u64 = 2011239855213; + let check = SwapInstruction::Withdraw { + pool_token_amount, + minimum_token_a_amount, + minimum_token_b_amount, + }; let packed = check.pack(); let mut expect = vec![3]; - expect.extend_from_slice(&amount.to_le_bytes()); + expect.extend_from_slice(&pool_token_amount.to_le_bytes()); + expect.extend_from_slice(&minimum_token_a_amount.to_le_bytes()); + expect.extend_from_slice(&minimum_token_b_amount.to_le_bytes()); 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 52acc41367ecbc..08f2cf172116ac 100644 --- a/token-swap/program/src/processor.rs +++ b/token-swap/program/src/processor.rs @@ -227,7 +227,8 @@ impl Processor { /// Processes an [Swap](enum.Instruction.html). pub fn process_swap( program_id: &Pubkey, - amount: u64, + amount_in: u64, + minimum_amount_out: u64, accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); @@ -260,7 +261,7 @@ impl Processor { let source_account = Self::unpack_token_account(&swap_source_info.data.borrow())?; let dest_account = Self::unpack_token_account(&swap_destination_info.data.borrow())?; - let output = if *swap_source_info.key == token_swap.token_a { + let amount_out = if *swap_source_info.key == token_swap.token_a { let mut invariant = ConstantProduct { token_a: source_account.amount, token_b: dest_account.amount, @@ -268,7 +269,7 @@ impl Processor { fee_denominator: token_swap.fee_denominator, }; invariant - .swap_a_to_b(amount) + .swap_a_to_b(amount_in) .ok_or(SwapError::CalculationFailure)? } else { let mut invariant = ConstantProduct { @@ -278,9 +279,12 @@ impl Processor { fee_denominator: token_swap.fee_denominator, }; invariant - .swap_b_to_a(amount) + .swap_b_to_a(amount_in) .ok_or(SwapError::CalculationFailure)? }; + if amount_out < minimum_amount_out { + return Err(SwapError::ExceededSlippage.into()); + } Self::token_transfer( swap_info.key, token_program_info.clone(), @@ -288,7 +292,7 @@ impl Processor { swap_source_info.clone(), authority_info.clone(), token_swap.nonce, - amount, + amount_in, )?; Self::token_transfer( swap_info.key, @@ -297,7 +301,7 @@ impl Processor { destination_info.clone(), authority_info.clone(), token_swap.nonce, - output, + amount_out, )?; Ok(()) } @@ -305,7 +309,9 @@ impl Processor { /// Processes an [Deposit](enum.Instruction.html). pub fn process_deposit( program_id: &Pubkey, - pool_amount: u64, + pool_token_amount: u64, + maximum_token_a_amount: u64, + maximum_token_b_amount: u64, accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); @@ -341,11 +347,17 @@ impl Processor { PoolTokenConverter::new_existing(pool_mint.supply, token_a.amount, token_b.amount); let a_amount = converter - .token_a_rate(pool_amount) + .token_a_rate(pool_token_amount) .ok_or(SwapError::CalculationFailure)?; + if a_amount > maximum_token_a_amount { + return Err(SwapError::ExceededSlippage.into()); + } let b_amount = converter - .token_b_rate(pool_amount) + .token_b_rate(pool_token_amount) .ok_or(SwapError::CalculationFailure)?; + if b_amount > maximum_token_b_amount { + return Err(SwapError::ExceededSlippage.into()); + } Self::token_transfer( swap_info.key, @@ -372,7 +384,7 @@ impl Processor { dest_info.clone(), authority_info.clone(), token_swap.nonce, - pool_amount, + pool_token_amount, )?; Ok(()) @@ -381,7 +393,9 @@ impl Processor { /// Processes an [Withdraw](enum.Instruction.html). pub fn process_withdraw( program_id: &Pubkey, - pool_amount: u64, + pool_token_amount: u64, + minimum_token_a_amount: u64, + minimum_token_b_amount: u64, accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); @@ -417,11 +431,17 @@ impl Processor { PoolTokenConverter::new_existing(pool_mint.supply, token_a.amount, token_b.amount); let a_amount = converter - .token_a_rate(pool_amount) + .token_a_rate(pool_token_amount) .ok_or(SwapError::CalculationFailure)?; + if a_amount < minimum_token_a_amount { + return Err(SwapError::ExceededSlippage.into()); + } let b_amount = converter - .token_b_rate(pool_amount) + .token_b_rate(pool_token_amount) .ok_or(SwapError::CalculationFailure)?; + if b_amount < minimum_token_b_amount { + return Err(SwapError::ExceededSlippage.into()); + } Self::token_transfer( swap_info.key, @@ -448,7 +468,7 @@ impl Processor { pool_mint_info.clone(), authority_info.clone(), token_swap.nonce, - pool_amount, + pool_token_amount, )?; Ok(()) } @@ -471,17 +491,40 @@ impl Processor { accounts, ) } - SwapInstruction::Swap { amount } => { + SwapInstruction::Swap { + amount_in, + minimum_amount_out, + } => { info!("Instruction: Swap"); - Self::process_swap(program_id, amount, accounts) + Self::process_swap(program_id, amount_in, minimum_amount_out, accounts) } - SwapInstruction::Deposit { amount } => { + SwapInstruction::Deposit { + pool_token_amount, + maximum_token_a_amount, + maximum_token_b_amount, + } => { info!("Instruction: Deposit"); - Self::process_deposit(program_id, amount, accounts) + Self::process_deposit( + program_id, + pool_token_amount, + maximum_token_a_amount, + maximum_token_b_amount, + accounts, + ) } - SwapInstruction::Withdraw { amount } => { + SwapInstruction::Withdraw { + pool_token_amount, + minimum_token_a_amount, + minimum_token_b_amount, + } => { info!("Instruction: Withdraw"); - Self::process_withdraw(program_id, amount, accounts) + Self::process_withdraw( + program_id, + pool_token_amount, + minimum_token_a_amount, + minimum_token_b_amount, + accounts, + ) } } } @@ -559,6 +602,9 @@ impl PrintProgramError for SwapError { SwapError::InvalidOutput => info!("Error: InvalidOutput"), SwapError::CalculationFailure => info!("Error: CalculationFailure"), SwapError::InvalidInstruction => info!("Error: InvalidInstruction"), + SwapError::ExceededSlippage => { + info!("Error: Swap instruction exceeds desired slippage limit") + } } } } @@ -771,7 +817,8 @@ mod tests { swap_destination_key: &Pubkey, user_destination_key: &Pubkey, mut user_destination_account: &mut Account, - amount: u64, + amount_in: u64, + minimum_amount_out: u64, ) -> ProgramResult { // approve moving from user source account do_process_instruction( @@ -781,7 +828,7 @@ mod tests { &self.authority_key, &user_key, &[], - amount, + amount_in, ) .unwrap(), vec![ @@ -806,7 +853,8 @@ mod tests { &swap_source_key, &swap_destination_key, &user_destination_key, - amount, + amount_in, + minimum_amount_out, ) .unwrap(), vec![ @@ -888,6 +936,8 @@ mod tests { &self.pool_mint_key, &depositor_pool_key, pool_amount, + amount_a, + amount_b, ) .unwrap(), vec![ @@ -914,6 +964,8 @@ mod tests { token_b_key: &Pubkey, mut token_b_account: &mut Account, pool_amount: u64, + minimum_a_amount: u64, + minimum_b_amount: u64, ) -> ProgramResult { // approve swap program to take out pool tokens do_process_instruction( @@ -948,6 +1000,8 @@ mod tests { &token_a_key, &token_b_key, pool_amount, + minimum_a_amount, + minimum_b_amount, ) .unwrap(), vec![ @@ -1719,6 +1773,8 @@ mod tests { &accounts.pool_mint_key, &pool_key, pool_amount, + deposit_a, + deposit_b, ) .unwrap(), vec![ @@ -1762,6 +1818,8 @@ mod tests { &accounts.pool_mint_key, &pool_key, pool_amount, + deposit_a, + deposit_b, ) .unwrap(), vec![ @@ -1880,6 +1938,50 @@ mod tests { accounts.pool_mint_account = old_pool_account; } + // slippage exceeeded + { + 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); + // maximum A amount in too low + assert_eq!( + Err(SwapError::ExceededSlippage.into()), + accounts.deposit( + &depositor_key, + &pool_key, + &mut pool_account, + &token_a_key, + &mut token_a_account, + &token_b_key, + &mut token_b_account, + pool_amount, + deposit_a / 10, + deposit_b, + ) + ); + // maximum B amount in too low + assert_eq!( + Err(SwapError::ExceededSlippage.into()), + accounts.deposit( + &depositor_key, + &pool_key, + &mut pool_account, + &token_a_key, + &mut token_a_account, + &token_b_key, + &mut token_b_account, + pool_amount, + deposit_a, + deposit_b / 10, + ) + ); + } + // correctly deposit { let ( @@ -1946,6 +2048,8 @@ mod tests { let initial_b = token_b_amount / 10; let initial_pool = pool_converter.supply / 10; let withdraw_amount = initial_pool / 4; + let minimum_a_amount = initial_a / 40; + let minimum_b_amount = initial_b / 40; // swap not initialized { @@ -1968,6 +2072,8 @@ mod tests { &token_b_key, &mut token_b_account, withdraw_amount, + minimum_a_amount, + minimum_b_amount, ) ); } @@ -2001,6 +2107,8 @@ mod tests { &token_b_key, &mut token_b_account, withdraw_amount, + minimum_a_amount, + minimum_b_amount, ) ); accounts.authority_key = old_authority; @@ -2033,6 +2141,8 @@ mod tests { &token_b_key, &mut token_b_account, withdraw_amount, + minimum_a_amount / 2, + minimum_b_amount / 2, ) ); } @@ -2064,6 +2174,8 @@ mod tests { &token_a_key, &mut token_a_account, withdraw_amount, + minimum_a_amount, + minimum_b_amount, ) ); } @@ -2109,6 +2221,8 @@ mod tests { &token_b_key, &mut token_b_account, withdraw_amount, + minimum_a_amount, + minimum_b_amount, ) ); } @@ -2138,6 +2252,8 @@ mod tests { &token_a_key, &token_b_key, withdraw_amount, + minimum_a_amount, + minimum_b_amount, ) .unwrap(), vec![ @@ -2187,6 +2303,8 @@ mod tests { &token_a_key, &token_b_key, withdraw_amount, + minimum_a_amount, + minimum_b_amount, ) .unwrap(), vec![ @@ -2239,6 +2357,8 @@ mod tests { &token_b_key, &mut token_b_account, withdraw_amount, + minimum_a_amount, + minimum_b_amount, ) ); @@ -2263,6 +2383,8 @@ mod tests { &token_b_key, &mut token_b_account, withdraw_amount, + minimum_a_amount, + minimum_b_amount, ) ); @@ -2304,6 +2426,8 @@ mod tests { &token_b_key, &mut token_b_account, withdraw_amount, + minimum_a_amount, + minimum_b_amount, ) ); @@ -2311,6 +2435,56 @@ mod tests { accounts.pool_mint_account = old_pool_account; } + // slippage exceeeded + { + 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, + ); + // minimum A amount out too high + assert_eq!( + Err(SwapError::ExceededSlippage.into()), + accounts.withdraw( + &withdrawer_key, + &pool_key, + &mut pool_account, + &token_a_key, + &mut token_a_account, + &token_b_key, + &mut token_b_account, + withdraw_amount, + minimum_a_amount * 10, + minimum_b_amount, + ) + ); + // minimum B amount out too high + assert_eq!( + Err(SwapError::ExceededSlippage.into()), + accounts.withdraw( + &withdrawer_key, + &pool_key, + &mut pool_account, + &token_a_key, + &mut token_a_account, + &token_b_key, + &mut token_b_account, + withdraw_amount, + minimum_a_amount, + minimum_b_amount * 10, + ) + ); + } + // correct withdrawal { let ( @@ -2338,6 +2512,8 @@ mod tests { &token_b_key, &mut token_b_account, withdraw_amount, + minimum_a_amount, + minimum_b_amount, ) .unwrap(); @@ -2382,6 +2558,7 @@ mod tests { ); let initial_a = token_a_amount / 5; let initial_b = token_b_amount / 5; + let minimum_b_amount = initial_b / 2; let swap_token_a_key = accounts.token_a_key.clone(); let swap_token_b_key = accounts.token_b_key.clone(); @@ -2407,6 +2584,7 @@ mod tests { &token_b_key, &mut token_b_account, initial_a, + minimum_b_amount, ) ); } @@ -2440,6 +2618,7 @@ mod tests { &token_b_key, &mut token_b_account, initial_a, + minimum_b_amount, ) ); accounts.authority_key = old_authority; @@ -2469,6 +2648,7 @@ mod tests { &accounts.token_b_key, &token_b_key, initial_a, + minimum_b_amount, ) .unwrap(), vec![ @@ -2505,6 +2685,7 @@ mod tests { &token_b_key, &mut token_b_account, initial_a * 2, + minimum_b_amount * 2, ) ); } @@ -2532,6 +2713,7 @@ mod tests { &token_b_key, &token_b_key, initial_a, + minimum_b_amount, ) .unwrap(), vec![ @@ -2568,6 +2750,7 @@ mod tests { &token_a_key, &mut token_a_account, initial_a, + minimum_b_amount, ) ); } @@ -2593,6 +2776,7 @@ mod tests { &token_a_key, &mut token_a_account, initial_a, + minimum_b_amount, ) ); } @@ -2620,6 +2804,7 @@ mod tests { &accounts.token_b_key, &token_b_key, initial_a, + minimum_b_amount, ) .unwrap(), vec![ @@ -2635,6 +2820,32 @@ mod tests { ); } + // slippage exceeeded: minimum out amount too high + { + 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); + assert_eq!( + Err(SwapError::ExceededSlippage.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, + initial_a, + minimum_b_amount * 2, + ) + ); + } + // correct swap { let ( @@ -2647,6 +2858,7 @@ mod tests { ) = accounts.setup_token_accounts(&user_key, &swapper_key, initial_a, initial_b, 0); // swap one way let a_to_b_amount = initial_a / 10; + let minimum_b_amount = initial_b / 20; accounts .swap( &swapper_key, @@ -2657,6 +2869,7 @@ mod tests { &token_b_key, &mut token_b_account, a_to_b_amount, + minimum_b_amount, ) .unwrap(); @@ -2687,6 +2900,7 @@ mod tests { // swap the other way let b_to_a_amount = initial_b / 10; + let minimum_a_amount = initial_a / 20; accounts .swap( &swapper_key, @@ -2697,6 +2911,7 @@ mod tests { &token_a_key, &mut token_a_account, b_to_a_amount, + minimum_a_amount, ) .unwrap();