From ec3276f06bfeb091a87378ce6b15e20ccb1aea85 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Wed, 22 Nov 2023 13:10:15 -0500 Subject: [PATCH] block on withdraw if borrow attribution limits are exceeded --- token-lending/program/src/processor.rs | 21 ++ .../program/tests/attributed_borrows.rs | 195 ++++++++++++++++++ token-lending/program/tests/borrow_weight.rs | 2 + .../tests/helpers/solend_program_test.rs | 16 +- .../tests/withdraw_obligation_collateral.rs | 6 + ...ollateral_and_redeem_reserve_collateral.rs | 2 + token-lending/sdk/src/instruction.rs | 68 +++--- 7 files changed, 284 insertions(+), 26 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 534c48da096..1e4a071f596 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1390,6 +1390,7 @@ fn process_withdraw_obligation_collateral( clock, token_program_id, false, + &accounts[8..], )?; Ok(()) } @@ -1408,6 +1409,7 @@ fn _withdraw_obligation_collateral<'a>( clock: &Clock, token_program_id: &AccountInfo<'a>, account_for_rate_limiter: bool, + deposit_reserve_infos: &[AccountInfo], ) -> Result { let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { @@ -1528,8 +1530,26 @@ fn _withdraw_obligation_collateral<'a>( return Err(LendingError::WithdrawTooLarge.into()); } + let withdraw_value = withdraw_reserve.market_value( + withdraw_reserve + .collateral_exchange_rate()? + .decimal_collateral_to_liquidity(Decimal::from(withdraw_amount))?, + )?; + + // update relevant values before updating borrow attribution values + obligation.deposited_value = obligation.deposited_value.saturating_sub(withdraw_value); + + obligation.deposits[collateral_index].market_value = obligation.deposits[collateral_index] + .market_value + .saturating_sub(withdraw_value); + + update_borrow_attribution_values(&mut obligation, deposit_reserve_infos)?; + + // obligation.withdraw must be called after updating borrow attribution values, since we can + // lose information if an entire deposit is removed, making the former calculation incorrect obligation.withdraw(withdraw_amount, collateral_index)?; obligation.last_update.mark_stale(); + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; spl_token_transfer(TokenTransferParams { @@ -2249,6 +2269,7 @@ fn process_withdraw_obligation_collateral_and_redeem_reserve_liquidity( clock, token_program_id, true, + &accounts[12..], )?; _redeem_reserve_collateral( diff --git a/token-lending/program/tests/attributed_borrows.rs b/token-lending/program/tests/attributed_borrows.rs index c250c76fce3..dd2ebfa3a2c 100644 --- a/token-lending/program/tests/attributed_borrows.rs +++ b/token-lending/program/tests/attributed_borrows.rs @@ -1,5 +1,6 @@ #![cfg(feature = "test-bpf")] +use solend_program::math::TryDiv; use crate::solend_program_test::custom_scenario; use solana_sdk::instruction::InstructionError; @@ -398,3 +399,197 @@ async fn test_calculations() { // test.advance_clock_by_slots(1).await; // } + +#[tokio::test] +async fn test_withdraw() { + let (mut test, lending_market, reserves, obligations, users, lending_market_owner) = + custom_scenario( + &[ + ReserveArgs { + mint: usdc_mint::id(), + config: ReserveConfig { + loan_to_value_ratio: 80, + liquidation_threshold: 81, + max_liquidation_threshold: 82, + fees: ReserveFees { + host_fee_percentage: 0, + ..ReserveFees::default() + }, + optimal_borrow_rate: 0, + max_borrow_rate: 0, + ..test_reserve_config() + }, + liquidity_amount: 100_000 * FRACTIONAL_TO_USDC, + price: PriceArgs { + price: 10, + conf: 0, + expo: -1, + ema_price: 10, + ema_conf: 1, + }, + }, + ReserveArgs { + mint: wsol_mint::id(), + config: ReserveConfig { + loan_to_value_ratio: 80, + liquidation_threshold: 81, + max_liquidation_threshold: 82, + fees: ReserveFees { + host_fee_percentage: 0, + ..ReserveFees::default() + }, + optimal_borrow_rate: 0, + max_borrow_rate: 0, + ..test_reserve_config() + }, + liquidity_amount: 100 * LAMPORTS_PER_SOL, + price: PriceArgs { + price: 10, + conf: 0, + expo: 0, + ema_price: 10, + ema_conf: 0, + }, + }, + ], + &[ObligationArgs { + deposits: vec![ + (usdc_mint::id(), 30 * FRACTIONAL_TO_USDC), + (wsol_mint::id(), 2 * LAMPORTS_PER_SOL), + ], + borrows: vec![(usdc_mint::id(), 10 * FRACTIONAL_TO_USDC)], + }], + ) + .await; + + // usd borrow attribution is currently $6 + + // change borrow attribution limit + lending_market + .update_reserve_config( + &mut test, + &lending_market_owner, + &reserves[0], + ReserveConfig { + attributed_borrow_limit: 6, + ..reserves[0].account.config + }, + reserves[0].account.rate_limiter.config, + None, + ) + .await + .unwrap(); + + // attempt to withdraw 1 sol from obligation 0, this should fail + let err = lending_market + .withdraw_obligation_collateral( + &mut test, + &reserves[1], + &obligations[0], + &users[0], + LAMPORTS_PER_SOL, + ) + .await + .unwrap_err() + .unwrap(); + + assert_eq!( + err, + TransactionError::InstructionError( + 1, + InstructionError::Custom(LendingError::BorrowAttributionLimitExceeded as u32) + ) + ); + + // change borrow attribution limit so that the borrow will succeed + lending_market + .update_reserve_config( + &mut test, + &lending_market_owner, + &reserves[0], + ReserveConfig { + attributed_borrow_limit: 10, + ..reserves[0].account.config + }, + reserves[0].account.rate_limiter.config, + None, + ) + .await + .unwrap(); + + test.advance_clock_by_slots(1).await; + + // attempt to withdraw 1 sol from obligation 0, this should pass now + lending_market + .withdraw_obligation_collateral( + &mut test, + &reserves[1], + &obligations[0], + &users[0], + LAMPORTS_PER_SOL, + ) + .await + .unwrap(); + + // check reserve and obligation borrow attribution values + { + let usdc_reserve_post = test.load_account::(reserves[0].pubkey).await; + assert_eq!( + usdc_reserve_post.account.attributed_borrow_value, + Decimal::from(7500u64).try_div(Decimal::from(1000u64)).unwrap() + ); + + let wsol_reserve_post = test.load_account::(reserves[1].pubkey).await; + assert_eq!( + wsol_reserve_post.account.attributed_borrow_value, + Decimal::from_percent(250) + ); + + let obligation_post = test.load_account::(obligations[0].pubkey).await; + assert_eq!( + obligation_post.account.deposits[0].attributed_borrow_value, + Decimal::from(7500u64).try_div(Decimal::from(1000u64)).unwrap() + ); + assert_eq!( + obligation_post.account.deposits[1].attributed_borrow_value, + Decimal::from(2500u64).try_div(Decimal::from(1000u64)).unwrap() + ); + } + + test.advance_clock_by_slots(1).await; + + // withdraw the rest + lending_market + .withdraw_obligation_collateral( + &mut test, + &reserves[1], + &obligations[0], + &users[0], + LAMPORTS_PER_SOL, + ) + .await + .unwrap(); + + test.advance_clock_by_slots(1).await; + + // check reserve and obligation borrow attribution values + { + let usdc_reserve_post = test.load_account::(reserves[0].pubkey).await; + assert_eq!( + usdc_reserve_post.account.attributed_borrow_value, + Decimal::from(10u64) + ); + + let wsol_reserve_post = test.load_account::(reserves[1].pubkey).await; + assert_eq!( + wsol_reserve_post.account.attributed_borrow_value, + Decimal::zero() + ); + + let obligation_post = test.load_account::(obligations[0].pubkey).await; + assert_eq!( + obligation_post.account.deposits[0].attributed_borrow_value, + Decimal::from(10u64) + ); + } +} diff --git a/token-lending/program/tests/borrow_weight.rs b/token-lending/program/tests/borrow_weight.rs index 723e5792dde..1f8c306a052 100644 --- a/token-lending/program/tests/borrow_weight.rs +++ b/token-lending/program/tests/borrow_weight.rs @@ -176,6 +176,8 @@ async fn test_borrow() { test.advance_clock_by_slots(1).await; + let obligation = test.load_account::(obligation.pubkey).await; + // max withdraw { let balance_checker = BalanceChecker::start(&mut test, &[&user]).await; diff --git a/token-lending/program/tests/helpers/solend_program_test.rs b/token-lending/program/tests/helpers/solend_program_test.rs index f8a2083d1dd..bb8c2c94485 100644 --- a/token-lending/program/tests/helpers/solend_program_test.rs +++ b/token-lending/program/tests/helpers/solend_program_test.rs @@ -1168,7 +1168,7 @@ impl Info { test.process_transaction( &[ - ComputeBudgetInstruction::set_compute_unit_limit(70_000), + ComputeBudgetInstruction::set_compute_unit_limit(100_000), withdraw_obligation_collateral_and_redeem_reserve_collateral( solend_program::id(), collateral_amount, @@ -1184,6 +1184,12 @@ impl Info { withdraw_reserve.account.liquidity.supply_pubkey, user.keypair.pubkey(), user.keypair.pubkey(), + obligation + .account + .deposits + .iter() + .map(|d| d.deposit_reserve) + .collect(), ), ], Some(&[&user.keypair]), @@ -1206,7 +1212,7 @@ impl Info { test.process_transaction( &[ - ComputeBudgetInstruction::set_compute_unit_limit(40_000), + ComputeBudgetInstruction::set_compute_unit_limit(100_000), withdraw_obligation_collateral( solend_program::id(), collateral_amount, @@ -1217,6 +1223,12 @@ impl Info { obligation.pubkey, self.pubkey, user.keypair.pubkey(), + obligation + .account + .deposits + .iter() + .map(|d| d.deposit_reserve) + .collect(), ), ], Some(&[&user.keypair]), diff --git a/token-lending/program/tests/withdraw_obligation_collateral.rs b/token-lending/program/tests/withdraw_obligation_collateral.rs index 679875fdb09..95214774134 100644 --- a/token-lending/program/tests/withdraw_obligation_collateral.rs +++ b/token-lending/program/tests/withdraw_obligation_collateral.rs @@ -1,7 +1,9 @@ #![cfg(feature = "test-bpf")] +use solend_program::math::TrySub; mod helpers; +use solend_sdk::math::Decimal; use crate::solend_program_test::scenario_1; use helpers::solend_program_test::{BalanceChecker, TokenBalanceChange}; use helpers::*; @@ -58,9 +60,11 @@ async fn test_success_withdraw_fixed_amount() { deposits: [ObligationCollateral { deposit_reserve: usdc_reserve.pubkey, deposited_amount: 100_000_000_000 - 1_000_000, + market_value: Decimal::from(99_999u64), ..obligation.account.deposits[0] }] .to_vec(), + deposited_value: Decimal::from(99_999u64), ..obligation.account } ); @@ -121,9 +125,11 @@ async fn test_success_withdraw_max() { deposits: [ObligationCollateral { deposit_reserve: usdc_reserve.pubkey, deposited_amount: expected_remaining_collateral, + market_value: Decimal::from(200u64), ..obligation.account.deposits[0] }] .to_vec(), + deposited_value: Decimal::from(200u64), ..obligation.account } ); diff --git a/token-lending/program/tests/withdraw_obligation_collateral_and_redeem_reserve_collateral.rs b/token-lending/program/tests/withdraw_obligation_collateral_and_redeem_reserve_collateral.rs index 123e73f5105..8ce586d8b14 100644 --- a/token-lending/program/tests/withdraw_obligation_collateral_and_redeem_reserve_collateral.rs +++ b/token-lending/program/tests/withdraw_obligation_collateral_and_redeem_reserve_collateral.rs @@ -141,9 +141,11 @@ async fn test_success() { deposits: [ObligationCollateral { deposit_reserve: usdc_reserve.pubkey, deposited_amount: 200 * FRACTIONAL_TO_USDC, + market_value: Decimal::from(200u64), ..obligation.account.deposits[0] }] .to_vec(), + deposited_value: Decimal::from(200u64), ..obligation.account } ); diff --git a/token-lending/sdk/src/instruction.rs b/token-lending/sdk/src/instruction.rs index bd41d4fa590..9ca0407b8f9 100644 --- a/token-lending/sdk/src/instruction.rs +++ b/token-lending/sdk/src/instruction.rs @@ -1312,27 +1312,37 @@ pub fn withdraw_obligation_collateral_and_redeem_reserve_collateral( reserve_liquidity_supply_pubkey: Pubkey, obligation_owner_pubkey: Pubkey, user_transfer_authority_pubkey: Pubkey, + collateral_reserves: Vec, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id, ); + + let mut accounts = vec![ + AccountMeta::new(source_collateral_pubkey, false), + AccountMeta::new(destination_collateral_pubkey, false), + AccountMeta::new(withdraw_reserve_pubkey, false), + AccountMeta::new(obligation_pubkey, false), + AccountMeta::new(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new(destination_liquidity_pubkey, false), + AccountMeta::new(reserve_collateral_mint_pubkey, false), + AccountMeta::new(reserve_liquidity_supply_pubkey, false), + AccountMeta::new_readonly(obligation_owner_pubkey, true), + AccountMeta::new_readonly(user_transfer_authority_pubkey, true), + AccountMeta::new_readonly(spl_token::id(), false), + ]; + + accounts.extend( + collateral_reserves + .into_iter() + .map(|pubkey| AccountMeta::new(pubkey, false)), + ); + Instruction { program_id, - accounts: vec![ - AccountMeta::new(source_collateral_pubkey, false), - AccountMeta::new(destination_collateral_pubkey, false), - AccountMeta::new(withdraw_reserve_pubkey, false), - AccountMeta::new(obligation_pubkey, false), - AccountMeta::new(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new(destination_liquidity_pubkey, false), - AccountMeta::new(reserve_collateral_mint_pubkey, false), - AccountMeta::new(reserve_liquidity_supply_pubkey, false), - AccountMeta::new_readonly(obligation_owner_pubkey, true), - AccountMeta::new_readonly(user_transfer_authority_pubkey, true), - AccountMeta::new_readonly(spl_token::id(), false), - ], + accounts, data: LendingInstruction::WithdrawObligationCollateralAndRedeemReserveCollateral { collateral_amount, } @@ -1351,23 +1361,33 @@ pub fn withdraw_obligation_collateral( obligation_pubkey: Pubkey, lending_market_pubkey: Pubkey, obligation_owner_pubkey: Pubkey, + collateral_reserves: Vec ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], &program_id, ); + + let mut accounts = vec![ + AccountMeta::new(source_collateral_pubkey, false), + AccountMeta::new(destination_collateral_pubkey, false), + AccountMeta::new_readonly(withdraw_reserve_pubkey, false), + AccountMeta::new(obligation_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(lending_market_authority_pubkey, false), + AccountMeta::new_readonly(obligation_owner_pubkey, true), + AccountMeta::new_readonly(spl_token::id(), false), + ]; + + accounts.extend( + collateral_reserves + .into_iter() + .map(|pubkey| AccountMeta::new(pubkey, false)), + ); + Instruction { program_id, - accounts: vec![ - AccountMeta::new(source_collateral_pubkey, false), - AccountMeta::new(destination_collateral_pubkey, false), - AccountMeta::new_readonly(withdraw_reserve_pubkey, false), - AccountMeta::new(obligation_pubkey, false), - AccountMeta::new_readonly(lending_market_pubkey, false), - AccountMeta::new_readonly(lending_market_authority_pubkey, false), - AccountMeta::new_readonly(obligation_owner_pubkey, true), - AccountMeta::new_readonly(spl_token::id(), false), - ], + accounts, data: LendingInstruction::WithdrawObligationCollateral { collateral_amount }.pack(), } }