diff --git a/token-lending/program/src/error.rs b/token-lending/program/src/error.rs index 05aca35d..23da1977 100644 --- a/token-lending/program/src/error.rs +++ b/token-lending/program/src/error.rs @@ -160,6 +160,9 @@ pub enum LendingError { #[error("Not enough liquidity after flash loan")] NotEnoughLiquidityAfterFlashLoan, // 45 + /// Lending instruction exceeds desired slippage limit + #[error("Amount smaller than desired slippage limit")] + ExceededSlippage, } impl From for ProgramError { diff --git a/token-lending/program/src/instruction.rs b/token-lending/program/src/instruction.rs index 5ff50cf5..8b4c50b4 100644 --- a/token-lending/program/src/instruction.rs +++ b/token-lending/program/src/instruction.rs @@ -219,7 +219,8 @@ pub enum LendingInstruction { BorrowObligationLiquidity { /// Amount of liquidity to borrow - u64::MAX for 100% of borrowing power liquidity_amount: u64, - // @TODO: slippage constraint - https://git.io/JmV67 + /// Minimum amount of liquidity to receive, if borrowing 100% of borrowing power + slippage_limit: u64, }, // 11 @@ -370,8 +371,12 @@ impl LendingInstruction { Self::WithdrawObligationCollateral { collateral_amount } } 10 => { - let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; - Self::BorrowObligationLiquidity { liquidity_amount } + let (liquidity_amount, rest) = Self::unpack_u64(rest)?; + let (slippage_limit, _rest) = Self::unpack_u64(rest).unwrap_or((0, &[])); + Self::BorrowObligationLiquidity { + liquidity_amount, + slippage_limit, + } } 11 => { let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; @@ -525,9 +530,13 @@ impl LendingInstruction { buf.push(9); buf.extend_from_slice(&collateral_amount.to_le_bytes()); } - Self::BorrowObligationLiquidity { liquidity_amount } => { + Self::BorrowObligationLiquidity { + liquidity_amount, + slippage_limit, + } => { buf.push(10); buf.extend_from_slice(&liquidity_amount.to_le_bytes()); + buf.extend_from_slice(&slippage_limit.to_le_bytes()); } Self::RepayObligationLiquidity { liquidity_amount } => { buf.push(11); @@ -860,6 +869,7 @@ pub fn withdraw_obligation_collateral( pub fn borrow_obligation_liquidity( program_id: Pubkey, liquidity_amount: u64, + slippage_limit: Option, source_liquidity_pubkey: Pubkey, destination_liquidity_pubkey: Pubkey, borrow_reserve_pubkey: Pubkey, @@ -888,10 +898,15 @@ pub fn borrow_obligation_liquidity( if let Some(host_fee_receiver_pubkey) = host_fee_receiver_pubkey { accounts.push(AccountMeta::new(host_fee_receiver_pubkey, false)); } + let slippage_limit = slippage_limit.unwrap_or(0); Instruction { program_id, accounts, - data: LendingInstruction::BorrowObligationLiquidity { liquidity_amount }.pack(), + data: LendingInstruction::BorrowObligationLiquidity { + liquidity_amount, + slippage_limit, + } + .pack(), } } @@ -1309,6 +1324,7 @@ mod tests { let instruction = borrow_obligation_liquidity( program_id, liquidity_amount, + None, source_liquidity_pubkey, destination_liquidity_pubkey, borrow_reserve_pubkey, @@ -1322,7 +1338,11 @@ mod tests { assert_eq!(instruction.accounts.len(), 11); assert_eq!( instruction.data, - LendingInstruction::BorrowObligationLiquidity { liquidity_amount }.pack() + LendingInstruction::BorrowObligationLiquidity { + liquidity_amount, + slippage_limit: 0 + } + .pack() ); } diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index d4258569..a93d5669 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -83,9 +83,17 @@ pub fn process_instruction( msg!("Instruction: Withdraw Obligation Collateral"); process_withdraw_obligation_collateral(program_id, collateral_amount, accounts) } - LendingInstruction::BorrowObligationLiquidity { liquidity_amount } => { + LendingInstruction::BorrowObligationLiquidity { + liquidity_amount, + slippage_limit, + } => { msg!("Instruction: Borrow Obligation Liquidity"); - process_borrow_obligation_liquidity(program_id, liquidity_amount, accounts) + process_borrow_obligation_liquidity( + program_id, + liquidity_amount, + slippage_limit, + accounts, + ) } LendingInstruction::RepayObligationLiquidity { liquidity_amount } => { msg!("Instruction: Repay Obligation Liquidity"); @@ -1023,6 +1031,7 @@ fn process_withdraw_obligation_collateral( fn process_borrow_obligation_liquidity( program_id: &Pubkey, liquidity_amount: u64, + slippage_limit: u64, accounts: &[AccountInfo], ) -> ProgramResult { if liquidity_amount == 0 { @@ -1141,6 +1150,11 @@ fn process_borrow_obligation_liquidity( return Err(LendingError::BorrowTooSmall.into()); } + if liquidity_amount == u64::MAX && receive_amount < slippage_limit { + msg!("Received liquidity would be smaller than the desired slippage limit"); + return Err(LendingError::ExceededSlippage.into()); + } + borrow_reserve.liquidity.borrow(borrow_amount)?; borrow_reserve.last_update.mark_stale(); Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; diff --git a/token-lending/program/tests/borrow_obligation_liquidity.rs b/token-lending/program/tests/borrow_obligation_liquidity.rs index 1ae0de23..cf5df6eb 100644 --- a/token-lending/program/tests/borrow_obligation_liquidity.rs +++ b/token-lending/program/tests/borrow_obligation_liquidity.rs @@ -102,6 +102,7 @@ async fn test_borrow_usdc_fixed_amount() { borrow_obligation_liquidity( spl_token_lending::id(), USDC_BORROW_AMOUNT_FRACTIONAL, + None, usdc_test_reserve.liquidity_supply_pubkey, usdc_test_reserve.user_liquidity_pubkey, usdc_test_reserve.pubkey, @@ -249,6 +250,7 @@ async fn test_borrow_sol_max_amount() { borrow_obligation_liquidity( spl_token_lending::id(), u64::MAX, + None, sol_test_reserve.liquidity_supply_pubkey, sol_test_reserve.user_liquidity_pubkey, sol_test_reserve.pubkey, @@ -380,6 +382,7 @@ async fn test_borrow_too_large() { borrow_obligation_liquidity( spl_token_lending::id(), USDC_BORROW_AMOUNT_FRACTIONAL, + None, usdc_test_reserve.liquidity_supply_pubkey, usdc_test_reserve.user_liquidity_pubkey, usdc_test_reserve.pubkey, @@ -408,3 +411,353 @@ async fn test_borrow_too_large() { ) ); } + +#[tokio::test] +async fn test_borrow_max_receive_minimum() { + let mut test = ProgramTest::new( + "spl_token_lending", + spl_token_lending::id(), + processor!(process_instruction), + ); + + // limit to track compute unit increase + test.set_compute_max_units(60_000); + + const FEE_AMOUNT: u64 = 5000; + const HOST_FEE_AMOUNT: u64 = 1000; + + const USDC_DEPOSIT_AMOUNT_FRACTIONAL: u64 = + 2_000 * FRACTIONAL_TO_USDC * INITIAL_COLLATERAL_RATIO; + const SOL_BORROW_AMOUNT_LAMPORTS: u64 = 50 * LAMPORTS_TO_SOL; + const USDC_RESERVE_COLLATERAL_FRACTIONAL: u64 = 2 * USDC_DEPOSIT_AMOUNT_FRACTIONAL; + const SOL_RESERVE_LIQUIDITY_LAMPORTS: u64 = 2 * SOL_BORROW_AMOUNT_LAMPORTS; + const SLIPPAGE_LIMIT: u64 = SOL_BORROW_AMOUNT_LAMPORTS - FEE_AMOUNT; + + let user_accounts_owner = Keypair::new(); + let lending_market = add_lending_market(&mut test); + + let mut reserve_config = TEST_RESERVE_CONFIG; + reserve_config.loan_to_value_ratio = 50; + + let usdc_mint = add_usdc_mint(&mut test); + let usdc_oracle = add_usdc_oracle(&mut test); + let usdc_test_reserve = add_reserve( + &mut test, + &lending_market, + &usdc_oracle, + &user_accounts_owner, + AddReserveArgs { + liquidity_amount: USDC_RESERVE_COLLATERAL_FRACTIONAL, + liquidity_mint_pubkey: usdc_mint.pubkey, + liquidity_mint_decimals: usdc_mint.decimals, + config: reserve_config, + mark_fresh: true, + ..AddReserveArgs::default() + }, + ); + + let sol_oracle = add_sol_oracle(&mut test); + let sol_test_reserve = add_reserve( + &mut test, + &lending_market, + &sol_oracle, + &user_accounts_owner, + AddReserveArgs { + liquidity_amount: SOL_RESERVE_LIQUIDITY_LAMPORTS, + liquidity_mint_pubkey: spl_token::native_mint::id(), + liquidity_mint_decimals: 9, + config: reserve_config, + mark_fresh: true, + ..AddReserveArgs::default() + }, + ); + + let test_obligation = add_obligation( + &mut test, + &lending_market, + &user_accounts_owner, + AddObligationArgs { + deposits: &[(&usdc_test_reserve, USDC_DEPOSIT_AMOUNT_FRACTIONAL)], + ..AddObligationArgs::default() + }, + ); + + let (mut banks_client, payer, recent_blockhash) = test.start().await; + + let initial_liquidity_supply = + get_token_balance(&mut banks_client, sol_test_reserve.liquidity_supply_pubkey).await; + + let mut transaction = Transaction::new_with_payer( + &[ + refresh_obligation( + spl_token_lending::id(), + test_obligation.pubkey, + vec![usdc_test_reserve.pubkey], + ), + borrow_obligation_liquidity( + spl_token_lending::id(), + u64::MAX, + Some(SLIPPAGE_LIMIT), + sol_test_reserve.liquidity_supply_pubkey, + sol_test_reserve.user_liquidity_pubkey, + sol_test_reserve.pubkey, + sol_test_reserve.liquidity_fee_receiver_pubkey, + test_obligation.pubkey, + lending_market.pubkey, + test_obligation.owner, + Some(sol_test_reserve.liquidity_host_pubkey), + ), + ], + Some(&payer.pubkey()), + ); + + transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + assert!(banks_client.process_transaction(transaction).await.is_ok()); + + let sol_reserve = sol_test_reserve.get_state(&mut banks_client).await; + let obligation = test_obligation.get_state(&mut banks_client).await; + + let (total_fee, host_fee) = sol_reserve + .config + .fees + .calculate_borrow_fees(SOL_BORROW_AMOUNT_LAMPORTS.into(), FeeCalculation::Inclusive) + .unwrap(); + + assert_eq!(total_fee, FEE_AMOUNT); + assert_eq!(host_fee, HOST_FEE_AMOUNT); + + let borrow_amount = + get_token_balance(&mut banks_client, sol_test_reserve.user_liquidity_pubkey).await; + assert_eq!(borrow_amount, SOL_BORROW_AMOUNT_LAMPORTS - FEE_AMOUNT); + + let liquidity = &obligation.borrows[0]; + assert_eq!( + liquidity.borrowed_amount_wads, + Decimal::from(SOL_BORROW_AMOUNT_LAMPORTS) + ); + + let liquidity_supply = + get_token_balance(&mut banks_client, sol_test_reserve.liquidity_supply_pubkey).await; + assert_eq!( + liquidity_supply, + initial_liquidity_supply - SOL_BORROW_AMOUNT_LAMPORTS + ); + + let fee_balance = get_token_balance( + &mut banks_client, + sol_test_reserve.liquidity_fee_receiver_pubkey, + ) + .await; + assert_eq!(fee_balance, FEE_AMOUNT - HOST_FEE_AMOUNT); + + let host_fee_balance = + get_token_balance(&mut banks_client, sol_test_reserve.liquidity_host_pubkey).await; + assert_eq!(host_fee_balance, HOST_FEE_AMOUNT); +} + +#[tokio::test] +async fn test_borrow_max_receive_less_than_slippage() { + let mut test = ProgramTest::new( + "spl_token_lending", + spl_token_lending::id(), + processor!(process_instruction), + ); + + // limit to track compute unit increase + test.set_compute_max_units(60_000); + + const FEE_AMOUNT: u64 = 5000; + + const USDC_DEPOSIT_AMOUNT_FRACTIONAL: u64 = + 2_000 * FRACTIONAL_TO_USDC * INITIAL_COLLATERAL_RATIO; + const SOL_BORROW_AMOUNT_LAMPORTS: u64 = 50 * LAMPORTS_TO_SOL; + const USDC_RESERVE_COLLATERAL_FRACTIONAL: u64 = 2 * USDC_DEPOSIT_AMOUNT_FRACTIONAL; + const SOL_RESERVE_LIQUIDITY_LAMPORTS: u64 = 2 * SOL_BORROW_AMOUNT_LAMPORTS; + const SLIPPAGE_LIMIT: u64 = SOL_BORROW_AMOUNT_LAMPORTS - FEE_AMOUNT + 1; + + let user_accounts_owner = Keypair::new(); + let lending_market = add_lending_market(&mut test); + + let mut reserve_config = TEST_RESERVE_CONFIG; + reserve_config.loan_to_value_ratio = 50; + + let usdc_mint = add_usdc_mint(&mut test); + let usdc_oracle = add_usdc_oracle(&mut test); + let usdc_test_reserve = add_reserve( + &mut test, + &lending_market, + &usdc_oracle, + &user_accounts_owner, + AddReserveArgs { + liquidity_amount: USDC_RESERVE_COLLATERAL_FRACTIONAL, + liquidity_mint_pubkey: usdc_mint.pubkey, + liquidity_mint_decimals: usdc_mint.decimals, + config: reserve_config, + mark_fresh: true, + ..AddReserveArgs::default() + }, + ); + + let sol_oracle = add_sol_oracle(&mut test); + let sol_test_reserve = add_reserve( + &mut test, + &lending_market, + &sol_oracle, + &user_accounts_owner, + AddReserveArgs { + liquidity_amount: SOL_RESERVE_LIQUIDITY_LAMPORTS, + liquidity_mint_pubkey: spl_token::native_mint::id(), + liquidity_mint_decimals: 9, + config: reserve_config, + mark_fresh: true, + ..AddReserveArgs::default() + }, + ); + + let test_obligation = add_obligation( + &mut test, + &lending_market, + &user_accounts_owner, + AddObligationArgs { + deposits: &[(&usdc_test_reserve, USDC_DEPOSIT_AMOUNT_FRACTIONAL)], + ..AddObligationArgs::default() + }, + ); + + let (mut banks_client, payer, recent_blockhash) = test.start().await; + + let mut transaction = Transaction::new_with_payer( + &[ + refresh_obligation( + spl_token_lending::id(), + test_obligation.pubkey, + vec![usdc_test_reserve.pubkey], + ), + borrow_obligation_liquidity( + spl_token_lending::id(), + u64::MAX, + Some(SLIPPAGE_LIMIT), + sol_test_reserve.liquidity_supply_pubkey, + sol_test_reserve.user_liquidity_pubkey, + sol_test_reserve.pubkey, + sol_test_reserve.liquidity_fee_receiver_pubkey, + test_obligation.pubkey, + lending_market.pubkey, + test_obligation.owner, + Some(sol_test_reserve.liquidity_host_pubkey), + ), + ], + Some(&payer.pubkey()), + ); + + transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + assert_eq!( + banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 1, + InstructionError::Custom(LendingError::ExceededSlippage as u32) + ) + ); +} + +#[tokio::test] +async fn test_borrow_less_than_max_with_slippage() { + let mut test = ProgramTest::new( + "spl_token_lending", + spl_token_lending::id(), + processor!(process_instruction), + ); + + const USDC_TOTAL_BORROW_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC; + const FEE_AMOUNT: u64 = 100; + + const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO; + const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = USDC_TOTAL_BORROW_FRACTIONAL - FEE_AMOUNT; + const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS; + const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 2 * USDC_TOTAL_BORROW_FRACTIONAL; + const SLIPPAGE_LIMIT: u64 = u64::MAX; + + let user_accounts_owner = Keypair::new(); + let lending_market = add_lending_market(&mut test); + + let mut reserve_config = TEST_RESERVE_CONFIG; + reserve_config.loan_to_value_ratio = 50; + + let sol_oracle = add_sol_oracle(&mut test); + let sol_test_reserve = add_reserve( + &mut test, + &lending_market, + &sol_oracle, + &user_accounts_owner, + AddReserveArgs { + collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS, + liquidity_mint_pubkey: spl_token::native_mint::id(), + liquidity_mint_decimals: 9, + config: reserve_config, + mark_fresh: true, + ..AddReserveArgs::default() + }, + ); + + let usdc_mint = add_usdc_mint(&mut test); + let usdc_oracle = add_usdc_oracle(&mut test); + let usdc_test_reserve = add_reserve( + &mut test, + &lending_market, + &usdc_oracle, + &user_accounts_owner, + AddReserveArgs { + liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL, + liquidity_mint_pubkey: usdc_mint.pubkey, + liquidity_mint_decimals: usdc_mint.decimals, + config: reserve_config, + mark_fresh: true, + ..AddReserveArgs::default() + }, + ); + + let test_obligation = add_obligation( + &mut test, + &lending_market, + &user_accounts_owner, + AddObligationArgs { + deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)], + ..AddObligationArgs::default() + }, + ); + + let (mut banks_client, payer, recent_blockhash) = test.start().await; + + let mut transaction = Transaction::new_with_payer( + &[ + refresh_obligation( + spl_token_lending::id(), + test_obligation.pubkey, + vec![sol_test_reserve.pubkey], + ), + borrow_obligation_liquidity( + spl_token_lending::id(), + USDC_BORROW_AMOUNT_FRACTIONAL, + Some(SLIPPAGE_LIMIT), + usdc_test_reserve.liquidity_supply_pubkey, + usdc_test_reserve.user_liquidity_pubkey, + usdc_test_reserve.pubkey, + usdc_test_reserve.liquidity_fee_receiver_pubkey, + test_obligation.pubkey, + lending_market.pubkey, + test_obligation.owner, + Some(usdc_test_reserve.liquidity_host_pubkey), + ), + ], + Some(&payer.pubkey()), + ); + + transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash); + + // check that transaction succeeds + banks_client.process_transaction(transaction).await.unwrap(); +} diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index 1b0ad891..f37c761f 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -651,6 +651,7 @@ impl TestLendingMarket { &[borrow_obligation_liquidity( spl_token_lending::id(), liquidity_amount, + None, borrow_reserve.liquidity_supply_pubkey, borrow_reserve.user_liquidity_pubkey, borrow_reserve.pubkey, diff --git a/token-lending/program/tests/obligation_end_to_end.rs b/token-lending/program/tests/obligation_end_to_end.rs index 1f306c63..61901888 100644 --- a/token-lending/program/tests/obligation_end_to_end.rs +++ b/token-lending/program/tests/obligation_end_to_end.rs @@ -165,6 +165,7 @@ async fn test_success() { borrow_obligation_liquidity( spl_token_lending::id(), USDC_BORROW_AMOUNT_FRACTIONAL, + None, usdc_test_reserve.liquidity_supply_pubkey, usdc_test_reserve.user_liquidity_pubkey, usdc_test_reserve.pubkey,