Skip to content

Commit

Permalink
block on withdraw if borrow attribution limits are exceeded
Browse files Browse the repository at this point in the history
  • Loading branch information
0xripleys committed Nov 22, 2023
1 parent a4e6ebd commit ec3276f
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 26 deletions.
21 changes: 21 additions & 0 deletions token-lending/program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1390,6 +1390,7 @@ fn process_withdraw_obligation_collateral(
clock,
token_program_id,
false,
&accounts[8..],
)?;
Ok(())
}
Expand All @@ -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<u64, ProgramError> {
let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?;
if lending_market_info.owner != program_id {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -2249,6 +2269,7 @@ fn process_withdraw_obligation_collateral_and_redeem_reserve_liquidity(
clock,
token_program_id,
true,
&accounts[12..],
)?;

_redeem_reserve_collateral(
Expand Down
195 changes: 195 additions & 0 deletions token-lending/program/tests/attributed_borrows.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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::<Reserve>(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::<Reserve>(reserves[1].pubkey).await;
assert_eq!(
wsol_reserve_post.account.attributed_borrow_value,
Decimal::from_percent(250)
);

let obligation_post = test.load_account::<Obligation>(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::<Reserve>(reserves[0].pubkey).await;
assert_eq!(
usdc_reserve_post.account.attributed_borrow_value,
Decimal::from(10u64)
);

let wsol_reserve_post = test.load_account::<Reserve>(reserves[1].pubkey).await;
assert_eq!(
wsol_reserve_post.account.attributed_borrow_value,
Decimal::zero()
);

let obligation_post = test.load_account::<Obligation>(obligations[0].pubkey).await;
assert_eq!(
obligation_post.account.deposits[0].attributed_borrow_value,
Decimal::from(10u64)
);
}
}
2 changes: 2 additions & 0 deletions token-lending/program/tests/borrow_weight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ async fn test_borrow() {

test.advance_clock_by_slots(1).await;

let obligation = test.load_account::<Obligation>(obligation.pubkey).await;

// max withdraw
{
let balance_checker = BalanceChecker::start(&mut test, &[&user]).await;
Expand Down
16 changes: 14 additions & 2 deletions token-lending/program/tests/helpers/solend_program_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1168,7 +1168,7 @@ impl Info<LendingMarket> {

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,
Expand All @@ -1184,6 +1184,12 @@ impl Info<LendingMarket> {
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]),
Expand All @@ -1206,7 +1212,7 @@ impl Info<LendingMarket> {

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,
Expand All @@ -1217,6 +1223,12 @@ impl Info<LendingMarket> {
obligation.pubkey,
self.pubkey,
user.keypair.pubkey(),
obligation
.account
.deposits
.iter()
.map(|d| d.deposit_reserve)
.collect(),
),
],
Some(&[&user.keypair]),
Expand Down
6 changes: 6 additions & 0 deletions token-lending/program/tests/withdraw_obligation_collateral.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand Down Expand Up @@ -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
}
);
Expand Down Expand Up @@ -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
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
);
Expand Down
Loading

0 comments on commit ec3276f

Please sign in to comment.