From 06b2cc27ab8c58cb89edd5ebdbce56d5f2ccf7cc Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Mon, 18 Sep 2023 13:20:35 +0800 Subject: [PATCH 01/25] wip --- token-lending/program/src/processor.rs | 33 ++++++++++++++++++- .../tests/deposit_obligation_collateral.rs | 3 +- ...rve_liquidity_and_obligation_collateral.rs | 3 +- token-lending/program/tests/init_reserve.rs | 3 +- ...uidate_obligation_and_redeem_collateral.rs | 4 ++- token-lending/sdk/src/state/obligation.rs | 14 +++++--- token-lending/sdk/src/state/reserve.rs | 18 ++++++++-- 7 files changed, 66 insertions(+), 12 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 9eb5549bd68..c372d06f5ae 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -998,12 +998,15 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> } let mut deposited_value = Decimal::zero(); - let mut borrowed_value = Decimal::zero(); + let mut borrowed_value = Decimal::zero(); // weighted borrow value wrt borrow weights let mut borrowed_value_upper_bound = Decimal::zero(); let mut allowed_borrow_value = Decimal::zero(); let mut unhealthy_borrow_value = Decimal::zero(); let mut super_unhealthy_borrow_value = Decimal::zero(); + let mut deposit_reserve_infos = vec![]; + let mut true_borrow_value = Decimal::zero(); + for (index, collateral) in obligation.deposits.iter_mut().enumerate() { let deposit_reserve_info = next_account_info(account_info_iter)?; if deposit_reserve_info.owner != program_id { @@ -1052,6 +1055,8 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> unhealthy_borrow_value.try_add(market_value.try_mul(liquidation_threshold_rate)?)?; super_unhealthy_borrow_value = super_unhealthy_borrow_value .try_add(market_value.try_mul(max_liquidation_threshold_rate)?)?; + + deposit_reserve_infos.push(deposit_reserve_info); } let mut borrowing_isolated_asset = false; @@ -1120,6 +1125,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> borrowed_value.try_add(market_value.try_mul(borrow_reserve.borrow_weight())?)?; borrowed_value_upper_bound = borrowed_value_upper_bound .try_add(market_value_upper_bound.try_mul(borrow_reserve.borrow_weight())?)?; + true_borrow_value = true_borrow_value.try_add(market_value)?; } if account_info_iter.peek().is_some() { @@ -1142,6 +1148,31 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> obligation.last_update.update_slot(clock.slot); + // attributed borrow calculation + for (index, collateral) in obligation.deposits.iter_mut().enumerate() { + let deposit_reserve_info = &deposit_reserve_infos[index]; + let mut deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; + + // sanity check + if collateral.deposit_reserve != *deposit_reserve_info.key { + msg!("Something went wrong, deposit reserve account mismatch"); + return Err(LendingError::InvalidAccountInput.into()); + } + + // maybe need to do a saturating sub here in case there are precision issues + deposit_reserve.attributed_borrow_value = deposit_reserve + .attributed_borrow_value + .try_sub(collateral.attributed_borrow_value)?; + + collateral.attributed_borrow_value = collateral.market_value.try_div(obligation.deposited_value)?; + + deposit_reserve.attributed_borrow_value = deposit_reserve + .attributed_borrow_value + .try_add(collateral.attributed_borrow_value)?; + + Reserve::pack(deposit_reserve, &mut deposit_reserve_info.data.borrow_mut())?; + } + // move the ObligationLiquidity with the max borrow weight to the front if let Some((_, max_borrow_weight_index)) = max_borrow_weight { obligation.borrows.swap(0, max_borrow_weight_index); diff --git a/token-lending/program/tests/deposit_obligation_collateral.rs b/token-lending/program/tests/deposit_obligation_collateral.rs index 8f318fc4c00..c0879d9abe2 100644 --- a/token-lending/program/tests/deposit_obligation_collateral.rs +++ b/token-lending/program/tests/deposit_obligation_collateral.rs @@ -91,7 +91,8 @@ async fn test_success() { deposits: vec![ObligationCollateral { deposit_reserve: usdc_reserve.pubkey, deposited_amount: 1_000_000, - market_value: Decimal::zero() // this field only gets updated on a refresh + market_value: Decimal::zero(), // this field only gets updated on a refresh + attributed_borrow_value: Decimal::zero() }], ..obligation.account } diff --git a/token-lending/program/tests/deposit_reserve_liquidity_and_obligation_collateral.rs b/token-lending/program/tests/deposit_reserve_liquidity_and_obligation_collateral.rs index 87bd779b5c5..579d80b3d56 100644 --- a/token-lending/program/tests/deposit_reserve_liquidity_and_obligation_collateral.rs +++ b/token-lending/program/tests/deposit_reserve_liquidity_and_obligation_collateral.rs @@ -130,7 +130,8 @@ async fn test_success() { deposits: [ObligationCollateral { deposit_reserve: usdc_reserve.pubkey, deposited_amount: 1_000_000, - market_value: Decimal::zero() + market_value: Decimal::zero(), + attributed_borrow_value: Decimal::zero() }] .to_vec(), ..obligation.account diff --git a/token-lending/program/tests/init_reserve.rs b/token-lending/program/tests/init_reserve.rs index fc3754d3b76..a66f8d59209 100644 --- a/token-lending/program/tests/init_reserve.rs +++ b/token-lending/program/tests/init_reserve.rs @@ -180,7 +180,8 @@ async fn test_success() { supply_pubkey: reserve_collateral_supply_pubkey, }, config: reserve_config, - rate_limiter: RateLimiter::new(RateLimiterConfig::default(), 1001) + rate_limiter: RateLimiter::new(RateLimiterConfig::default(), 1001), + attributed_borrow_value: Decimal::zero(), } ); } diff --git a/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs b/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs index 7e749fbccbd..ea8e1b50e23 100644 --- a/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs +++ b/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs @@ -214,7 +214,9 @@ async fn test_success_new() { deposits: [ObligationCollateral { deposit_reserve: usdc_reserve.pubkey, deposited_amount: (100_000 - expected_usdc_withdrawn) * FRACTIONAL_TO_USDC, - market_value: Decimal::from(100_000u64) // old value + market_value: Decimal::from(100_000u64), // old value + attributed_borrow_value: obligation_post.account.deposits[0] + .attributed_borrow_value, // don't care about verifying this here }] .to_vec(), borrows: [ObligationLiquidity { diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs index 40ce4d9406f..edba4563a15 100644 --- a/token-lending/sdk/src/state/obligation.rs +++ b/token-lending/sdk/src/state/obligation.rs @@ -314,6 +314,8 @@ pub struct ObligationCollateral { pub deposited_amount: u64, /// Collateral market value in quote currency pub market_value: Decimal, + /// How much borrow is attributed to this collateral (USD) + pub attributed_borrow_value: Decimal } impl ObligationCollateral { @@ -323,6 +325,7 @@ impl ObligationCollateral { deposit_reserve, deposited_amount: 0, market_value: Decimal::zero(), + attributed_borrow_value: Decimal::zero(), } } @@ -478,11 +481,12 @@ impl Pack for Obligation { for collateral in &self.deposits { let deposits_flat = array_mut_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN]; #[allow(clippy::ptr_offset_with_cast)] - let (deposit_reserve, deposited_amount, market_value, _padding_deposit) = - mut_array_refs![deposits_flat, PUBKEY_BYTES, 8, 16, 32]; + let (deposit_reserve, deposited_amount, market_value, attributed_borrow_value, _padding_deposit) = + mut_array_refs![deposits_flat, PUBKEY_BYTES, 8, 16, 16, 16]; deposit_reserve.copy_from_slice(collateral.deposit_reserve.as_ref()); *deposited_amount = collateral.deposited_amount.to_le_bytes(); pack_decimal(collateral.market_value, market_value); + pack_decimal(collateral.attributed_borrow_value, attributed_borrow_value); offset += OBLIGATION_COLLATERAL_LEN; } @@ -564,12 +568,13 @@ impl Pack for Obligation { for _ in 0..deposits_len { let deposits_flat = array_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN]; #[allow(clippy::ptr_offset_with_cast)] - let (deposit_reserve, deposited_amount, market_value, _padding_deposit) = - array_refs![deposits_flat, PUBKEY_BYTES, 8, 16, 32]; + let (deposit_reserve, deposited_amount, market_value, attributed_borrow_value, _padding_deposit) = + array_refs![deposits_flat, PUBKEY_BYTES, 8, 16, 16, 16]; deposits.push(ObligationCollateral { deposit_reserve: Pubkey::new(deposit_reserve), deposited_amount: u64::from_le_bytes(*deposited_amount), market_value: unpack_decimal(market_value), + attributed_borrow_value: unpack_decimal(attributed_borrow_value), }); offset += OBLIGATION_COLLATERAL_LEN; } @@ -643,6 +648,7 @@ mod test { deposit_reserve: Pubkey::new_unique(), deposited_amount: rng.gen(), market_value: rand_decimal(), + attributed_borrow_value: rand_decimal(), }], borrows: vec![ObligationLiquidity { borrow_reserve: Pubkey::new_unique(), diff --git a/token-lending/sdk/src/state/reserve.rs b/token-lending/sdk/src/state/reserve.rs index 3fdd13c3672..2c650c7109f 100644 --- a/token-lending/sdk/src/state/reserve.rs +++ b/token-lending/sdk/src/state/reserve.rs @@ -58,6 +58,8 @@ pub struct Reserve { pub config: ReserveConfig, /// Outflow Rate Limiter (denominated in tokens) pub rate_limiter: RateLimiter, + /// Attributed borrows in USD + pub attributed_borrow_value: Decimal, } impl Reserve { @@ -77,6 +79,7 @@ impl Reserve { self.collateral = params.collateral; self.config = params.config; self.rate_limiter = RateLimiter::new(params.rate_limiter_config, params.current_slot); + self.attributed_borrow_value = Decimal::zero(); } /// get borrow weight. Guaranteed to be greater than 1 @@ -1229,6 +1232,7 @@ impl Pack for Reserve { config_extra_oracle_pubkey, liquidity_extra_market_price_flag, liquidity_extra_market_price, + attributed_borrow_value, _padding, ) = mut_array_refs![ output, @@ -1276,7 +1280,8 @@ impl Pack for Reserve { 32, 1, 16, - 81 + 16, + 65 ]; // reserve @@ -1356,6 +1361,8 @@ impl Pack for Reserve { *config_added_borrow_weight_bps = self.config.added_borrow_weight_bps.to_le_bytes(); *config_max_liquidation_bonus = self.config.max_liquidation_bonus.to_le_bytes(); *config_max_liquidation_threshold = self.config.max_liquidation_threshold.to_le_bytes(); + + pack_decimal(self.attributed_borrow_value, attributed_borrow_value); } /// Unpacks a byte buffer into a [ReserveInfo](struct.ReserveInfo.html). @@ -1407,6 +1414,7 @@ impl Pack for Reserve { config_extra_oracle_pubkey, liquidity_extra_market_price_flag, liquidity_extra_market_price, + attributed_borrow_value, _padding, ) = array_refs![ input, @@ -1454,7 +1462,8 @@ impl Pack for Reserve { 32, 1, 16, - 81 + 16, + 65 ]; let version = u8::from_le_bytes(*version); @@ -1561,6 +1570,7 @@ impl Pack for Reserve { }, }, rate_limiter: RateLimiter::unpack_from_slice(rate_limiter)?, + attributed_borrow_value: unpack_decimal(attributed_borrow_value), }) } } @@ -1651,6 +1661,7 @@ mod test { extra_oracle_pubkey, }, rate_limiter: rand_rate_limiter(), + attributed_borrow_value: rand_decimal(), }; let mut packed = [0u8; Reserve::LEN]; @@ -2559,7 +2570,8 @@ mod test { deposits: vec![ObligationCollateral { deposit_reserve: Pubkey::new_unique(), deposited_amount: test_case.deposit_amount, - market_value: test_case.deposit_market_value + market_value: test_case.deposit_market_value, + attributed_borrow_value: test_case.borrow_market_value, }], borrows: vec![ObligationLiquidity { borrow_reserve: Pubkey::new_unique(), From 3d3d94f938afdd746d74af188ce416b9138d3450 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Tue, 19 Sep 2023 14:43:36 +0800 Subject: [PATCH 02/25] inlining kind of worked --- token-lending/program/src/processor.rs | 27 +++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index c372d06f5ae..06f4182e744 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -983,6 +983,7 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro Ok(()) } +#[inline(never)] // avoid stack frame limit fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter().peekable(); let obligation_info = next_account_info(account_info_iter)?; @@ -1004,9 +1005,19 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> let mut unhealthy_borrow_value = Decimal::zero(); let mut super_unhealthy_borrow_value = Decimal::zero(); - let mut deposit_reserve_infos = vec![]; let mut true_borrow_value = Decimal::zero(); + let mut arr = [0u8; 206]; + for i in 0..arr.len() { + arr[i] = (i % 256) as u8; + } + let mut s = 0; + for i in 0..arr.len() { + s += arr[i]; + } + msg!("s: {}", s); + + for (index, collateral) in obligation.deposits.iter_mut().enumerate() { let deposit_reserve_info = next_account_info(account_info_iter)?; if deposit_reserve_info.owner != program_id { @@ -1055,8 +1066,6 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> unhealthy_borrow_value.try_add(market_value.try_mul(liquidation_threshold_rate)?)?; super_unhealthy_borrow_value = super_unhealthy_borrow_value .try_add(market_value.try_mul(max_liquidation_threshold_rate)?)?; - - deposit_reserve_infos.push(deposit_reserve_info); } let mut borrowing_isolated_asset = false; @@ -1148,9 +1157,10 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> obligation.last_update.update_slot(clock.slot); + let deposit_infos = &mut accounts.iter().skip(1); // attributed borrow calculation for (index, collateral) in obligation.deposits.iter_mut().enumerate() { - let deposit_reserve_info = &deposit_reserve_infos[index]; + let deposit_reserve_info = next_account_info(deposit_infos)?; let mut deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; // sanity check @@ -1164,7 +1174,14 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> .attributed_borrow_value .try_sub(collateral.attributed_borrow_value)?; - collateral.attributed_borrow_value = collateral.market_value.try_div(obligation.deposited_value)?; + if obligation.deposited_value > Decimal::zero() { + collateral.attributed_borrow_value = collateral + .market_value + .try_mul(obligation.borrowed_value)? + .try_div(obligation.deposited_value)?; + } else { + collateral.attributed_borrow_value = Decimal::zero(); + } deposit_reserve.attributed_borrow_value = deposit_reserve .attributed_borrow_value From 0fc5ca5a215db3a361b98a2900cd2cd342f5c6c8 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Wed, 20 Sep 2023 15:44:22 +0800 Subject: [PATCH 03/25] minor progress --- token-lending/program/src/processor.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 06f4182e744..cc248c2edec 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1007,17 +1007,6 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> let mut true_borrow_value = Decimal::zero(); - let mut arr = [0u8; 206]; - for i in 0..arr.len() { - arr[i] = (i % 256) as u8; - } - let mut s = 0; - for i in 0..arr.len() { - s += arr[i]; - } - msg!("s: {}", s); - - for (index, collateral) in obligation.deposits.iter_mut().enumerate() { let deposit_reserve_info = next_account_info(account_info_iter)?; if deposit_reserve_info.owner != program_id { @@ -1172,13 +1161,21 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> // maybe need to do a saturating sub here in case there are precision issues deposit_reserve.attributed_borrow_value = deposit_reserve .attributed_borrow_value - .try_sub(collateral.attributed_borrow_value)?; + .try_sub(collateral.attributed_borrow_value) + .map_err(|e| { + msg!("sub failed"); + e + })?; if obligation.deposited_value > Decimal::zero() { collateral.attributed_borrow_value = collateral .market_value .try_mul(obligation.borrowed_value)? - .try_div(obligation.deposited_value)?; + .try_div(obligation.deposited_value) + .map_err(|e| { + msg!("div failed"); + e + })?; } else { collateral.attributed_borrow_value = Decimal::zero(); } From dcfcec8bb369565a92eb9d56f267691937def5a7 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Wed, 15 Nov 2023 11:39:41 -0500 Subject: [PATCH 04/25] saturating sub, add attributed_borrow_limit parameter --- token-lending/program/src/processor.rs | 12 ++++-------- token-lending/sdk/src/instruction.rs | 15 ++++++++++++--- token-lending/sdk/src/math/common.rs | 6 ++++++ token-lending/sdk/src/math/decimal.rs | 6 ++++++ token-lending/sdk/src/state/reserve.rs | 13 +++++++++++-- 5 files changed, 39 insertions(+), 13 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index cc248c2edec..c0c2a1a5201 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -36,6 +36,7 @@ use solend_sdk::{ oracles::{ get_oracle_type, get_pyth_price_unchecked, validate_pyth_price_account_info, OracleType, }, + math::SaturatingSub, state::{LendingMarketMetadata, RateLimiter, RateLimiterConfig, ReserveType}, }; use solend_sdk::{switchboard_v2_devnet, switchboard_v2_mainnet}; @@ -1005,8 +1006,6 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> let mut unhealthy_borrow_value = Decimal::zero(); let mut super_unhealthy_borrow_value = Decimal::zero(); - let mut true_borrow_value = Decimal::zero(); - for (index, collateral) in obligation.deposits.iter_mut().enumerate() { let deposit_reserve_info = next_account_info(account_info_iter)?; if deposit_reserve_info.owner != program_id { @@ -1059,6 +1058,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> let mut borrowing_isolated_asset = false; let mut max_borrow_weight = None; + let mut true_borrow_value = Decimal::zero(); for (index, liquidity) in obligation.borrows.iter_mut().enumerate() { let borrow_reserve_info = next_account_info(account_info_iter)?; if borrow_reserve_info.owner != program_id { @@ -1147,6 +1147,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> obligation.last_update.update_slot(clock.slot); let deposit_infos = &mut accounts.iter().skip(1); + // attributed borrow calculation for (index, collateral) in obligation.deposits.iter_mut().enumerate() { let deposit_reserve_info = next_account_info(deposit_infos)?; @@ -1158,14 +1159,9 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> return Err(LendingError::InvalidAccountInput.into()); } - // maybe need to do a saturating sub here in case there are precision issues deposit_reserve.attributed_borrow_value = deposit_reserve .attributed_borrow_value - .try_sub(collateral.attributed_borrow_value) - .map_err(|e| { - msg!("sub failed"); - e - })?; + .saturating_sub(collateral.attributed_borrow_value); if obligation.deposited_value > Decimal::zero() { collateral.attributed_borrow_value = collateral diff --git a/token-lending/sdk/src/instruction.rs b/token-lending/sdk/src/instruction.rs index d4e7193503b..2a80656c3b8 100644 --- a/token-lending/sdk/src/instruction.rs +++ b/token-lending/sdk/src/instruction.rs @@ -168,7 +168,7 @@ pub enum LendingInstruction { /// /// 0. `[writable]` Obligation account. /// 1. `[]` Clock sysvar (optional, will be removed soon). - /// .. `[]` Collateral deposit reserve accounts - refreshed, all, in order. + /// .. `[writable]` Collateral deposit reserve accounts - refreshed, all, in order. /// .. `[]` Liquidity borrow reserve accounts - refreshed, all, in order. RefreshObligation, @@ -563,7 +563,7 @@ impl LendingInstruction { let (max_liquidation_bonus, rest) = Self::unpack_u8(rest)?; let (max_liquidation_threshold, rest) = Self::unpack_u8(rest)?; let (scaled_price_offset_bps, rest) = Self::unpack_i64(rest)?; - let (extra_oracle_pubkey, _rest) = match Self::unpack_u8(rest)? { + let (extra_oracle_pubkey, rest) = match Self::unpack_u8(rest)? { (0, rest) => (None, rest), (1, rest) => { let (pubkey, rest) = Self::unpack_pubkey(rest)?; @@ -571,6 +571,7 @@ impl LendingInstruction { } _ => return Err(LendingError::InstructionUnpackError.into()), }; + let (attributed_borrow_limit, _rest) = Self::unpack_u64(rest)?; Self::InitReserve { liquidity_amount, config: ReserveConfig { @@ -599,6 +600,7 @@ impl LendingInstruction { reserve_type: ReserveType::from_u8(asset_type).unwrap(), scaled_price_offset_bps, extra_oracle_pubkey, + attributed_borrow_limit, }, } } @@ -676,6 +678,7 @@ impl LendingInstruction { } _ => return Err(LendingError::InstructionUnpackError.into()), }; + let (attributed_borrow_limit, rest) = Self::unpack_u64(rest)?; let (window_duration, rest) = Self::unpack_u64(rest)?; let (max_outflow, _rest) = Self::unpack_u64(rest)?; @@ -706,6 +709,7 @@ impl LendingInstruction { reserve_type: ReserveType::from_u8(asset_type).unwrap(), scaled_price_offset_bps, extra_oracle_pubkey, + attributed_borrow_limit, }, rate_limiter_config: RateLimiterConfig { window_duration, @@ -871,6 +875,7 @@ impl LendingInstruction { reserve_type: asset_type, scaled_price_offset_bps, extra_oracle_pubkey, + attributed_borrow_limit, }, } => { buf.push(2); @@ -906,6 +911,7 @@ impl LendingInstruction { buf.push(0); } }; + buf.extend_from_slice(&attributed_borrow_limit.to_le_bytes()); } Self::RefreshReserve => { buf.push(3); @@ -992,6 +998,7 @@ impl LendingInstruction { buf.push(0); } }; + buf.extend_from_slice(&config.attributed_borrow_limit.to_le_bytes()); buf.extend_from_slice(&rate_limiter_config.window_duration.to_le_bytes()); buf.extend_from_slice(&rate_limiter_config.max_outflow.to_le_bytes()); } @@ -1261,7 +1268,7 @@ pub fn refresh_obligation( accounts.extend( reserve_pubkeys .into_iter() - .map(|pubkey| AccountMeta::new_readonly(pubkey, false)), + .map(|pubkey| AccountMeta::new(pubkey, false)), ); Instruction { program_id, @@ -1837,6 +1844,7 @@ mod test { } else { Some(Pubkey::new_unique()) }, + attributed_borrow_limit: rng.gen() }, }; @@ -2003,6 +2011,7 @@ mod test { } else { None }, + attributed_borrow_limit: rng.gen() }, rate_limiter_config: RateLimiterConfig { window_duration: rng.gen::(), diff --git a/token-lending/sdk/src/math/common.rs b/token-lending/sdk/src/math/common.rs index 081ee56b0f0..68925b55265 100644 --- a/token-lending/sdk/src/math/common.rs +++ b/token-lending/sdk/src/math/common.rs @@ -19,6 +19,12 @@ pub trait TrySub: Sized { fn try_sub(self, rhs: Self) -> Result; } +/// Subtract and set to zero on underflow +pub trait SaturatingSub: Sized { + /// Subtract + fn saturating_sub(self, rhs: Self) -> Self; +} + /// Try to subtract, return an error on overflow pub trait TryAdd: Sized { /// Add diff --git a/token-lending/sdk/src/math/decimal.rs b/token-lending/sdk/src/math/decimal.rs index d3b14996680..71bc11b70df 100644 --- a/token-lending/sdk/src/math/decimal.rs +++ b/token-lending/sdk/src/math/decimal.rs @@ -165,6 +165,12 @@ impl TrySub for Decimal { } } +impl SaturatingSub for Decimal { + fn saturating_sub(self, rhs: Self) -> Self { + Self(self.0.saturating_sub(rhs.0)) + } +} + impl TryDiv for Decimal { fn try_div(self, rhs: u64) -> Result { Ok(Self( diff --git a/token-lending/sdk/src/state/reserve.rs b/token-lending/sdk/src/state/reserve.rs index 2c650c7109f..2e922a5ffa2 100644 --- a/token-lending/sdk/src/state/reserve.rs +++ b/token-lending/sdk/src/state/reserve.rs @@ -943,6 +943,8 @@ pub struct ReserveConfig { pub scaled_price_offset_bps: i64, /// Extra oracle. Only used to limit borrows and withdrawals. pub extra_oracle_pubkey: Option, + /// Attributed Borrow limit in USD + pub attributed_borrow_limit: u64 } /// validates reserve configs @@ -1233,6 +1235,7 @@ impl Pack for Reserve { liquidity_extra_market_price_flag, liquidity_extra_market_price, attributed_borrow_value, + config_attributed_borrow_limit, _padding, ) = mut_array_refs![ output, @@ -1281,7 +1284,8 @@ impl Pack for Reserve { 1, 16, 16, - 65 + 8, + 57 ]; // reserve @@ -1361,6 +1365,7 @@ impl Pack for Reserve { *config_added_borrow_weight_bps = self.config.added_borrow_weight_bps.to_le_bytes(); *config_max_liquidation_bonus = self.config.max_liquidation_bonus.to_le_bytes(); *config_max_liquidation_threshold = self.config.max_liquidation_threshold.to_le_bytes(); + *config_attributed_borrow_limit = self.config.attributed_borrow_limit.to_le_bytes(); pack_decimal(self.attributed_borrow_value, attributed_borrow_value); } @@ -1415,6 +1420,7 @@ impl Pack for Reserve { liquidity_extra_market_price_flag, liquidity_extra_market_price, attributed_borrow_value, + config_attributed_borrow_limit, _padding, ) = array_refs![ input, @@ -1463,7 +1469,8 @@ impl Pack for Reserve { 1, 16, 16, - 65 + 8, + 57 ]; let version = u8::from_le_bytes(*version); @@ -1568,6 +1575,7 @@ impl Pack for Reserve { } else { Some(Pubkey::new_from_array(*config_extra_oracle_pubkey)) }, + attributed_borrow_limit: u64::from_le_bytes(*config_attributed_borrow_limit), }, rate_limiter: RateLimiter::unpack_from_slice(rate_limiter)?, attributed_borrow_value: unpack_decimal(attributed_borrow_value), @@ -1659,6 +1667,7 @@ mod test { reserve_type: ReserveType::from_u8(rng.gen::() % 2).unwrap(), scaled_price_offset_bps: rng.gen(), extra_oracle_pubkey, + attributed_borrow_limit: rng.gen(), }, rate_limiter: rand_rate_limiter(), attributed_borrow_value: rand_decimal(), From ec5798fee9704868a4969879b470380b15560685 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Wed, 15 Nov 2023 12:19:19 -0500 Subject: [PATCH 05/25] all tests passing --- token-lending/program/tests/helpers/mod.rs | 1 + .../tests/helpers/solend_program_test.rs | 5 +++ .../program/tests/isolated_tier_assets.rs | 5 +++ ...uidate_obligation_and_redeem_collateral.rs | 1 + .../program/tests/refresh_obligation.rs | 39 +++++++++++-------- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index 1f8ce836436..4eb662e9463 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -55,6 +55,7 @@ pub fn test_reserve_config() -> ReserveConfig { reserve_type: ReserveType::Regular, scaled_price_offset_bps: 0, extra_oracle_pubkey: None, + attributed_borrow_limit: u64::MAX, } } diff --git a/token-lending/program/tests/helpers/solend_program_test.rs b/token-lending/program/tests/helpers/solend_program_test.rs index 4c42488ee48..e3adf1b94ad 100644 --- a/token-lending/program/tests/helpers/solend_program_test.rs +++ b/token-lending/program/tests/helpers/solend_program_test.rs @@ -1651,6 +1651,11 @@ pub async fn scenario_1( .await .unwrap(); + lending_market + .refresh_reserve(&mut test, &usdc_reserve) + .await + .unwrap(); + // populate deposit value correctly. let obligation = test.load_account::(obligation.pubkey).await; lending_market diff --git a/token-lending/program/tests/isolated_tier_assets.rs b/token-lending/program/tests/isolated_tier_assets.rs index d2e5abef109..17575cf2b86 100644 --- a/token-lending/program/tests/isolated_tier_assets.rs +++ b/token-lending/program/tests/isolated_tier_assets.rs @@ -1,6 +1,7 @@ #![cfg(feature = "test-bpf")] use crate::solend_program_test::custom_scenario; +use solend_program::state::ObligationCollateral; use crate::solend_program_test::ObligationArgs; use crate::solend_program_test::PriceArgs; @@ -112,6 +113,10 @@ async fn test_refresh_obligation() { slot: 1001, stale: false }, + deposits: vec![ObligationCollateral { + attributed_borrow_value: Decimal::from(10u64), + ..obligations[0].account.deposits[0] + }], borrows: vec![ObligationLiquidity { borrow_reserve: wsol_reserve.pubkey, cumulative_borrow_rate_wads: Decimal::one(), diff --git a/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs b/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs index ea8e1b50e23..6145cc67fdd 100644 --- a/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs +++ b/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs @@ -178,6 +178,7 @@ async fn test_success_new() { - expected_usdc_withdrawn * FRACTIONAL_TO_USDC, ..usdc_reserve.account.collateral }, + attributed_borrow_value: Decimal::from(55000u64), ..usdc_reserve.account } ); diff --git a/token-lending/program/tests/refresh_obligation.rs b/token-lending/program/tests/refresh_obligation.rs index e040d65c5e0..7d89793c823 100644 --- a/token-lending/program/tests/refresh_obligation.rs +++ b/token-lending/program/tests/refresh_obligation.rs @@ -192,6 +192,23 @@ async fn test_success() { .await; assert_eq!(lending_market_post, lending_market); + // 1 + 0.3/SLOTS_PER_YEAR + let new_cumulative_borrow_rate = Decimal::one() + .try_add( + Decimal::from_percent(wsol_reserve.account.config.max_borrow_rate) + .try_div(Decimal::from(SLOTS_PER_YEAR)) + .unwrap(), + ) + .unwrap(); + let new_borrowed_amount_wads = new_cumulative_borrow_rate + .try_mul(Decimal::from(6 * LAMPORTS_PER_SOL)) + .unwrap(); + let new_borrow_value = new_borrowed_amount_wads + .try_mul(Decimal::from(10u64)) + .unwrap() + .try_div(Decimal::from(LAMPORTS_PER_SOL)) + .unwrap(); + let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; assert_eq!( usdc_reserve_post.account, @@ -204,24 +221,13 @@ async fn test_success() { smoothed_market_price: Decimal::from_percent(90), ..usdc_reserve.account.liquidity }, + attributed_borrow_value: new_borrow_value, ..usdc_reserve.account } ); let wsol_reserve_post = test.load_account::(wsol_reserve.pubkey).await; - // 1 + 0.3/SLOTS_PER_YEAR - let new_cumulative_borrow_rate = Decimal::one() - .try_add( - Decimal::from_percent(wsol_reserve.account.config.max_borrow_rate) - .try_div(Decimal::from(SLOTS_PER_YEAR)) - .unwrap(), - ) - .unwrap(); - let new_borrowed_amount_wads = new_cumulative_borrow_rate - .try_mul(Decimal::from(6 * LAMPORTS_PER_SOL)) - .unwrap(); - assert_eq!( wsol_reserve_post.account, Reserve { @@ -241,11 +247,6 @@ async fn test_success() { ); let obligation_post = test.load_account::(obligation.pubkey).await; - let new_borrow_value = new_borrowed_amount_wads - .try_mul(Decimal::from(10u64)) - .unwrap() - .try_div(Decimal::from(LAMPORTS_PER_SOL)) - .unwrap(); assert_eq!( obligation_post.account, @@ -254,6 +255,10 @@ async fn test_success() { slot: 1001, stale: false }, + deposits: [ObligationCollateral { + attributed_borrow_value: new_borrow_value, + ..obligation.account.deposits[0] + }].to_vec(), borrows: [ObligationLiquidity { borrow_reserve: wsol_reserve.pubkey, cumulative_borrow_rate_wads: new_cumulative_borrow_rate, From 2bb22cb6b89f9617b044b5ca1cf00b9bfc2f065c Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Wed, 15 Nov 2023 12:50:32 -0500 Subject: [PATCH 06/25] basic test --- .../program/tests/attributed_borrows.rs | 185 ++++++++++++++++++ .../tests/helpers/solend_program_test.rs | 2 + 2 files changed, 187 insertions(+) create mode 100644 token-lending/program/tests/attributed_borrows.rs diff --git a/token-lending/program/tests/attributed_borrows.rs b/token-lending/program/tests/attributed_borrows.rs new file mode 100644 index 00000000000..078db5383e0 --- /dev/null +++ b/token-lending/program/tests/attributed_borrows.rs @@ -0,0 +1,185 @@ +#![cfg(feature = "test-bpf")] + +use solend_program::state::Reserve; +use crate::solend_program_test::custom_scenario; +use solend_program::state::ObligationCollateral; + +use crate::solend_program_test::ObligationArgs; +use crate::solend_program_test::PriceArgs; +use crate::solend_program_test::ReserveArgs; + +use solana_program::native_token::LAMPORTS_PER_SOL; +use solana_sdk::instruction::InstructionError; +use solana_sdk::transaction::TransactionError; +use solend_program::error::LendingError; +use solend_sdk::math::Decimal; + +use solend_program::state::LastUpdate; +use solend_program::state::ReserveType; +use solend_program::state::{Obligation, ObligationLiquidity, ReserveConfig}; + +use solend_sdk::state::ReserveFees; +mod helpers; + +use helpers::*; +use solana_program_test::*; + +#[tokio::test] +async fn test_calculations() { + let (mut test, lending_market, reserves, obligations, users, _) = 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(), 80 * FRACTIONAL_TO_USDC), + (wsol_mint::id(), 2 * LAMPORTS_PER_SOL), + ], + borrows: vec![ + (usdc_mint::id(), 10 * FRACTIONAL_TO_USDC), + (wsol_mint::id(), LAMPORTS_PER_SOL), + ], + }, + ObligationArgs { + deposits: vec![ + (usdc_mint::id(), 400 * FRACTIONAL_TO_USDC), + (wsol_mint::id(), 10 * LAMPORTS_PER_SOL), + ], + borrows: vec![ + (usdc_mint::id(), 100 * FRACTIONAL_TO_USDC), + (wsol_mint::id(), 2 * LAMPORTS_PER_SOL), + ], + }, + ], + ) + .await; + + // check initial borrow attribution values + // obligation 0 + // usdc.borrow_attribution = 80 / 100 * 20 = 16 + assert_eq!( + obligations[0].account.deposits[0].attributed_borrow_value, + Decimal::from(16u64) + ); + // wsol.borrow_attribution = 20 / 100 * 20 = 4 + assert_eq!( + obligations[0].account.deposits[1].attributed_borrow_value, + Decimal::from(4u64) + ); + + // obligation 1 + // usdc.borrow_attribution = 400 / 500 * 120 = 96 + assert_eq!( + obligations[1].account.deposits[0].attributed_borrow_value, + Decimal::from(96u64) + ); + // wsol.borrow_attribution = 100 / 500 * 120 = 24 + assert_eq!( + obligations[1].account.deposits[1].attributed_borrow_value, + Decimal::from(24u64) + ); + + // usdc reserve: 16 + 96 = 112 + assert_eq!( + reserves[0].account.attributed_borrow_value, + Decimal::from(112u64) + ); + // wsol reserve: 4 + 24 = 28 + assert_eq!( + reserves[1].account.attributed_borrow_value, + Decimal::from(28u64) + ); + + // borrow another 10 usd from obligation 0 + lending_market + .borrow_obligation_liquidity( + &mut test, + &reserves[0], + &obligations[0], + &users[0], + None, + 10 * FRACTIONAL_TO_USDC, + ) + .await + .unwrap(); + + test.advance_clock_by_slots(1).await; + + lending_market + .refresh_obligation(&mut test, &obligations[0]) + .await + .unwrap(); + + let obligation_post = test.load_account::(obligations[0].pubkey).await; + + // obligation 0 after borrowing 10 usd + // usdc.borrow_attribution = 80 / 100 * 30 = 24 + assert_eq!( + obligation_post.account.deposits[0].attributed_borrow_value, + Decimal::from(24u64) + ); + + // wsol.borrow_attribution = 20 / 100 * 30 = 6 + assert_eq!( + obligation_post.account.deposits[1].attributed_borrow_value, + Decimal::from(6u64) + ); + + let usdc_reserve_post = test.load_account::(reserves[0].pubkey).await; + assert_eq!( + usdc_reserve_post.account.attributed_borrow_value, + Decimal::from(120u64) + ); + + let wsol_reserve_post = test.load_account::(reserves[1].pubkey).await; + assert_eq!( + wsol_reserve_post.account.attributed_borrow_value, + Decimal::from(30u64) + ); +} diff --git a/token-lending/program/tests/helpers/solend_program_test.rs b/token-lending/program/tests/helpers/solend_program_test.rs index e3adf1b94ad..58728299d9b 100644 --- a/token-lending/program/tests/helpers/solend_program_test.rs +++ b/token-lending/program/tests/helpers/solend_program_test.rs @@ -1837,6 +1837,8 @@ pub async fn custom_scenario( // load accounts into reserve for reserve in reserves.iter_mut() { + lending_market.refresh_reserve(&mut test, reserve).await.unwrap(); + *reserve = test.load_account(reserve.pubkey).await; } From 986257602a04541c5dc4f12404d3bc618fa7f423 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Wed, 15 Nov 2023 12:51:18 -0500 Subject: [PATCH 07/25] saturating sub test --- token-lending/cli/src/main.rs | 2 ++ token-lending/program/src/processor.rs | 2 +- .../program/tests/attributed_borrows.rs | 11 +++------- .../tests/helpers/solend_program_test.rs | 7 +++++-- .../program/tests/refresh_obligation.rs | 3 ++- token-lending/sdk/src/math/decimal.rs | 12 +++++++++++ token-lending/sdk/src/state/obligation.rs | 20 ++++++++++++++----- token-lending/sdk/src/state/reserve.rs | 2 +- 8 files changed, 41 insertions(+), 18 deletions(-) diff --git a/token-lending/cli/src/main.rs b/token-lending/cli/src/main.rs index 7e4c6a66a46..89e51f9ad7e 100644 --- a/token-lending/cli/src/main.rs +++ b/token-lending/cli/src/main.rs @@ -1143,6 +1143,7 @@ fn main() { let reserve_type = value_of(arg_matches, "reserve_type").unwrap(); let scaled_price_offset_bps = value_of(arg_matches, "scaled_price_offset_bps").unwrap(); let extra_oracle_pubkey = pubkey_of(arg_matches, "extra_oracle_pubkey").unwrap(); + let attributed_borrow_limit = value_of(arg_matches, "attributed_borrow_limit").unwrap(); let borrow_fee_wad = (borrow_fee * WAD as f64) as u64; let flash_loan_fee_wad = (flash_loan_fee * WAD as f64) as u64; @@ -1198,6 +1199,7 @@ fn main() { reserve_type, scaled_price_offset_bps, extra_oracle_pubkey: Some(extra_oracle_pubkey), + attributed_borrow_limit, }, source_liquidity_pubkey, source_liquidity_owner_keypair, diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index c0c2a1a5201..dcd79450857 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1149,7 +1149,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> let deposit_infos = &mut accounts.iter().skip(1); // attributed borrow calculation - for (index, collateral) in obligation.deposits.iter_mut().enumerate() { + for (_index, collateral) in obligation.deposits.iter_mut().enumerate() { let deposit_reserve_info = next_account_info(deposit_infos)?; let mut deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; diff --git a/token-lending/program/tests/attributed_borrows.rs b/token-lending/program/tests/attributed_borrows.rs index 078db5383e0..55faa8f455c 100644 --- a/token-lending/program/tests/attributed_borrows.rs +++ b/token-lending/program/tests/attributed_borrows.rs @@ -1,22 +1,17 @@ #![cfg(feature = "test-bpf")] -use solend_program::state::Reserve; use crate::solend_program_test::custom_scenario; -use solend_program::state::ObligationCollateral; +use solend_program::state::Reserve; use crate::solend_program_test::ObligationArgs; use crate::solend_program_test::PriceArgs; use crate::solend_program_test::ReserveArgs; use solana_program::native_token::LAMPORTS_PER_SOL; -use solana_sdk::instruction::InstructionError; -use solana_sdk::transaction::TransactionError; -use solend_program::error::LendingError; + use solend_sdk::math::Decimal; -use solend_program::state::LastUpdate; -use solend_program::state::ReserveType; -use solend_program::state::{Obligation, ObligationLiquidity, ReserveConfig}; +use solend_program::state::{Obligation, ReserveConfig}; use solend_sdk::state::ReserveFees; mod helpers; diff --git a/token-lending/program/tests/helpers/solend_program_test.rs b/token-lending/program/tests/helpers/solend_program_test.rs index 58728299d9b..c0ed706f033 100644 --- a/token-lending/program/tests/helpers/solend_program_test.rs +++ b/token-lending/program/tests/helpers/solend_program_test.rs @@ -1016,7 +1016,7 @@ impl Info { .await; test.process_transaction(&refresh_ixs, None).await.unwrap(); - let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(55_000)]; + let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(60_000)]; instructions.push(borrow_obligation_liquidity( solend_program::id(), liquidity_amount, @@ -1837,7 +1837,10 @@ pub async fn custom_scenario( // load accounts into reserve for reserve in reserves.iter_mut() { - lending_market.refresh_reserve(&mut test, reserve).await.unwrap(); + lending_market + .refresh_reserve(&mut test, reserve) + .await + .unwrap(); *reserve = test.load_account(reserve.pubkey).await; } diff --git a/token-lending/program/tests/refresh_obligation.rs b/token-lending/program/tests/refresh_obligation.rs index 7d89793c823..7d1d7c1e5c4 100644 --- a/token-lending/program/tests/refresh_obligation.rs +++ b/token-lending/program/tests/refresh_obligation.rs @@ -258,7 +258,8 @@ async fn test_success() { deposits: [ObligationCollateral { attributed_borrow_value: new_borrow_value, ..obligation.account.deposits[0] - }].to_vec(), + }] + .to_vec(), borrows: [ObligationLiquidity { borrow_reserve: wsol_reserve.pubkey, cumulative_borrow_rate_wads: new_cumulative_borrow_rate, diff --git a/token-lending/sdk/src/math/decimal.rs b/token-lending/sdk/src/math/decimal.rs index 71bc11b70df..3de42a007cb 100644 --- a/token-lending/sdk/src/math/decimal.rs +++ b/token-lending/sdk/src/math/decimal.rs @@ -313,4 +313,16 @@ mod test { "0.000000000000000001" ); } + + #[test] + fn test_saturating_sub() { + assert_eq!( + Decimal::from(1u64).saturating_sub(Decimal::from(2u64)), + Decimal::zero() + ); + assert_eq!( + Decimal::from(2u64).saturating_sub(Decimal::from(1u64)), + Decimal::one() + ); + } } diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs index edba4563a15..1b1a230c9eb 100644 --- a/token-lending/sdk/src/state/obligation.rs +++ b/token-lending/sdk/src/state/obligation.rs @@ -315,7 +315,7 @@ pub struct ObligationCollateral { /// Collateral market value in quote currency pub market_value: Decimal, /// How much borrow is attributed to this collateral (USD) - pub attributed_borrow_value: Decimal + pub attributed_borrow_value: Decimal, } impl ObligationCollateral { @@ -481,8 +481,13 @@ impl Pack for Obligation { for collateral in &self.deposits { let deposits_flat = array_mut_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN]; #[allow(clippy::ptr_offset_with_cast)] - let (deposit_reserve, deposited_amount, market_value, attributed_borrow_value, _padding_deposit) = - mut_array_refs![deposits_flat, PUBKEY_BYTES, 8, 16, 16, 16]; + let ( + deposit_reserve, + deposited_amount, + market_value, + attributed_borrow_value, + _padding_deposit, + ) = mut_array_refs![deposits_flat, PUBKEY_BYTES, 8, 16, 16, 16]; deposit_reserve.copy_from_slice(collateral.deposit_reserve.as_ref()); *deposited_amount = collateral.deposited_amount.to_le_bytes(); pack_decimal(collateral.market_value, market_value); @@ -568,8 +573,13 @@ impl Pack for Obligation { for _ in 0..deposits_len { let deposits_flat = array_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN]; #[allow(clippy::ptr_offset_with_cast)] - let (deposit_reserve, deposited_amount, market_value, attributed_borrow_value, _padding_deposit) = - array_refs![deposits_flat, PUBKEY_BYTES, 8, 16, 16, 16]; + let ( + deposit_reserve, + deposited_amount, + market_value, + attributed_borrow_value, + _padding_deposit, + ) = array_refs![deposits_flat, PUBKEY_BYTES, 8, 16, 16, 16]; deposits.push(ObligationCollateral { deposit_reserve: Pubkey::new(deposit_reserve), deposited_amount: u64::from_le_bytes(*deposited_amount), diff --git a/token-lending/sdk/src/state/reserve.rs b/token-lending/sdk/src/state/reserve.rs index 2e922a5ffa2..6886fcd41e5 100644 --- a/token-lending/sdk/src/state/reserve.rs +++ b/token-lending/sdk/src/state/reserve.rs @@ -944,7 +944,7 @@ pub struct ReserveConfig { /// Extra oracle. Only used to limit borrows and withdrawals. pub extra_oracle_pubkey: Option, /// Attributed Borrow limit in USD - pub attributed_borrow_limit: u64 + pub attributed_borrow_limit: u64, } /// validates reserve configs From 0b0164f03ba7acf5901b8ed8f54ce254c0028027 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Thu, 16 Nov 2023 16:59:10 -0500 Subject: [PATCH 08/25] block borrows if borrow attribution limit is reached. also update borrow attribution values in the borrow instruction --- token-lending/program/src/processor.rs | 45 +++++++++++++++++++ .../tests/borrow_obligation_liquidity.rs | 24 ++++++++++ .../tests/helpers/solend_program_test.rs | 3 +- token-lending/sdk/src/instruction.rs | 5 +++ 4 files changed, 76 insertions(+), 1 deletion(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index dcd79450857..5c44c1a36b3 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1796,6 +1796,51 @@ fn process_borrow_obligation_liquidity( })?; } + // check that the borrow doesn't exceed the borrow attribution limit for any of the deposit + // reserves + let borrow_value_usd = borrow_reserve.market_value(borrow_amount)?; + for deposit in obligation.deposits.iter_mut() { + let deposit_reserve_info = next_account_info(account_info_iter)?; + if *deposit_reserve_info.key != deposit.deposit_reserve { + msg!("Deposit reserve provided does not match the deposit reserve in the obligation"); + return Err(LendingError::InvalidAccountInput.into()); + } + + let mut reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; + + // edge case. if the deposit reserve == borrow reserve, we need to use the already loaded + // borrow reserve instead of unpacking it again, otherwise we'll lose data. + let deposit_reserve = if deposit_reserve_info.key != borrow_reserve_info.key { + &mut reserve + } else { + &mut borrow_reserve + }; + + // divbyzero not possible since we check that it's nonzero earlier + let additional_borrow_attributed = borrow_value_usd + .try_mul(deposit.market_value)? + .try_div(obligation.deposited_value)?; + + deposit_reserve.attributed_borrow_value = deposit_reserve + .attributed_borrow_value + .try_add(additional_borrow_attributed)?; + + if deposit_reserve.attributed_borrow_value + > Decimal::from(deposit_reserve.config.attributed_borrow_limit) + { + msg!("Borrow would exceed the deposit reserve's borrow attribution limit"); + return Err(LendingError::BorrowTooLarge.into()); + } + + deposit.attributed_borrow_value = deposit + .attributed_borrow_value + .try_add(additional_borrow_attributed)?; + + if deposit_reserve_info.key != borrow_reserve_info.key { + Reserve::pack(reserve, &mut deposit_reserve_info.data.borrow_mut())?; + } + } + LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?; borrow_reserve.liquidity.borrow(borrow_amount)?; diff --git a/token-lending/program/tests/borrow_obligation_liquidity.rs b/token-lending/program/tests/borrow_obligation_liquidity.rs index 319cf0394cc..4d435193f88 100644 --- a/token-lending/program/tests/borrow_obligation_liquidity.rs +++ b/token-lending/program/tests/borrow_obligation_liquidity.rs @@ -177,6 +177,11 @@ async fn test_success() { let lending_market_post = test .load_account::(lending_market.pubkey) .await; + + let borrow_value = Decimal::from(10 * (4 * LAMPORTS_PER_SOL + 400)) + .try_div(Decimal::from(1_000_000_000_u64)) + .unwrap(); + assert_eq!( lending_market_post.account, LendingMarket { @@ -196,6 +201,21 @@ async fn test_success() { } ); + let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; + assert_eq!( + usdc_reserve_post.account, + Reserve { + last_update: LastUpdate { + slot: 1000, + stale: false, + }, + attributed_borrow_value: borrow_value, + ..usdc_reserve.account + }, + "{:#?}", + usdc_reserve_post, + ); + let wsol_reserve_post = test.load_account::(wsol_reserve.pubkey).await; let expected_wsol_reserve_post = Reserve { last_update: LastUpdate { @@ -232,6 +252,10 @@ async fn test_success() { slot: 1000, stale: true }, + deposits: vec![ObligationCollateral { + attributed_borrow_value: borrow_value, + ..obligation.account.deposits[0] + }], borrows: vec![ObligationLiquidity { borrow_reserve: wsol_reserve.pubkey, borrowed_amount_wads: Decimal::from(4 * LAMPORTS_PER_SOL + 400), diff --git a/token-lending/program/tests/helpers/solend_program_test.rs b/token-lending/program/tests/helpers/solend_program_test.rs index c0ed706f033..9e81aaccf54 100644 --- a/token-lending/program/tests/helpers/solend_program_test.rs +++ b/token-lending/program/tests/helpers/solend_program_test.rs @@ -1016,7 +1016,7 @@ impl Info { .await; test.process_transaction(&refresh_ixs, None).await.unwrap(); - let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(60_000)]; + let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(80_000)]; instructions.push(borrow_obligation_liquidity( solend_program::id(), liquidity_amount, @@ -1028,6 +1028,7 @@ impl Info { obligation.pubkey, self.pubkey, user.keypair.pubkey(), + obligation.account.deposits.iter().map(|d| d.deposit_reserve).collect(), host_fee_receiver_pubkey, )); diff --git a/token-lending/sdk/src/instruction.rs b/token-lending/sdk/src/instruction.rs index 2a80656c3b8..dd4f725254d 100644 --- a/token-lending/sdk/src/instruction.rs +++ b/token-lending/sdk/src/instruction.rs @@ -1440,6 +1440,7 @@ pub fn borrow_obligation_liquidity( obligation_pubkey: Pubkey, lending_market_pubkey: Pubkey, obligation_owner_pubkey: Pubkey, + collateral_reserves: Vec, host_fee_receiver_pubkey: Option, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( @@ -1457,6 +1458,10 @@ pub fn borrow_obligation_liquidity( AccountMeta::new_readonly(obligation_owner_pubkey, true), AccountMeta::new_readonly(spl_token::id(), false), ]; + for collateral_reserve in collateral_reserves { + accounts.push(AccountMeta::new(collateral_reserve, false)); + } + if let Some(host_fee_receiver_pubkey) = host_fee_receiver_pubkey { accounts.push(AccountMeta::new(host_fee_receiver_pubkey, false)); } From e1b5f35f218810b213a35fde271608ef5d0903d4 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Thu, 16 Nov 2023 17:54:17 -0500 Subject: [PATCH 09/25] test blocking borrows --- token-lending/program/src/processor.rs | 2 +- .../program/tests/attributed_borrows.rs | 243 +++++++++++++----- 2 files changed, 175 insertions(+), 70 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 5c44c1a36b3..44b6265ef30 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1809,7 +1809,7 @@ fn process_borrow_obligation_liquidity( let mut reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; // edge case. if the deposit reserve == borrow reserve, we need to use the already loaded - // borrow reserve instead of unpacking it again, otherwise we'll lose data. + // borrow reserve instead of unpacking it again, otherwise we'll lose prior changes let deposit_reserve = if deposit_reserve_info.key != borrow_reserve_info.key { &mut reserve } else { diff --git a/token-lending/program/tests/attributed_borrows.rs b/token-lending/program/tests/attributed_borrows.rs index 55faa8f455c..4727dd028a6 100644 --- a/token-lending/program/tests/attributed_borrows.rs +++ b/token-lending/program/tests/attributed_borrows.rs @@ -1,7 +1,13 @@ #![cfg(feature = "test-bpf")] use crate::solend_program_test::custom_scenario; +use solana_sdk::instruction::InstructionError; +use solana_sdk::transaction::TransactionError; +use solend_program::math::TryAdd; +use solend_program::state::LastUpdate; use solend_program::state::Reserve; +use solend_sdk::error::LendingError; +use solend_sdk::state::ReserveLiquidity; use crate::solend_program_test::ObligationArgs; use crate::solend_program_test::PriceArgs; @@ -21,79 +27,80 @@ use solana_program_test::*; #[tokio::test] async fn test_calculations() { - let (mut test, lending_market, reserves, obligations, users, _) = 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() + 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, }, - 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() + 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, }, - 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(), 80 * FRACTIONAL_TO_USDC), + (wsol_mint::id(), 2 * LAMPORTS_PER_SOL), + ], + borrows: vec![ + (usdc_mint::id(), 10 * FRACTIONAL_TO_USDC), + (wsol_mint::id(), LAMPORTS_PER_SOL), + ], }, - }, - ], - &[ - ObligationArgs { - deposits: vec![ - (usdc_mint::id(), 80 * FRACTIONAL_TO_USDC), - (wsol_mint::id(), 2 * LAMPORTS_PER_SOL), - ], - borrows: vec![ - (usdc_mint::id(), 10 * FRACTIONAL_TO_USDC), - (wsol_mint::id(), LAMPORTS_PER_SOL), - ], - }, - ObligationArgs { - deposits: vec![ - (usdc_mint::id(), 400 * FRACTIONAL_TO_USDC), - (wsol_mint::id(), 10 * LAMPORTS_PER_SOL), - ], - borrows: vec![ - (usdc_mint::id(), 100 * FRACTIONAL_TO_USDC), - (wsol_mint::id(), 2 * LAMPORTS_PER_SOL), - ], - }, - ], - ) - .await; + ObligationArgs { + deposits: vec![ + (usdc_mint::id(), 400 * FRACTIONAL_TO_USDC), + (wsol_mint::id(), 10 * LAMPORTS_PER_SOL), + ], + borrows: vec![ + (usdc_mint::id(), 100 * FRACTIONAL_TO_USDC), + (wsol_mint::id(), 2 * LAMPORTS_PER_SOL), + ], + }, + ], + ) + .await; // check initial borrow attribution values // obligation 0 @@ -131,8 +138,24 @@ async fn test_calculations() { Decimal::from(28u64) ); - // borrow another 10 usd from obligation 0 + // change borrow attribution limit, check that it's applied lending_market + .update_reserve_config( + &mut test, + &lending_market_owner, + &reserves[0], + ReserveConfig { + attributed_borrow_limit: 113, + ..reserves[0].account.config + }, + reserves[0].account.rate_limiter.config, + None, + ) + .await + .unwrap(); + + // attempt to borrow another 10 usd from obligation 0, this should fail + let err = lending_market .borrow_obligation_liquidity( &mut test, &reserves[0], @@ -142,10 +165,92 @@ async fn test_calculations() { 10 * FRACTIONAL_TO_USDC, ) .await + .unwrap_err() + .unwrap(); + + assert_eq!( + err, + TransactionError::InstructionError( + 1, + InstructionError::Custom(LendingError::BorrowTooLarge 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: 120, + ..reserves[0].account.config + }, + reserves[0].account.rate_limiter.config, + None, + ) + .await .unwrap(); test.advance_clock_by_slots(1).await; + // attempt to borrow another 10 usd from obligation 0, this should pass now + lending_market + .borrow_obligation_liquidity( + &mut test, + &reserves[0], + &obligations[0], + &users[0], + None, + 10 * FRACTIONAL_TO_USDC, + ) + .await + .unwrap(); + + + // check both reserves before refresh, since the borrow attribution values should have been + // updated + { + let usdc_reserve = reserves[0].account.clone(); + let usdc_reserve_post = test.load_account::(reserves[0].pubkey).await; + let expected_usdc_reserve_post = Reserve { + last_update: LastUpdate { + slot: 1001, + stale: true, + }, + liquidity: ReserveLiquidity { + available_amount: usdc_reserve.liquidity.available_amount - 10 * FRACTIONAL_TO_USDC, + borrowed_amount_wads: usdc_reserve + .liquidity + .borrowed_amount_wads + .try_add(Decimal::from(10 * FRACTIONAL_TO_USDC)) + .unwrap(), + ..usdc_reserve.liquidity + }, + rate_limiter: { + let mut rate_limiter = usdc_reserve.rate_limiter; + rate_limiter + .update(1000, Decimal::from(10 * FRACTIONAL_TO_USDC)) + .unwrap(); + + rate_limiter + }, + attributed_borrow_value: Decimal::from(120u64), + config: ReserveConfig { + attributed_borrow_limit: 120, + ..usdc_reserve.config + }, + ..usdc_reserve + }; + assert_eq!(usdc_reserve_post.account, expected_usdc_reserve_post); + + let wsol_reserve_post = test.load_account::(reserves[1].pubkey).await; + assert_eq!( + wsol_reserve_post.account.attributed_borrow_value, + Decimal::from(30u64) + ); + } + lending_market .refresh_obligation(&mut test, &obligations[0]) .await From fd3cd2dce5db2d383bce93f948409d7b1fbfc9fa Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Thu, 16 Nov 2023 17:59:59 -0500 Subject: [PATCH 10/25] clippy --- token-lending/program/tests/attributed_borrows.rs | 1 - token-lending/program/tests/helpers/solend_program_test.rs | 7 ++++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/token-lending/program/tests/attributed_borrows.rs b/token-lending/program/tests/attributed_borrows.rs index 4727dd028a6..ab7cc531059 100644 --- a/token-lending/program/tests/attributed_borrows.rs +++ b/token-lending/program/tests/attributed_borrows.rs @@ -207,7 +207,6 @@ async fn test_calculations() { .await .unwrap(); - // check both reserves before refresh, since the borrow attribution values should have been // updated { diff --git a/token-lending/program/tests/helpers/solend_program_test.rs b/token-lending/program/tests/helpers/solend_program_test.rs index 9e81aaccf54..5d58d8aff60 100644 --- a/token-lending/program/tests/helpers/solend_program_test.rs +++ b/token-lending/program/tests/helpers/solend_program_test.rs @@ -1028,7 +1028,12 @@ impl Info { obligation.pubkey, self.pubkey, user.keypair.pubkey(), - obligation.account.deposits.iter().map(|d| d.deposit_reserve).collect(), + obligation + .account + .deposits + .iter() + .map(|d| d.deposit_reserve) + .collect(), host_fee_receiver_pubkey, )); From b21d8319af0b129515af24ab7ec160db4b9751e6 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Fri, 17 Nov 2023 10:20:43 -0500 Subject: [PATCH 11/25] writing compute unit test --- .../program/tests/attributed_borrows.rs | 119 ++++++++++++++++++ .../tests/helpers/solend_program_test.rs | 20 ++- 2 files changed, 134 insertions(+), 5 deletions(-) diff --git a/token-lending/program/tests/attributed_borrows.rs b/token-lending/program/tests/attributed_borrows.rs index ab7cc531059..284a40e6e01 100644 --- a/token-lending/program/tests/attributed_borrows.rs +++ b/token-lending/program/tests/attributed_borrows.rs @@ -1,6 +1,10 @@ #![cfg(feature = "test-bpf")] use crate::solend_program_test::custom_scenario; +use crate::solend_program_test::User; +use crate::tokio::time::sleep; +use crate::tokio::time::Duration; +use log::info; use solana_sdk::instruction::InstructionError; use solana_sdk::transaction::TransactionError; use solend_program::math::TryAdd; @@ -282,3 +286,118 @@ async fn test_calculations() { Decimal::from(30u64) ); } + +#[tokio::test] +async fn benchmark() { + // setup + let reserve_arg = 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 * FRACTIONAL_TO_USDC, + price: PriceArgs { + price: 10, + conf: 0, + expo: -1, + ema_price: 10, + ema_conf: 1, + }, + }; + + let reserve_args = vec![reserve_arg; 9]; + + let obligation_args = ObligationArgs { + deposits: vec![], + borrows: vec![], + }; + + let (mut test, lending_market, reserves, obligations, mut users, lending_market_owner) = + custom_scenario(&reserve_args, &[obligation_args]).await; + + let user = User::new_with_balances( + &mut test, + &[(&usdc_mint::id(), 100_000 * FRACTIONAL_TO_USDC)], + ) + .await; + + user.transfer( + &usdc_mint::id(), + users[0].get_account(&usdc_mint::id()).unwrap(), + 100_000 * FRACTIONAL_TO_USDC, + &mut test, + ) + .await; + + test.advance_clock_by_slots(1).await; + + for reserve in &reserves { + users[0] + .create_token_account(&reserve.account.collateral.mint_pubkey, &mut test) + .await; + + lending_market + .deposit_reserve_liquidity_and_obligation_collateral( + &mut test, + reserve, + &obligations[0], + &users[0], + 10 * FRACTIONAL_TO_USDC, + ) + .await + .unwrap(); + + test.advance_clock_by_slots(1).await; + } + + lending_market + .borrow_obligation_liquidity( + &mut test, + &reserves[0], + &obligations[0], + &users[0], + None, + FRACTIONAL_TO_USDC, + ) + .await + .unwrap(); + + info!("Starting benchmark"); + // lending_market + // .refresh_obligation(&mut test, &obligations[0]) + // .await + // .unwrap(); + + // test.advance_clock_by_slots(1).await; + + for reserve in reserves.iter().skip(1).rev() { + lending_market + .withdraw_obligation_collateral_and_redeem_reserve_collateral( + &mut test, + reserve, + &obligations[0], + &users[0], + u64::MAX, + ) + .await + .unwrap(); + + test.advance_clock_by_slots(1).await; + } + + lending_market + .refresh_obligation(&mut test, &obligations[0]) + .await + .unwrap(); + + test.advance_clock_by_slots(1).await; +} diff --git a/token-lending/program/tests/helpers/solend_program_test.rs b/token-lending/program/tests/helpers/solend_program_test.rs index 5d58d8aff60..a0f66c67c4c 100644 --- a/token-lending/program/tests/helpers/solend_program_test.rs +++ b/token-lending/program/tests/helpers/solend_program_test.rs @@ -680,6 +680,7 @@ impl User { } } +#[derive(Debug, Clone)] pub struct PriceArgs { pub price: i64, pub conf: u64, @@ -994,7 +995,7 @@ impl Info { Err(e) => return Err(e), }; - let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(80_000)]; + let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(1_000_000)]; instructions.push(refresh_reserve_instructions.last().unwrap().clone()); test.process_transaction(&instructions, None).await @@ -1016,7 +1017,7 @@ impl Info { .await; test.process_transaction(&refresh_ixs, None).await.unwrap(); - let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(80_000)]; + let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(1_000_000)]; instructions.push(borrow_obligation_liquidity( solend_program::id(), liquidity_amount, @@ -1685,6 +1686,7 @@ pub async fn scenario_1( ) } +#[derive(Debug, Clone)] pub struct ReserveArgs { pub mint: Pubkey, pub config: ReserveConfig, @@ -1709,9 +1711,17 @@ pub async fn custom_scenario( User, ) { let mut test = SolendProgramTest::start_new().await; - let mints_and_liquidity_amounts = reserve_args - .iter() - .map(|reserve_arg| (&reserve_arg.mint, reserve_arg.liquidity_amount)) + let mut mints_and_liquidity_amounts = HashMap::new(); + for arg in reserve_args { + mints_and_liquidity_amounts + .entry(&arg.mint) + .and_modify(|e| *e += arg.liquidity_amount) + .or_insert(arg.liquidity_amount); + } + + let mints_and_liquidity_amounts = mints_and_liquidity_amounts + .into_iter() + .map(|(mint, liquidity_amount)| (mint, liquidity_amount)) .collect::>(); let lending_market_owner = From 5d78df78aa76cae9efa6adf5fd27dbda9501af6d Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Fri, 17 Nov 2023 11:42:06 -0500 Subject: [PATCH 12/25] clippy --- token-lending/program/tests/attributed_borrows.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/token-lending/program/tests/attributed_borrows.rs b/token-lending/program/tests/attributed_borrows.rs index 284a40e6e01..30887c1813e 100644 --- a/token-lending/program/tests/attributed_borrows.rs +++ b/token-lending/program/tests/attributed_borrows.rs @@ -2,8 +2,7 @@ use crate::solend_program_test::custom_scenario; use crate::solend_program_test::User; -use crate::tokio::time::sleep; -use crate::tokio::time::Duration; + use log::info; use solana_sdk::instruction::InstructionError; use solana_sdk::transaction::TransactionError; @@ -321,7 +320,7 @@ async fn benchmark() { borrows: vec![], }; - let (mut test, lending_market, reserves, obligations, mut users, lending_market_owner) = + let (mut test, lending_market, reserves, obligations, mut users, _lending_market_owner) = custom_scenario(&reserve_args, &[obligation_args]).await; let user = User::new_with_balances( From d9b8c5cd0296b58b19f3a0a83d800cbade55093e Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Wed, 22 Nov 2023 11:48:23 -0500 Subject: [PATCH 13/25] refactor borrow attribution calculation --- token-lending/program/src/processor.rs | 71 +++++++++++++++++--------- 1 file changed, 48 insertions(+), 23 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 44b6265ef30..8e7a80017f5 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -32,6 +32,7 @@ use solana_program::{ Sysvar, }, }; +use solend_sdk::state::ObligationCollateral; use solend_sdk::{ oracles::{ get_oracle_type, get_pyth_price_unchecked, validate_pyth_price_account_info, OracleType, @@ -1146,10 +1147,42 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> obligation.last_update.update_slot(clock.slot); - let deposit_infos = &mut accounts.iter().skip(1); + update_borrow_attribution_values(&mut obligation, &accounts[1..])?; - // attributed borrow calculation - for (_index, collateral) in obligation.deposits.iter_mut().enumerate() { + // move the ObligationLiquidity with the max borrow weight to the front + if let Some((_, max_borrow_weight_index)) = max_borrow_weight { + obligation.borrows.swap(0, max_borrow_weight_index); + } + + // filter out ObligationCollaterals and ObligationLiquiditys with an amount of zero + obligation + .deposits + .retain(|collateral| collateral.deposited_amount > 0); + obligation + .borrows + .retain(|liquidity| liquidity.borrowed_amount_wads > Decimal::zero()); + + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; + + Ok(()) +} + +/// This function updates the borrow attribution value on the ObligationCollateral and +/// the reserve. +/// +/// Prerequisites: +/// - the collateral's market value must be refreshed +/// - the obligation's deposited_value must be refreshed +/// - the obligation's borrowed_value must be refreshed +/// +/// Note that this function packs and unpacks deposit reserves. +fn update_borrow_attribution_values( + obligation: &mut Obligation, + deposit_reserve_infos: &[AccountInfo], +) -> ProgramResult { + let deposit_infos = &mut deposit_reserve_infos.iter(); + + for collateral in obligation.deposits.iter_mut() { let deposit_reserve_info = next_account_info(deposit_infos)?; let mut deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; @@ -1167,11 +1200,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> collateral.attributed_borrow_value = collateral .market_value .try_mul(obligation.borrowed_value)? - .try_div(obligation.deposited_value) - .map_err(|e| { - msg!("div failed"); - e - })?; + .try_div(obligation.deposited_value)? } else { collateral.attributed_borrow_value = Decimal::zero(); } @@ -1180,24 +1209,20 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> .attributed_borrow_value .try_add(collateral.attributed_borrow_value)?; - Reserve::pack(deposit_reserve, &mut deposit_reserve_info.data.borrow_mut())?; - } + if deposit_reserve.attributed_borrow_value + > Decimal::from(deposit_reserve.config.attributed_borrow_limit) + { + msg!( + "Attributed borrow value is over the limit for reserve {} and mint {}", + deposit_reserve_info.key, + deposit_reserve.liquidity.mint_pubkey + ); + return Err(LendingError::InvalidAmount.into()); + } - // move the ObligationLiquidity with the max borrow weight to the front - if let Some((_, max_borrow_weight_index)) = max_borrow_weight { - obligation.borrows.swap(0, max_borrow_weight_index); + Reserve::pack(deposit_reserve, &mut deposit_reserve_info.data.borrow_mut())?; } - // filter out ObligationCollaterals and ObligationLiquiditys with an amount of zero - obligation - .deposits - .retain(|collateral| collateral.deposited_amount > 0); - obligation - .borrows - .retain(|liquidity| liquidity.borrowed_amount_wads > Decimal::zero()); - - Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; - Ok(()) } From d7f3e782c1a0b2f7a4dceb7c7e51bdd60e96b57e Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Wed, 22 Nov 2023 12:30:30 -0500 Subject: [PATCH 14/25] refactoring borrow + fixing borrow tests --- token-lending/program/src/processor.rs | 61 ++--- .../program/tests/attributed_borrows.rs | 232 +++++++++--------- .../tests/borrow_obligation_liquidity.rs | 2 +- token-lending/sdk/src/error.rs | 3 + 4 files changed, 133 insertions(+), 165 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 8e7a80017f5..049f061737e 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -32,7 +32,7 @@ use solana_program::{ Sysvar, }, }; -use solend_sdk::state::ObligationCollateral; + use solend_sdk::{ oracles::{ get_oracle_type, get_pyth_price_unchecked, validate_pyth_price_account_info, OracleType, @@ -1217,7 +1217,7 @@ fn update_borrow_attribution_values( deposit_reserve_info.key, deposit_reserve.liquidity.mint_pubkey ); - return Err(LendingError::InvalidAmount.into()); + return Err(LendingError::BorrowAttributionLimitExceeded.into()); } Reserve::pack(deposit_reserve, &mut deposit_reserve_info.data.borrow_mut())?; @@ -1821,55 +1821,15 @@ fn process_borrow_obligation_liquidity( })?; } - // check that the borrow doesn't exceed the borrow attribution limit for any of the deposit - // reserves - let borrow_value_usd = borrow_reserve.market_value(borrow_amount)?; - for deposit in obligation.deposits.iter_mut() { - let deposit_reserve_info = next_account_info(account_info_iter)?; - if *deposit_reserve_info.key != deposit.deposit_reserve { - msg!("Deposit reserve provided does not match the deposit reserve in the obligation"); - return Err(LendingError::InvalidAccountInput.into()); - } - - let mut reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; - - // edge case. if the deposit reserve == borrow reserve, we need to use the already loaded - // borrow reserve instead of unpacking it again, otherwise we'll lose prior changes - let deposit_reserve = if deposit_reserve_info.key != borrow_reserve_info.key { - &mut reserve - } else { - &mut borrow_reserve - }; - - // divbyzero not possible since we check that it's nonzero earlier - let additional_borrow_attributed = borrow_value_usd - .try_mul(deposit.market_value)? - .try_div(obligation.deposited_value)?; - - deposit_reserve.attributed_borrow_value = deposit_reserve - .attributed_borrow_value - .try_add(additional_borrow_attributed)?; - - if deposit_reserve.attributed_borrow_value - > Decimal::from(deposit_reserve.config.attributed_borrow_limit) - { - msg!("Borrow would exceed the deposit reserve's borrow attribution limit"); - return Err(LendingError::BorrowTooLarge.into()); - } - - deposit.attributed_borrow_value = deposit - .attributed_borrow_value - .try_add(additional_borrow_attributed)?; - - if deposit_reserve_info.key != borrow_reserve_info.key { - Reserve::pack(reserve, &mut deposit_reserve_info.data.borrow_mut())?; - } - } - LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?; borrow_reserve.liquidity.borrow(borrow_amount)?; borrow_reserve.last_update.mark_stale(); + + obligation.borrowed_value = obligation + .borrowed_value + .try_add(borrow_reserve.market_value(borrow_amount)?)?; + Reserve::pack(*borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?; let obligation_liquidity = obligation @@ -1877,6 +1837,13 @@ fn process_borrow_obligation_liquidity( obligation_liquidity.borrow(borrow_amount)?; obligation.last_update.mark_stale(); + + update_borrow_attribution_values(&mut obligation, &accounts[9..])?; + // HACK: fast forward through the used account info's + for _ in 0..obligation.deposits.len() { + next_account_info(account_info_iter)?; + } + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; let mut owner_fee = borrow_fee; diff --git a/token-lending/program/tests/attributed_borrows.rs b/token-lending/program/tests/attributed_borrows.rs index 30887c1813e..c250c76fce3 100644 --- a/token-lending/program/tests/attributed_borrows.rs +++ b/token-lending/program/tests/attributed_borrows.rs @@ -1,9 +1,7 @@ #![cfg(feature = "test-bpf")] use crate::solend_program_test::custom_scenario; -use crate::solend_program_test::User; -use log::info; use solana_sdk::instruction::InstructionError; use solana_sdk::transaction::TransactionError; use solend_program::math::TryAdd; @@ -175,7 +173,7 @@ async fn test_calculations() { err, TransactionError::InstructionError( 1, - InstructionError::Custom(LendingError::BorrowTooLarge as u32) + InstructionError::Custom(LendingError::BorrowAttributionLimitExceeded as u32) ) ); @@ -286,117 +284,117 @@ async fn test_calculations() { ); } -#[tokio::test] -async fn benchmark() { - // setup - let reserve_arg = 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 * FRACTIONAL_TO_USDC, - price: PriceArgs { - price: 10, - conf: 0, - expo: -1, - ema_price: 10, - ema_conf: 1, - }, - }; - - let reserve_args = vec![reserve_arg; 9]; - - let obligation_args = ObligationArgs { - deposits: vec![], - borrows: vec![], - }; - - let (mut test, lending_market, reserves, obligations, mut users, _lending_market_owner) = - custom_scenario(&reserve_args, &[obligation_args]).await; - - let user = User::new_with_balances( - &mut test, - &[(&usdc_mint::id(), 100_000 * FRACTIONAL_TO_USDC)], - ) - .await; - - user.transfer( - &usdc_mint::id(), - users[0].get_account(&usdc_mint::id()).unwrap(), - 100_000 * FRACTIONAL_TO_USDC, - &mut test, - ) - .await; - - test.advance_clock_by_slots(1).await; - - for reserve in &reserves { - users[0] - .create_token_account(&reserve.account.collateral.mint_pubkey, &mut test) - .await; - - lending_market - .deposit_reserve_liquidity_and_obligation_collateral( - &mut test, - reserve, - &obligations[0], - &users[0], - 10 * FRACTIONAL_TO_USDC, - ) - .await - .unwrap(); - - test.advance_clock_by_slots(1).await; - } - - lending_market - .borrow_obligation_liquidity( - &mut test, - &reserves[0], - &obligations[0], - &users[0], - None, - FRACTIONAL_TO_USDC, - ) - .await - .unwrap(); - - info!("Starting benchmark"); - // lending_market - // .refresh_obligation(&mut test, &obligations[0]) - // .await - // .unwrap(); - - // test.advance_clock_by_slots(1).await; - - for reserve in reserves.iter().skip(1).rev() { - lending_market - .withdraw_obligation_collateral_and_redeem_reserve_collateral( - &mut test, - reserve, - &obligations[0], - &users[0], - u64::MAX, - ) - .await - .unwrap(); - - test.advance_clock_by_slots(1).await; - } - - lending_market - .refresh_obligation(&mut test, &obligations[0]) - .await - .unwrap(); - - test.advance_clock_by_slots(1).await; -} +// #[tokio::test] +// async fn benchmark() { +// // setup +// let reserve_arg = 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 * FRACTIONAL_TO_USDC, +// price: PriceArgs { +// price: 10, +// conf: 0, +// expo: -1, +// ema_price: 10, +// ema_conf: 1, +// }, +// }; + +// let reserve_args = vec![reserve_arg; 9]; + +// let obligation_args = ObligationArgs { +// deposits: vec![], +// borrows: vec![], +// }; + +// let (mut test, lending_market, reserves, obligations, mut users, _lending_market_owner) = +// custom_scenario(&reserve_args, &[obligation_args]).await; + +// let user = User::new_with_balances( +// &mut test, +// &[(&usdc_mint::id(), 100_000 * FRACTIONAL_TO_USDC)], +// ) +// .await; + +// user.transfer( +// &usdc_mint::id(), +// users[0].get_account(&usdc_mint::id()).unwrap(), +// 100_000 * FRACTIONAL_TO_USDC, +// &mut test, +// ) +// .await; + +// test.advance_clock_by_slots(1).await; + +// for reserve in &reserves { +// users[0] +// .create_token_account(&reserve.account.collateral.mint_pubkey, &mut test) +// .await; + +// lending_market +// .deposit_reserve_liquidity_and_obligation_collateral( +// &mut test, +// reserve, +// &obligations[0], +// &users[0], +// 10 * FRACTIONAL_TO_USDC, +// ) +// .await +// .unwrap(); + +// test.advance_clock_by_slots(1).await; +// } + +// lending_market +// .borrow_obligation_liquidity( +// &mut test, +// &reserves[0], +// &obligations[0], +// &users[0], +// None, +// FRACTIONAL_TO_USDC, +// ) +// .await +// .unwrap(); + +// info!("Starting benchmark"); +// // lending_market +// // .refresh_obligation(&mut test, &obligations[0]) +// // .await +// // .unwrap(); + +// // test.advance_clock_by_slots(1).await; + +// for reserve in reserves.iter().skip(1).rev() { +// lending_market +// .withdraw_obligation_collateral_and_redeem_reserve_collateral( +// &mut test, +// reserve, +// &obligations[0], +// &users[0], +// u64::MAX, +// ) +// .await +// .unwrap(); + +// test.advance_clock_by_slots(1).await; +// } + +// lending_market +// .refresh_obligation(&mut test, &obligations[0]) +// .await +// .unwrap(); + +// test.advance_clock_by_slots(1).await; +// } diff --git a/token-lending/program/tests/borrow_obligation_liquidity.rs b/token-lending/program/tests/borrow_obligation_liquidity.rs index 4d435193f88..dc131030a1d 100644 --- a/token-lending/program/tests/borrow_obligation_liquidity.rs +++ b/token-lending/program/tests/borrow_obligation_liquidity.rs @@ -267,7 +267,7 @@ async fn test_success() { // refresh_obligation }], deposited_value: Decimal::from(100u64), - borrowed_value: Decimal::zero(), + borrowed_value: borrow_value, allowed_borrow_value: Decimal::from(50u64), unhealthy_borrow_value: Decimal::from(55u64), ..obligation.account diff --git a/token-lending/sdk/src/error.rs b/token-lending/sdk/src/error.rs index 23afcb6c8c9..8666d57e238 100644 --- a/token-lending/sdk/src/error.rs +++ b/token-lending/sdk/src/error.rs @@ -203,6 +203,9 @@ pub enum LendingError { /// Isolated Tier Asset Violation #[error("Isolated Tier Asset Violation")] IsolatedTierAssetViolation, + /// Borrow Attribution Limit Exceeded + #[error("Borrow Attribution Limit Exceeded")] + BorrowAttributionLimitExceeded, } impl From for ProgramError { From 87d265f42b58020c57f8c231d64e36078b71c6b4 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Wed, 22 Nov 2023 13:10:15 -0500 Subject: [PATCH 15/25] 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 049f061737e..6ff4e032405 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1457,6 +1457,7 @@ fn process_withdraw_obligation_collateral( clock, token_program_id, false, + &accounts[8..], )?; Ok(()) } @@ -1475,6 +1476,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 { @@ -1595,8 +1597,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 { @@ -2316,6 +2336,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 a0f66c67c4c..c799d047ade 100644 --- a/token-lending/program/tests/helpers/solend_program_test.rs +++ b/token-lending/program/tests/helpers/solend_program_test.rs @@ -1188,7 +1188,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, @@ -1204,6 +1204,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]), @@ -1226,7 +1232,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, @@ -1237,6 +1243,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 dd4f725254d..bb114bb5a21 100644 --- a/token-lending/sdk/src/instruction.rs +++ b/token-lending/sdk/src/instruction.rs @@ -1368,27 +1368,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, } @@ -1407,23 +1417,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(), } } From 0cc73b74ca9cc35485f69e15bdaa829e6a9fb4e6 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Wed, 22 Nov 2023 15:50:34 -0500 Subject: [PATCH 16/25] fix cli --- token-lending/cli/src/lending_state.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/token-lending/cli/src/lending_state.rs b/token-lending/cli/src/lending_state.rs index 25ccf760090..252d8ef3e7f 100644 --- a/token-lending/cli/src/lending_state.rs +++ b/token-lending/cli/src/lending_state.rs @@ -129,6 +129,11 @@ impl SolendState { self.obligation_pubkey, withdraw_reserve.lending_market, self.obligation.owner, + self.obligation + .deposits + .iter() + .map(|d| d.deposit_reserve) + .collect(), )); instructions From 25b05b9b97ecd269cd70e46e03392484ab2e51eb Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Wed, 22 Nov 2023 16:07:12 -0500 Subject: [PATCH 17/25] clippy and fmt --- token-lending/program/tests/attributed_borrows.rs | 14 ++++++++++---- .../tests/withdraw_obligation_collateral.rs | 3 +-- token-lending/sdk/src/instruction.rs | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/token-lending/program/tests/attributed_borrows.rs b/token-lending/program/tests/attributed_borrows.rs index dd2ebfa3a2c..ad5ff10837c 100644 --- a/token-lending/program/tests/attributed_borrows.rs +++ b/token-lending/program/tests/attributed_borrows.rs @@ -1,7 +1,7 @@ #![cfg(feature = "test-bpf")] -use solend_program::math::TryDiv; use crate::solend_program_test::custom_scenario; +use solend_program::math::TryDiv; use solana_sdk::instruction::InstructionError; use solana_sdk::transaction::TransactionError; @@ -536,7 +536,9 @@ async fn test_withdraw() { 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() + Decimal::from(7500u64) + .try_div(Decimal::from(1000u64)) + .unwrap() ); let wsol_reserve_post = test.load_account::(reserves[1].pubkey).await; @@ -548,11 +550,15 @@ async fn test_withdraw() { 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() + 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() + Decimal::from(2500u64) + .try_div(Decimal::from(1000u64)) + .unwrap() ); } diff --git a/token-lending/program/tests/withdraw_obligation_collateral.rs b/token-lending/program/tests/withdraw_obligation_collateral.rs index 95214774134..6cd535dec27 100644 --- a/token-lending/program/tests/withdraw_obligation_collateral.rs +++ b/token-lending/program/tests/withdraw_obligation_collateral.rs @@ -1,12 +1,11 @@ #![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::*; +use solend_sdk::math::Decimal; use solana_program_test::*; diff --git a/token-lending/sdk/src/instruction.rs b/token-lending/sdk/src/instruction.rs index bb114bb5a21..d18cc9ac793 100644 --- a/token-lending/sdk/src/instruction.rs +++ b/token-lending/sdk/src/instruction.rs @@ -1417,7 +1417,7 @@ pub fn withdraw_obligation_collateral( obligation_pubkey: Pubkey, lending_market_pubkey: Pubkey, obligation_owner_pubkey: Pubkey, - collateral_reserves: Vec + collateral_reserves: Vec, ) -> Instruction { let (lending_market_authority_pubkey, _bump_seed) = Pubkey::find_program_address( &[&lending_market_pubkey.to_bytes()[..PUBKEY_BYTES]], From 74069a2797d52dbec094a1eea6cf82845dc4011b Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Thu, 23 Nov 2023 12:07:23 -0500 Subject: [PATCH 18/25] adding true borrowed value to obligation --- token-lending/program/src/processor.rs | 20 +++++++++++++------ .../tests/borrow_obligation_liquidity.rs | 1 + token-lending/program/tests/forgive_debt.rs | 1 + .../program/tests/init_obligation.rs | 1 + .../program/tests/isolated_tier_assets.rs | 1 + ...uidate_obligation_and_redeem_collateral.rs | 1 + .../program/tests/refresh_obligation.rs | 6 ++++++ token-lending/sdk/src/state/obligation.rs | 13 ++++++++++-- 8 files changed, 36 insertions(+), 8 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 6ff4e032405..5eb65fa2893 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1002,6 +1002,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> let mut deposited_value = Decimal::zero(); let mut borrowed_value = Decimal::zero(); // weighted borrow value wrt borrow weights + let mut true_borrowed_value = Decimal::zero(); let mut borrowed_value_upper_bound = Decimal::zero(); let mut allowed_borrow_value = Decimal::zero(); let mut unhealthy_borrow_value = Decimal::zero(); @@ -1059,7 +1060,6 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> let mut borrowing_isolated_asset = false; let mut max_borrow_weight = None; - let mut true_borrow_value = Decimal::zero(); for (index, liquidity) in obligation.borrows.iter_mut().enumerate() { let borrow_reserve_info = next_account_info(account_info_iter)?; if borrow_reserve_info.owner != program_id { @@ -1124,7 +1124,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> borrowed_value.try_add(market_value.try_mul(borrow_reserve.borrow_weight())?)?; borrowed_value_upper_bound = borrowed_value_upper_bound .try_add(market_value_upper_bound.try_mul(borrow_reserve.borrow_weight())?)?; - true_borrow_value = true_borrow_value.try_add(market_value)?; + true_borrowed_value = true_borrowed_value.try_add(market_value)?; } if account_info_iter.peek().is_some() { @@ -1134,6 +1134,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> obligation.deposited_value = deposited_value; obligation.borrowed_value = borrowed_value; + obligation.true_borrowed_value = true_borrowed_value; obligation.borrowed_value_upper_bound = borrowed_value_upper_bound; obligation.borrowing_isolated_asset = borrowing_isolated_asset; @@ -1173,7 +1174,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> /// Prerequisites: /// - the collateral's market value must be refreshed /// - the obligation's deposited_value must be refreshed -/// - the obligation's borrowed_value must be refreshed +/// - the obligation's true_borrowed_value must be refreshed /// /// Note that this function packs and unpacks deposit reserves. fn update_borrow_attribution_values( @@ -1199,7 +1200,7 @@ fn update_borrow_attribution_values( if obligation.deposited_value > Decimal::zero() { collateral.attributed_borrow_value = collateral .market_value - .try_mul(obligation.borrowed_value)? + .try_mul(obligation.true_borrowed_value)? .try_div(obligation.deposited_value)? } else { collateral.attributed_borrow_value = Decimal::zero(); @@ -1846,8 +1847,15 @@ fn process_borrow_obligation_liquidity( borrow_reserve.liquidity.borrow(borrow_amount)?; borrow_reserve.last_update.mark_stale(); - obligation.borrowed_value = obligation - .borrowed_value + // updating these fields is needed to a correct borrow attribution value update later + obligation.borrowed_value = obligation.borrowed_value.try_add( + borrow_reserve + .market_value(borrow_amount)? + .try_mul(borrow_reserve.borrow_weight())?, + )?; + + obligation.true_borrowed_value = obligation + .true_borrowed_value .try_add(borrow_reserve.market_value(borrow_amount)?)?; 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 dc131030a1d..65e87a6df7a 100644 --- a/token-lending/program/tests/borrow_obligation_liquidity.rs +++ b/token-lending/program/tests/borrow_obligation_liquidity.rs @@ -268,6 +268,7 @@ async fn test_success() { }], deposited_value: Decimal::from(100u64), borrowed_value: borrow_value, + true_borrowed_value: borrow_value, allowed_borrow_value: Decimal::from(50u64), unhealthy_borrow_value: Decimal::from(55u64), ..obligation.account diff --git a/token-lending/program/tests/forgive_debt.rs b/token-lending/program/tests/forgive_debt.rs index 084b4a267c3..ac658f2d176 100644 --- a/token-lending/program/tests/forgive_debt.rs +++ b/token-lending/program/tests/forgive_debt.rs @@ -177,6 +177,7 @@ async fn test_forgive_debt_success_easy() { borrows: vec![], deposited_value: Decimal::zero(), borrowed_value: Decimal::from(8u64), + true_borrowed_value: Decimal::from(8u64), borrowed_value_upper_bound: Decimal::from(8u64), allowed_borrow_value: Decimal::zero(), unhealthy_borrow_value: Decimal::zero(), diff --git a/token-lending/program/tests/init_obligation.rs b/token-lending/program/tests/init_obligation.rs index d210e692219..b40f6d80245 100644 --- a/token-lending/program/tests/init_obligation.rs +++ b/token-lending/program/tests/init_obligation.rs @@ -45,6 +45,7 @@ async fn test_success() { borrows: Vec::new(), deposited_value: Decimal::zero(), borrowed_value: Decimal::zero(), + true_borrowed_value: Decimal::zero(), borrowed_value_upper_bound: Decimal::zero(), allowed_borrow_value: Decimal::zero(), unhealthy_borrow_value: Decimal::zero(), diff --git a/token-lending/program/tests/isolated_tier_assets.rs b/token-lending/program/tests/isolated_tier_assets.rs index 17575cf2b86..880fba6f378 100644 --- a/token-lending/program/tests/isolated_tier_assets.rs +++ b/token-lending/program/tests/isolated_tier_assets.rs @@ -124,6 +124,7 @@ async fn test_refresh_obligation() { market_value: Decimal::from(10u64), }], borrowed_value: Decimal::from(10u64), + true_borrowed_value: Decimal::from(10u64), borrowed_value_upper_bound: Decimal::from(10u64), borrowing_isolated_asset: true, ..obligations[0].account.clone() diff --git a/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs b/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs index 6145cc67fdd..55060b97cca 100644 --- a/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs +++ b/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs @@ -231,6 +231,7 @@ async fn test_success_new() { .to_vec(), deposited_value: Decimal::from(100_000u64), borrowed_value: Decimal::from(55_000u64), + true_borrowed_value: Decimal::from(55_000u64), borrowed_value_upper_bound: Decimal::from(55_000u64), allowed_borrow_value: Decimal::from(50_000u64), unhealthy_borrow_value: Decimal::from(55_000u64), diff --git a/token-lending/program/tests/refresh_obligation.rs b/token-lending/program/tests/refresh_obligation.rs index 7d1d7c1e5c4..6f5fde1f197 100644 --- a/token-lending/program/tests/refresh_obligation.rs +++ b/token-lending/program/tests/refresh_obligation.rs @@ -274,6 +274,12 @@ async fn test_success() { .try_div(Decimal::from(LAMPORTS_PER_SOL)) .unwrap(), + true_borrowed_value: new_borrowed_amount_wads + .try_mul(Decimal::from(10u64)) + .unwrap() + .try_div(Decimal::from(LAMPORTS_PER_SOL)) + .unwrap(), + // uses max(10, 11) = 11 for sol price borrowed_value_upper_bound: new_borrowed_amount_wads .try_mul(Decimal::from(11u64)) diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs index 1b1a230c9eb..758be5a76ea 100644 --- a/token-lending/sdk/src/state/obligation.rs +++ b/token-lending/sdk/src/state/obligation.rs @@ -40,6 +40,8 @@ pub struct Obligation { /// Risk-adjusted market value of borrows. /// ie sum(b.borrowed_amount * b.current_spot_price * b.borrow_weight for b in borrows) pub borrowed_value: Decimal, + /// True borrow value. Ie, not risk adjusted like "borrowed_value" + pub true_borrowed_value: Decimal, /// Risk-adjusted upper bound market value of borrows. /// ie sum(b.borrowed_amount * max(b.current_spot_price, b.smoothed_price) * b.borrow_weight for b in borrows) pub borrowed_value_upper_bound: Decimal, @@ -431,6 +433,7 @@ impl Pack for Obligation { borrowed_value_upper_bound, borrowing_isolated_asset, super_unhealthy_borrow_value, + true_borrowed_value, _padding, deposits_len, borrows_len, @@ -449,7 +452,8 @@ impl Pack for Obligation { 16, 1, 16, - 31, + 16, + 15, 1, 1, OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) @@ -471,6 +475,7 @@ impl Pack for Obligation { self.super_unhealthy_borrow_value, super_unhealthy_borrow_value, ); + pack_decimal(self.true_borrowed_value, true_borrowed_value); *deposits_len = u8::try_from(self.deposits.len()).unwrap().to_le_bytes(); *borrows_len = u8::try_from(self.borrows.len()).unwrap().to_le_bytes(); @@ -534,6 +539,7 @@ impl Pack for Obligation { borrowed_value_upper_bound, borrowing_isolated_asset, super_unhealthy_borrow_value, + true_borrowed_value, _padding, deposits_len, borrows_len, @@ -552,7 +558,8 @@ impl Pack for Obligation { 16, 1, 16, - 31, + 16, + 15, 1, 1, OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) @@ -619,6 +626,7 @@ impl Pack for Obligation { borrows, deposited_value: unpack_decimal(deposited_value), borrowed_value: unpack_decimal(borrowed_value), + true_borrowed_value: unpack_decimal(true_borrowed_value), borrowed_value_upper_bound: unpack_decimal(borrowed_value_upper_bound), allowed_borrow_value: unpack_decimal(allowed_borrow_value), unhealthy_borrow_value: unpack_decimal(unhealthy_borrow_value), @@ -668,6 +676,7 @@ mod test { }], deposited_value: rand_decimal(), borrowed_value: rand_decimal(), + true_borrowed_value: rand_decimal(), borrowed_value_upper_bound: rand_decimal(), allowed_borrow_value: rand_decimal(), unhealthy_borrow_value: rand_decimal(), From 7b0ffe73dadd9bfa2bfc4ad0dc9fbe9b4a5710b7 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Thu, 23 Nov 2023 12:54:33 -0500 Subject: [PATCH 19/25] track borrow attribution changes on a full liquidation --- token-lending/program/src/processor.rs | 17 ++- .../program/tests/attributed_borrows.rs | 114 ++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 5eb65fa2893..9893bf2de50 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -2075,7 +2075,7 @@ fn _liquidate_obligation<'a>( return Err(LendingError::ReserveStale.into()); } - let withdraw_reserve = Box::new(Reserve::unpack(&withdraw_reserve_info.data.borrow())?); + let mut withdraw_reserve = Box::new(Reserve::unpack(&withdraw_reserve_info.data.borrow())?); if withdraw_reserve_info.owner != program_id { msg!("Withdraw reserve provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); @@ -2190,6 +2190,21 @@ fn _liquidate_obligation<'a>( repay_reserve.last_update.mark_stale(); Reserve::pack(*repay_reserve, &mut repay_reserve_info.data.borrow_mut())?; + // if there is a full withdraw here (which can happen on a full liquidation), then the borrow + // attribution value needs to be updated on the reserve. note that we can't depend on + // refresh_obligation to update this correctly because the ObligationCollateral object will be + // deleted after this call. + if withdraw_amount == collateral.deposited_amount { + withdraw_reserve.attributed_borrow_value = withdraw_reserve + .attributed_borrow_value + .saturating_sub(collateral.market_value); + + Reserve::pack( + *withdraw_reserve, + &mut withdraw_reserve_info.data.borrow_mut(), + )?; + } + obligation.repay(settle_amount, liquidity_index)?; obligation.withdraw(withdraw_amount, collateral_index)?; obligation.last_update.mark_stale(); diff --git a/token-lending/program/tests/attributed_borrows.rs b/token-lending/program/tests/attributed_borrows.rs index ad5ff10837c..676c04cc9c2 100644 --- a/token-lending/program/tests/attributed_borrows.rs +++ b/token-lending/program/tests/attributed_borrows.rs @@ -1,6 +1,7 @@ #![cfg(feature = "test-bpf")] use crate::solend_program_test::custom_scenario; +use crate::solend_program_test::User; use solend_program::math::TryDiv; use solana_sdk::instruction::InstructionError; @@ -599,3 +600,116 @@ async fn test_withdraw() { ); } } + +#[tokio::test] +async fn test_liquidate() { + 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(), FRACTIONAL_TO_USDC / 2)], + borrows: vec![(wsol_mint::id(), LAMPORTS_PER_SOL / 40)], + }], + ) + .await; + + assert_eq!( + reserves[0].account.attributed_borrow_value, + Decimal::from_percent(25) + ); + + assert_eq!( + obligations[0].account.deposits[0].attributed_borrow_value, + Decimal::from_percent(25) + ); + + let liquidator = User::new_with_balances( + &mut test, + &[ + (&wsol_mint::id(), 100 * LAMPORTS_TO_SOL), + (&reserves[0].account.collateral.mint_pubkey, 0), + (&usdc_mint::id(), 0), + ], + ) + .await; + + test.set_price( + &wsol_mint::id(), + &PriceArgs { + price: 20, + conf: 0, + expo: 0, + ema_price: 10, + ema_conf: 0, + }, + ) + .await; + + test.advance_clock_by_slots(1).await; + + // full liquidation + lending_market + .liquidate_obligation_and_redeem_reserve_collateral( + &mut test, + &reserves[1], + &reserves[0], + &obligations[0], + &liquidator, + u64::MAX, + ) + .await + .unwrap(); + + let usdc_reserve_post = test.load_account::(reserves[0].pubkey).await; + assert_eq!( + usdc_reserve_post.account.attributed_borrow_value, + Decimal::zero() + ); +} From c5b4d2b0c553859fefcbccc26258453c7dc0ddb5 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Thu, 23 Nov 2023 13:21:22 -0500 Subject: [PATCH 20/25] set initial borrow attribution limit value to u64::MAX --- token-lending/sdk/src/state/reserve.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/token-lending/sdk/src/state/reserve.rs b/token-lending/sdk/src/state/reserve.rs index 6886fcd41e5..6e3ab31648d 100644 --- a/token-lending/sdk/src/state/reserve.rs +++ b/token-lending/sdk/src/state/reserve.rs @@ -1575,7 +1575,17 @@ impl Pack for Reserve { } else { Some(Pubkey::new_from_array(*config_extra_oracle_pubkey)) }, - attributed_borrow_limit: u64::from_le_bytes(*config_attributed_borrow_limit), + // this field is added in v2.0.3 and we will never set it to zero. only time it'll + // be zero is when we upgrade from v2.0.2 to v2.0.3. in that case, the correct + // thing to do is set the value to u64::MAX. + attributed_borrow_limit: { + let value = u64::from_le_bytes(*config_attributed_borrow_limit); + if value == 0 { + u64::MAX + } else { + value + } + }, }, rate_limiter: RateLimiter::unpack_from_slice(rate_limiter)?, attributed_borrow_value: unpack_decimal(attributed_borrow_value), From d40ca6057ac1b4aff2f64bfd9379f9155024ec60 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Wed, 29 Nov 2023 13:39:58 -0500 Subject: [PATCH 21/25] optionally error if attribution limit is exceeded --- token-lending/program/src/processor.rs | 12 +- .../program/tests/attributed_borrows.rs | 138 ++++++++++++++++++ 2 files changed, 145 insertions(+), 5 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 9893bf2de50..c33dde83e49 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1148,7 +1148,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> obligation.last_update.update_slot(clock.slot); - update_borrow_attribution_values(&mut obligation, &accounts[1..])?; + update_borrow_attribution_values(&mut obligation, &accounts[1..], false)?; // move the ObligationLiquidity with the max borrow weight to the front if let Some((_, max_borrow_weight_index)) = max_borrow_weight { @@ -1180,6 +1180,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> fn update_borrow_attribution_values( obligation: &mut Obligation, deposit_reserve_infos: &[AccountInfo], + error_if_limit_exceeded: bool, ) -> ProgramResult { let deposit_infos = &mut deposit_reserve_infos.iter(); @@ -1210,8 +1211,9 @@ fn update_borrow_attribution_values( .attributed_borrow_value .try_add(collateral.attributed_borrow_value)?; - if deposit_reserve.attributed_borrow_value - > Decimal::from(deposit_reserve.config.attributed_borrow_limit) + if error_if_limit_exceeded + && deposit_reserve.attributed_borrow_value + > Decimal::from(deposit_reserve.config.attributed_borrow_limit) { msg!( "Attributed borrow value is over the limit for reserve {} and mint {}", @@ -1611,7 +1613,7 @@ fn _withdraw_obligation_collateral<'a>( .market_value .saturating_sub(withdraw_value); - update_borrow_attribution_values(&mut obligation, deposit_reserve_infos)?; + update_borrow_attribution_values(&mut obligation, deposit_reserve_infos, true)?; // 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 @@ -1866,7 +1868,7 @@ fn process_borrow_obligation_liquidity( obligation_liquidity.borrow(borrow_amount)?; obligation.last_update.mark_stale(); - update_borrow_attribution_values(&mut obligation, &accounts[9..])?; + update_borrow_attribution_values(&mut obligation, &accounts[9..], true)?; // HACK: fast forward through the used account info's for _ in 0..obligation.deposits.len() { next_account_info(account_info_iter)?; diff --git a/token-lending/program/tests/attributed_borrows.rs b/token-lending/program/tests/attributed_borrows.rs index 676c04cc9c2..197473f349f 100644 --- a/token-lending/program/tests/attributed_borrows.rs +++ b/token-lending/program/tests/attributed_borrows.rs @@ -28,6 +28,144 @@ mod helpers; use helpers::*; use solana_program_test::*; +#[tokio::test] +async fn test_refresh_obligation() { + 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(), 80 * FRACTIONAL_TO_USDC), + (wsol_mint::id(), 2 * LAMPORTS_PER_SOL), + ], + borrows: vec![ + (usdc_mint::id(), 10 * FRACTIONAL_TO_USDC), + (wsol_mint::id(), LAMPORTS_PER_SOL), + ], + }, + ObligationArgs { + deposits: vec![ + (usdc_mint::id(), 400 * FRACTIONAL_TO_USDC), + (wsol_mint::id(), 10 * LAMPORTS_PER_SOL), + ], + borrows: vec![ + (usdc_mint::id(), 100 * FRACTIONAL_TO_USDC), + (wsol_mint::id(), 2 * LAMPORTS_PER_SOL), + ], + }, + ], + ) + .await; + + // check initial borrow attribution values + // obligation 0 + // usdc.borrow_attribution = 80 / 100 * 20 = 16 + assert_eq!( + obligations[0].account.deposits[0].attributed_borrow_value, + Decimal::from(16u64) + ); + // wsol.borrow_attribution = 20 / 100 * 20 = 4 + assert_eq!( + obligations[0].account.deposits[1].attributed_borrow_value, + Decimal::from(4u64) + ); + + // obligation 1 + // usdc.borrow_attribution = 400 / 500 * 120 = 96 + assert_eq!( + obligations[1].account.deposits[0].attributed_borrow_value, + Decimal::from(96u64) + ); + // wsol.borrow_attribution = 100 / 500 * 120 = 24 + assert_eq!( + obligations[1].account.deposits[1].attributed_borrow_value, + Decimal::from(24u64) + ); + + // usdc reserve: 16 + 96 = 112 + assert_eq!( + reserves[0].account.attributed_borrow_value, + Decimal::from(112u64) + ); + // wsol reserve: 4 + 24 = 28 + assert_eq!( + reserves[1].account.attributed_borrow_value, + Decimal::from(28u64) + ); + + // change borrow attribution limit, check that it's applied + lending_market + .update_reserve_config( + &mut test, + &lending_market_owner, + &reserves[0], + ReserveConfig { + attributed_borrow_limit: 1, + ..reserves[0].account.config + }, + reserves[0].account.rate_limiter.config, + None, + ) + .await + .unwrap(); + + test.advance_clock_by_slots(1).await; + + // make sure it doesn't error + lending_market + .refresh_obligation(&mut test, &obligations[0]) + .await + .unwrap(); +} + #[tokio::test] async fn test_calculations() { let (mut test, lending_market, reserves, obligations, users, lending_market_owner) = From 70710d03ee4638679bc7b67a270824ece10af387 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Wed, 29 Nov 2023 13:43:36 -0500 Subject: [PATCH 22/25] rename true borrow value to unweighted borrow value --- token-lending/program/src/processor.rs | 12 ++++++------ .../program/tests/borrow_obligation_liquidity.rs | 2 +- token-lending/program/tests/forgive_debt.rs | 2 +- token-lending/program/tests/init_obligation.rs | 2 +- token-lending/program/tests/isolated_tier_assets.rs | 2 +- .../liquidate_obligation_and_redeem_collateral.rs | 2 +- token-lending/program/tests/refresh_obligation.rs | 2 +- token-lending/sdk/src/state/obligation.rs | 8 ++++---- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index c33dde83e49..32c63d58250 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1002,7 +1002,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> let mut deposited_value = Decimal::zero(); let mut borrowed_value = Decimal::zero(); // weighted borrow value wrt borrow weights - let mut true_borrowed_value = Decimal::zero(); + let mut unweighted_borrowed_value = Decimal::zero(); let mut borrowed_value_upper_bound = Decimal::zero(); let mut allowed_borrow_value = Decimal::zero(); let mut unhealthy_borrow_value = Decimal::zero(); @@ -1124,7 +1124,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> borrowed_value.try_add(market_value.try_mul(borrow_reserve.borrow_weight())?)?; borrowed_value_upper_bound = borrowed_value_upper_bound .try_add(market_value_upper_bound.try_mul(borrow_reserve.borrow_weight())?)?; - true_borrowed_value = true_borrowed_value.try_add(market_value)?; + unweighted_borrowed_value = unweighted_borrowed_value.try_add(market_value)?; } if account_info_iter.peek().is_some() { @@ -1134,7 +1134,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> obligation.deposited_value = deposited_value; obligation.borrowed_value = borrowed_value; - obligation.true_borrowed_value = true_borrowed_value; + obligation.unweighted_borrowed_value = unweighted_borrowed_value; obligation.borrowed_value_upper_bound = borrowed_value_upper_bound; obligation.borrowing_isolated_asset = borrowing_isolated_asset; @@ -1201,7 +1201,7 @@ fn update_borrow_attribution_values( if obligation.deposited_value > Decimal::zero() { collateral.attributed_borrow_value = collateral .market_value - .try_mul(obligation.true_borrowed_value)? + .try_mul(obligation.unweighted_borrowed_value)? .try_div(obligation.deposited_value)? } else { collateral.attributed_borrow_value = Decimal::zero(); @@ -1856,8 +1856,8 @@ fn process_borrow_obligation_liquidity( .try_mul(borrow_reserve.borrow_weight())?, )?; - obligation.true_borrowed_value = obligation - .true_borrowed_value + obligation.unweighted_borrowed_value = obligation + .unweighted_borrowed_value .try_add(borrow_reserve.market_value(borrow_amount)?)?; 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 65e87a6df7a..9f23189461b 100644 --- a/token-lending/program/tests/borrow_obligation_liquidity.rs +++ b/token-lending/program/tests/borrow_obligation_liquidity.rs @@ -268,7 +268,7 @@ async fn test_success() { }], deposited_value: Decimal::from(100u64), borrowed_value: borrow_value, - true_borrowed_value: borrow_value, + unweighted_borrowed_value: borrow_value, allowed_borrow_value: Decimal::from(50u64), unhealthy_borrow_value: Decimal::from(55u64), ..obligation.account diff --git a/token-lending/program/tests/forgive_debt.rs b/token-lending/program/tests/forgive_debt.rs index ac658f2d176..63c5c0fb702 100644 --- a/token-lending/program/tests/forgive_debt.rs +++ b/token-lending/program/tests/forgive_debt.rs @@ -177,7 +177,7 @@ async fn test_forgive_debt_success_easy() { borrows: vec![], deposited_value: Decimal::zero(), borrowed_value: Decimal::from(8u64), - true_borrowed_value: Decimal::from(8u64), + unweighted_borrowed_value: Decimal::from(8u64), borrowed_value_upper_bound: Decimal::from(8u64), allowed_borrow_value: Decimal::zero(), unhealthy_borrow_value: Decimal::zero(), diff --git a/token-lending/program/tests/init_obligation.rs b/token-lending/program/tests/init_obligation.rs index b40f6d80245..8788899e6a1 100644 --- a/token-lending/program/tests/init_obligation.rs +++ b/token-lending/program/tests/init_obligation.rs @@ -45,7 +45,7 @@ async fn test_success() { borrows: Vec::new(), deposited_value: Decimal::zero(), borrowed_value: Decimal::zero(), - true_borrowed_value: Decimal::zero(), + unweighted_borrowed_value: Decimal::zero(), borrowed_value_upper_bound: Decimal::zero(), allowed_borrow_value: Decimal::zero(), unhealthy_borrow_value: Decimal::zero(), diff --git a/token-lending/program/tests/isolated_tier_assets.rs b/token-lending/program/tests/isolated_tier_assets.rs index 880fba6f378..e6615ce95cc 100644 --- a/token-lending/program/tests/isolated_tier_assets.rs +++ b/token-lending/program/tests/isolated_tier_assets.rs @@ -124,7 +124,7 @@ async fn test_refresh_obligation() { market_value: Decimal::from(10u64), }], borrowed_value: Decimal::from(10u64), - true_borrowed_value: Decimal::from(10u64), + unweighted_borrowed_value: Decimal::from(10u64), borrowed_value_upper_bound: Decimal::from(10u64), borrowing_isolated_asset: true, ..obligations[0].account.clone() diff --git a/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs b/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs index 55060b97cca..19f31a5fd6b 100644 --- a/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs +++ b/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs @@ -231,7 +231,7 @@ async fn test_success_new() { .to_vec(), deposited_value: Decimal::from(100_000u64), borrowed_value: Decimal::from(55_000u64), - true_borrowed_value: Decimal::from(55_000u64), + unweighted_borrowed_value: Decimal::from(55_000u64), borrowed_value_upper_bound: Decimal::from(55_000u64), allowed_borrow_value: Decimal::from(50_000u64), unhealthy_borrow_value: Decimal::from(55_000u64), diff --git a/token-lending/program/tests/refresh_obligation.rs b/token-lending/program/tests/refresh_obligation.rs index 6f5fde1f197..b16a0f3a519 100644 --- a/token-lending/program/tests/refresh_obligation.rs +++ b/token-lending/program/tests/refresh_obligation.rs @@ -274,7 +274,7 @@ async fn test_success() { .try_div(Decimal::from(LAMPORTS_PER_SOL)) .unwrap(), - true_borrowed_value: new_borrowed_amount_wads + unweighted_borrowed_value: new_borrowed_amount_wads .try_mul(Decimal::from(10u64)) .unwrap() .try_div(Decimal::from(LAMPORTS_PER_SOL)) diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs index 758be5a76ea..68a8b8f102a 100644 --- a/token-lending/sdk/src/state/obligation.rs +++ b/token-lending/sdk/src/state/obligation.rs @@ -41,7 +41,7 @@ pub struct Obligation { /// ie sum(b.borrowed_amount * b.current_spot_price * b.borrow_weight for b in borrows) pub borrowed_value: Decimal, /// True borrow value. Ie, not risk adjusted like "borrowed_value" - pub true_borrowed_value: Decimal, + pub unweighted_borrowed_value: Decimal, /// Risk-adjusted upper bound market value of borrows. /// ie sum(b.borrowed_amount * max(b.current_spot_price, b.smoothed_price) * b.borrow_weight for b in borrows) pub borrowed_value_upper_bound: Decimal, @@ -475,7 +475,7 @@ impl Pack for Obligation { self.super_unhealthy_borrow_value, super_unhealthy_borrow_value, ); - pack_decimal(self.true_borrowed_value, true_borrowed_value); + pack_decimal(self.unweighted_borrowed_value, true_borrowed_value); *deposits_len = u8::try_from(self.deposits.len()).unwrap().to_le_bytes(); *borrows_len = u8::try_from(self.borrows.len()).unwrap().to_le_bytes(); @@ -626,7 +626,7 @@ impl Pack for Obligation { borrows, deposited_value: unpack_decimal(deposited_value), borrowed_value: unpack_decimal(borrowed_value), - true_borrowed_value: unpack_decimal(true_borrowed_value), + unweighted_borrowed_value: unpack_decimal(true_borrowed_value), borrowed_value_upper_bound: unpack_decimal(borrowed_value_upper_bound), allowed_borrow_value: unpack_decimal(allowed_borrow_value), unhealthy_borrow_value: unpack_decimal(unhealthy_borrow_value), @@ -676,7 +676,7 @@ mod test { }], deposited_value: rand_decimal(), borrowed_value: rand_decimal(), - true_borrowed_value: rand_decimal(), + unweighted_borrowed_value: rand_decimal(), borrowed_value_upper_bound: rand_decimal(), allowed_borrow_value: rand_decimal(), unhealthy_borrow_value: rand_decimal(), From e1b3978d96b4ce72e932986158a088b4bbfa7213 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Wed, 29 Nov 2023 14:48:38 -0500 Subject: [PATCH 23/25] account for program upgrade in borrow attribution calculation --- token-lending/program/src/processor.rs | 10 +- .../program/tests/attributed_borrows.rs | 123 +++++++++++++++++- .../program/tests/init_obligation.rs | 3 +- token-lending/sdk/src/state/obligation.rs | 26 +++- 4 files changed, 151 insertions(+), 11 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 32c63d58250..6349d8fbe87 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1194,9 +1194,11 @@ fn update_borrow_attribution_values( return Err(LendingError::InvalidAccountInput.into()); } - deposit_reserve.attributed_borrow_value = deposit_reserve - .attributed_borrow_value - .saturating_sub(collateral.attributed_borrow_value); + if obligation.updated_borrow_attribution_after_upgrade { + deposit_reserve.attributed_borrow_value = deposit_reserve + .attributed_borrow_value + .saturating_sub(collateral.attributed_borrow_value); + } if obligation.deposited_value > Decimal::zero() { collateral.attributed_borrow_value = collateral @@ -1226,6 +1228,8 @@ fn update_borrow_attribution_values( Reserve::pack(deposit_reserve, &mut deposit_reserve_info.data.borrow_mut())?; } + obligation.updated_borrow_attribution_after_upgrade = true; + Ok(()) } diff --git a/token-lending/program/tests/attributed_borrows.rs b/token-lending/program/tests/attributed_borrows.rs index 197473f349f..2e2851d9e9a 100644 --- a/token-lending/program/tests/attributed_borrows.rs +++ b/token-lending/program/tests/attributed_borrows.rs @@ -1,8 +1,17 @@ #![cfg(feature = "test-bpf")] +use solana_sdk::compute_budget::ComputeBudgetInstruction; +use solend_sdk::instruction::refresh_obligation; + use crate::solend_program_test::custom_scenario; +use crate::solend_program_test::SolendProgramTest; use crate::solend_program_test::User; +use solana_sdk::pubkey::Pubkey; use solend_program::math::TryDiv; +use solend_program::processor::process_instruction; +use solend_sdk::state::ObligationCollateral; +use solend_sdk::state::ObligationLiquidity; +use solend_sdk::state::PROGRAM_VERSION; use solana_sdk::instruction::InstructionError; use solana_sdk::transaction::TransactionError; @@ -30,7 +39,7 @@ use solana_program_test::*; #[tokio::test] async fn test_refresh_obligation() { - let (mut test, lending_market, reserves, obligations, users, lending_market_owner) = + let (mut test, lending_market, reserves, obligations, _users, lending_market_owner) = custom_scenario( &[ ReserveArgs { @@ -851,3 +860,115 @@ async fn test_liquidate() { Decimal::zero() ); } + +#[tokio::test] +async fn test_calculation_on_program_upgrade() { + let mut test = ProgramTest::new( + "solend_program", + solend_program::id(), + processor!(process_instruction), + ); + + let reserve_1 = Reserve { + version: PROGRAM_VERSION, + last_update: LastUpdate { + slot: 1, + stale: false, + }, + attributed_borrow_value: Decimal::from(10u64), + liquidity: ReserveLiquidity { + market_price: Decimal::from(10u64), + mint_decimals: 0, + ..ReserveLiquidity::default() + }, + ..Reserve::default() + }; + let reserve_1_pubkey = Pubkey::new_unique(); + + test.add_packable_account( + reserve_1_pubkey, + u32::MAX as u64, + &reserve_1, + &solend_program::id(), + ); + + let reserve_2 = Reserve { + version: PROGRAM_VERSION, + last_update: LastUpdate { + slot: 1, + stale: false, + }, + liquidity: ReserveLiquidity { + market_price: Decimal::from(10u64), + mint_decimals: 0, + ..ReserveLiquidity::default() + }, + ..Reserve::default() + }; + let reserve_2_pubkey = Pubkey::new_unique(); + test.add_packable_account( + reserve_2_pubkey, + u32::MAX as u64, + &reserve_2, + &solend_program::id(), + ); + + let obligation_pubkey = Pubkey::new_unique(); + let obligation = Obligation { + version: PROGRAM_VERSION, + deposits: vec![ObligationCollateral { + deposit_reserve: reserve_1_pubkey, + deposited_amount: 2u64, + market_value: Decimal::from(20u64), + attributed_borrow_value: Decimal::from(10u64), + }], + borrows: vec![ObligationLiquidity { + borrow_reserve: reserve_2_pubkey, + borrowed_amount_wads: Decimal::from(1u64), + ..ObligationLiquidity::default() + }], + updated_borrow_attribution_after_upgrade: false, + ..Obligation::default() + }; + + test.add_packable_account( + obligation_pubkey, + u32::MAX as u64, + &obligation, + &solend_program::id(), + ); + + let mut test = SolendProgramTest::start_with_test(test).await; + + let ix = [refresh_obligation( + solend_program::id(), + obligation_pubkey, + vec![reserve_1_pubkey, reserve_2_pubkey], + )]; + + test.process_transaction(&ix, None).await.unwrap(); + + let reserve_1 = test.load_account::(reserve_1_pubkey).await; + assert_eq!( + reserve_1.account.attributed_borrow_value, + Decimal::from(20u64) + ); + + // run it again, this time make sure the borrow attribution value gets correctly subtracted + let ix = [ + ComputeBudgetInstruction::set_compute_unit_price(1), + refresh_obligation( + solend_program::id(), + obligation_pubkey, + vec![reserve_1_pubkey, reserve_2_pubkey], + ), + ]; + + test.process_transaction(&ix, None).await.unwrap(); + + let reserve_1 = test.load_account::(reserve_1_pubkey).await; + assert_eq!( + reserve_1.account.attributed_borrow_value, + Decimal::from(20u64) + ); +} diff --git a/token-lending/program/tests/init_obligation.rs b/token-lending/program/tests/init_obligation.rs index 8788899e6a1..bc802bc3954 100644 --- a/token-lending/program/tests/init_obligation.rs +++ b/token-lending/program/tests/init_obligation.rs @@ -50,7 +50,8 @@ async fn test_success() { allowed_borrow_value: Decimal::zero(), unhealthy_borrow_value: Decimal::zero(), super_unhealthy_borrow_value: Decimal::zero(), - borrowing_isolated_asset: false + borrowing_isolated_asset: false, + updated_borrow_attribution_after_upgrade: false } ); } diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs index 68a8b8f102a..b6b49efc332 100644 --- a/token-lending/sdk/src/state/obligation.rs +++ b/token-lending/sdk/src/state/obligation.rs @@ -61,6 +61,8 @@ pub struct Obligation { pub super_unhealthy_borrow_value: Decimal, /// True if the obligation is currently borrowing an isolated tier asset pub borrowing_isolated_asset: bool, + /// Updated borrow attribution after upgrade. initially false when upgrading to v2.0.3 + pub updated_borrow_attribution_after_upgrade: bool, } impl Obligation { @@ -433,7 +435,8 @@ impl Pack for Obligation { borrowed_value_upper_bound, borrowing_isolated_asset, super_unhealthy_borrow_value, - true_borrowed_value, + unweighted_borrowed_value, + updated_borrow_attribution_after_upgrade, _padding, deposits_len, borrows_len, @@ -453,7 +456,8 @@ impl Pack for Obligation { 1, 16, 16, - 15, + 1, + 14, 1, 1, OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) @@ -475,7 +479,11 @@ impl Pack for Obligation { self.super_unhealthy_borrow_value, super_unhealthy_borrow_value, ); - pack_decimal(self.unweighted_borrowed_value, true_borrowed_value); + pack_decimal(self.unweighted_borrowed_value, unweighted_borrowed_value); + pack_bool( + self.updated_borrow_attribution_after_upgrade, + updated_borrow_attribution_after_upgrade, + ); *deposits_len = u8::try_from(self.deposits.len()).unwrap().to_le_bytes(); *borrows_len = u8::try_from(self.borrows.len()).unwrap().to_le_bytes(); @@ -539,7 +547,8 @@ impl Pack for Obligation { borrowed_value_upper_bound, borrowing_isolated_asset, super_unhealthy_borrow_value, - true_borrowed_value, + unweighted_borrowed_value, + updated_borrow_attribution_after_upgrade, _padding, deposits_len, borrows_len, @@ -559,7 +568,8 @@ impl Pack for Obligation { 1, 16, 16, - 15, + 1, + 14, 1, 1, OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) @@ -626,12 +636,15 @@ impl Pack for Obligation { borrows, deposited_value: unpack_decimal(deposited_value), borrowed_value: unpack_decimal(borrowed_value), - unweighted_borrowed_value: unpack_decimal(true_borrowed_value), + unweighted_borrowed_value: unpack_decimal(unweighted_borrowed_value), borrowed_value_upper_bound: unpack_decimal(borrowed_value_upper_bound), allowed_borrow_value: unpack_decimal(allowed_borrow_value), unhealthy_borrow_value: unpack_decimal(unhealthy_borrow_value), super_unhealthy_borrow_value: unpack_decimal(super_unhealthy_borrow_value), borrowing_isolated_asset: unpack_bool(borrowing_isolated_asset)?, + updated_borrow_attribution_after_upgrade: unpack_bool( + updated_borrow_attribution_after_upgrade, + )?, }) } } @@ -682,6 +695,7 @@ mod test { unhealthy_borrow_value: rand_decimal(), super_unhealthy_borrow_value: rand_decimal(), borrowing_isolated_asset: rng.gen(), + updated_borrow_attribution_after_upgrade: rng.gen(), }; let mut packed = [0u8; OBLIGATION_LEN]; From 57a24c12435d89cddc32622eebeb164ed47b67e7 Mon Sep 17 00:00:00 2001 From: 0xripleys <105607696+0xripleys@users.noreply.github.com> Date: Sat, 16 Dec 2023 18:19:44 -0500 Subject: [PATCH 24/25] Collateralization Limits Part 2 (#172) * closable instruction * add more tests around signer * liquidate closeable obligations * test fix * pr fixes * adding a close borrow attribution limit * PR fixes --- token-lending/cli/src/main.rs | 8 +- token-lending/program/src/processor.rs | 164 ++++++++-- .../program/tests/attributed_borrows.rs | 135 +------- token-lending/program/tests/helpers/mod.rs | 35 +- .../tests/helpers/solend_program_test.rs | 27 ++ .../program/tests/init_obligation.rs | 2 +- ...uidate_obligation_and_redeem_collateral.rs | 162 ++++++++- .../tests/mark_obligation_as_closeable.rs | 270 +++++++++++++++ token-lending/program/tests/two_prices.rs | 2 +- token-lending/sdk/src/error.rs | 3 + token-lending/sdk/src/instruction.rs | 85 ++++- token-lending/sdk/src/state/obligation.rs | 19 +- token-lending/sdk/src/state/reserve.rs | 309 +++++++++++++++--- 13 files changed, 983 insertions(+), 238 deletions(-) create mode 100644 token-lending/program/tests/mark_obligation_as_closeable.rs diff --git a/token-lending/cli/src/main.rs b/token-lending/cli/src/main.rs index 89e51f9ad7e..d80a64d666a 100644 --- a/token-lending/cli/src/main.rs +++ b/token-lending/cli/src/main.rs @@ -1143,7 +1143,10 @@ fn main() { let reserve_type = value_of(arg_matches, "reserve_type").unwrap(); let scaled_price_offset_bps = value_of(arg_matches, "scaled_price_offset_bps").unwrap(); let extra_oracle_pubkey = pubkey_of(arg_matches, "extra_oracle_pubkey").unwrap(); - let attributed_borrow_limit = value_of(arg_matches, "attributed_borrow_limit").unwrap(); + let attributed_borrow_limit_open = + value_of(arg_matches, "attributed_borrow_limit_open").unwrap(); + let attributed_borrow_limit_close = + value_of(arg_matches, "attributed_borrow_limit_close").unwrap(); let borrow_fee_wad = (borrow_fee * WAD as f64) as u64; let flash_loan_fee_wad = (flash_loan_fee * WAD as f64) as u64; @@ -1199,7 +1202,8 @@ fn main() { reserve_type, scaled_price_offset_bps, extra_oracle_pubkey: Some(extra_oracle_pubkey), - attributed_borrow_limit, + attributed_borrow_limit_open, + attributed_borrow_limit_close, }, source_liquidity_pubkey, source_liquidity_owner_keypair, diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 6349d8fbe87..33f22e80867 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1,5 +1,6 @@ //! Program state processor +use crate::state::Bonus; use crate::{ self as solend_program, error::LendingError, @@ -15,6 +16,7 @@ use crate::{ }; use bytemuck::bytes_of; use pyth_sdk_solana::{self, state::ProductAccount}; + use solana_program::{ account_info::{next_account_info, AccountInfo}, entrypoint::ProgramResult, @@ -199,6 +201,10 @@ pub fn process_instruction( let metadata = LendingMarketMetadata::new_from_bytes(input)?; process_update_market_metadata(program_id, metadata, accounts) } + LendingInstruction::SetObligationCloseabilityStatus { closeable } => { + msg!("Instruction: Mark Obligation As Closable"); + process_set_obligation_closeability_status(program_id, closeable, accounts) + } } } @@ -1148,7 +1154,10 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> obligation.last_update.update_slot(clock.slot); - update_borrow_attribution_values(&mut obligation, &accounts[1..], false)?; + let (_, close_exceeded) = update_borrow_attribution_values(&mut obligation, &accounts[1..])?; + if close_exceeded.is_none() { + obligation.closeable = false; + } // move the ObligationLiquidity with the max borrow weight to the front if let Some((_, max_borrow_weight_index)) = max_borrow_weight { @@ -1180,10 +1189,12 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> fn update_borrow_attribution_values( obligation: &mut Obligation, deposit_reserve_infos: &[AccountInfo], - error_if_limit_exceeded: bool, -) -> ProgramResult { +) -> Result<(Option, Option), ProgramError> { let deposit_infos = &mut deposit_reserve_infos.iter(); + let mut open_exceeded = None; + let mut close_exceeded = None; + for collateral in obligation.deposits.iter_mut() { let deposit_reserve_info = next_account_info(deposit_infos)?; let mut deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?; @@ -1194,11 +1205,9 @@ fn update_borrow_attribution_values( return Err(LendingError::InvalidAccountInput.into()); } - if obligation.updated_borrow_attribution_after_upgrade { - deposit_reserve.attributed_borrow_value = deposit_reserve - .attributed_borrow_value - .saturating_sub(collateral.attributed_borrow_value); - } + deposit_reserve.attributed_borrow_value = deposit_reserve + .attributed_borrow_value + .saturating_sub(collateral.attributed_borrow_value); if obligation.deposited_value > Decimal::zero() { collateral.attributed_borrow_value = collateral @@ -1213,24 +1222,21 @@ fn update_borrow_attribution_values( .attributed_borrow_value .try_add(collateral.attributed_borrow_value)?; - if error_if_limit_exceeded - && deposit_reserve.attributed_borrow_value - > Decimal::from(deposit_reserve.config.attributed_borrow_limit) + if deposit_reserve.attributed_borrow_value + > Decimal::from(deposit_reserve.config.attributed_borrow_limit_open) { - msg!( - "Attributed borrow value is over the limit for reserve {} and mint {}", - deposit_reserve_info.key, - deposit_reserve.liquidity.mint_pubkey - ); - return Err(LendingError::BorrowAttributionLimitExceeded.into()); + open_exceeded = Some(*deposit_reserve_info.key); + } + if deposit_reserve.attributed_borrow_value + > Decimal::from(deposit_reserve.config.attributed_borrow_limit_close) + { + close_exceeded = Some(*deposit_reserve_info.key); } Reserve::pack(deposit_reserve, &mut deposit_reserve_info.data.borrow_mut())?; } - obligation.updated_borrow_attribution_after_upgrade = true; - - Ok(()) + Ok((open_exceeded, close_exceeded)) } #[inline(never)] // avoid stack frame limit @@ -1617,7 +1623,15 @@ fn _withdraw_obligation_collateral<'a>( .market_value .saturating_sub(withdraw_value); - update_borrow_attribution_values(&mut obligation, deposit_reserve_infos, true)?; + let (open_exceeded, _) = + update_borrow_attribution_values(&mut obligation, deposit_reserve_infos)?; + if let Some(reserve_pubkey) = open_exceeded { + msg!( + "Open borrow attribution limit exceeded for reserve {:?}", + reserve_pubkey + ); + return Err(LendingError::BorrowAttributionLimitExceeded.into()); + } // 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 @@ -1872,8 +1886,16 @@ fn process_borrow_obligation_liquidity( obligation_liquidity.borrow(borrow_amount)?; obligation.last_update.mark_stale(); - update_borrow_attribution_values(&mut obligation, &accounts[9..], true)?; - // HACK: fast forward through the used account info's + let (open_exceeded, _) = update_borrow_attribution_values(&mut obligation, &accounts[9..])?; + if let Some(reserve_pubkey) = open_exceeded { + msg!( + "Open borrow attribution limit exceeded for reserve {:?}", + reserve_pubkey + ); + return Err(LendingError::BorrowAttributionLimitExceeded.into()); + } + + // HACK: fast forward through the deposit reserve infos for _ in 0..obligation.deposits.len() { next_account_info(account_info_iter)?; } @@ -2042,7 +2064,7 @@ fn _liquidate_obligation<'a>( user_transfer_authority_info: &AccountInfo<'a>, clock: &Clock, token_program_id: &AccountInfo<'a>, -) -> Result<(u64, Decimal), ProgramError> { +) -> Result<(u64, Bonus), ProgramError> { let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; if lending_market_info.owner != program_id { msg!("Lending market provided is not owned by the lending program"); @@ -2128,8 +2150,9 @@ fn _liquidate_obligation<'a>( msg!("Obligation borrowed value is zero"); return Err(LendingError::ObligationBorrowsZero.into()); } - if obligation.borrowed_value < obligation.unhealthy_borrow_value { - msg!("Obligation is healthy and cannot be liquidated"); + + if obligation.borrowed_value < obligation.unhealthy_borrow_value && !obligation.closeable { + msg!("Obligation must be unhealthy or marked as closeable to be liquidated"); return Err(LendingError::ObligationHealthy.into()); } @@ -2171,16 +2194,17 @@ fn _liquidate_obligation<'a>( return Err(LendingError::InvalidMarketAuthority.into()); } + let bonus = withdraw_reserve.calculate_bonus(&obligation)?; let CalculateLiquidationResult { settle_amount, repay_amount, withdraw_amount, - bonus_rate, } = withdraw_reserve.calculate_liquidation( liquidity_amount, &obligation, liquidity, collateral, + &bonus, )?; if repay_amount == 0 { @@ -2234,7 +2258,7 @@ fn _liquidate_obligation<'a>( token_program: token_program_id.clone(), })?; - Ok((withdraw_amount, bonus_rate)) + Ok((withdraw_amount, bonus)) } #[inline(never)] // avoid stack frame limit @@ -2266,7 +2290,7 @@ fn process_liquidate_obligation_and_redeem_reserve_collateral( let token_program_id = next_account_info(account_info_iter)?; let clock = &Clock::get()?; - let (withdrawn_collateral_amount, bonus_rate) = _liquidate_obligation( + let (withdrawn_collateral_amount, bonus) = _liquidate_obligation( program_id, liquidity_amount, source_liquidity_info, @@ -2313,7 +2337,7 @@ fn process_liquidate_obligation_and_redeem_reserve_collateral( return Err(LendingError::InvalidAccountInput.into()); } let protocol_fee = withdraw_reserve - .calculate_protocol_liquidation_fee(withdraw_liquidity_amount, bonus_rate)?; + .calculate_protocol_liquidation_fee(withdraw_liquidity_amount, &bonus)?; spl_token_transfer(TokenTransferParams { source: destination_liquidity_info.clone(), @@ -3133,6 +3157,86 @@ fn process_update_market_metadata( Ok(()) } +/// process mark obligation as closable +pub fn process_set_obligation_closeability_status( + program_id: &Pubkey, + closeable: bool, + accounts: &[AccountInfo], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let obligation_info = next_account_info(account_info_iter)?; + let lending_market_info = next_account_info(account_info_iter)?; + let reserve_info = next_account_info(account_info_iter)?; + let signer_info = next_account_info(account_info_iter)?; + let clock = Clock::get()?; + + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + + let reserve = Reserve::unpack(&reserve_info.data.borrow())?; + if reserve_info.owner != program_id { + msg!("Reserve provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + if &reserve.lending_market != lending_market_info.key { + msg!("Reserve lending market does not match the lending market provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + + if reserve.attributed_borrow_value < Decimal::from(reserve.config.attributed_borrow_limit_close) + { + msg!("Reserve attributed borrow value is below the attributed borrow limit"); + return Err(LendingError::BorrowAttributionLimitNotExceeded.into()); + } + + let mut obligation = Obligation::unpack(&obligation_info.data.borrow())?; + if obligation_info.owner != program_id { + msg!("Obligation provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + + if &obligation.lending_market != lending_market_info.key { + msg!("Obligation lending market does not match the lending market provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if obligation.last_update.is_stale(clock.slot)? { + msg!("Obligation is stale and must be refreshed"); + return Err(LendingError::ObligationStale.into()); + } + + if &lending_market.risk_authority != signer_info.key && &lending_market.owner != signer_info.key + { + msg!("Signer must be risk authority or lending market owner"); + return Err(LendingError::InvalidAccountInput.into()); + } + + if !signer_info.is_signer { + msg!("Risk authority or lending market owner must be a signer"); + return Err(LendingError::InvalidSigner.into()); + } + + if obligation.borrowed_value == Decimal::zero() { + msg!("Obligation borrowed value is zero"); + return Err(LendingError::ObligationBorrowsZero.into()); + } + + obligation + .find_collateral_in_deposits(*reserve_info.key) + .map_err(|_| { + msg!("Obligation does not have a deposit for the reserve provided"); + LendingError::ObligationCollateralEmpty + })?; + + obligation.closeable = closeable; + + Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?; + + Ok(()) +} + fn assert_uninitialized( account_info: &AccountInfo, ) -> Result { diff --git a/token-lending/program/tests/attributed_borrows.rs b/token-lending/program/tests/attributed_borrows.rs index 2e2851d9e9a..6d62ccfb657 100644 --- a/token-lending/program/tests/attributed_borrows.rs +++ b/token-lending/program/tests/attributed_borrows.rs @@ -1,17 +1,10 @@ #![cfg(feature = "test-bpf")] -use solana_sdk::compute_budget::ComputeBudgetInstruction; -use solend_sdk::instruction::refresh_obligation; - use crate::solend_program_test::custom_scenario; -use crate::solend_program_test::SolendProgramTest; + use crate::solend_program_test::User; -use solana_sdk::pubkey::Pubkey; + use solend_program::math::TryDiv; -use solend_program::processor::process_instruction; -use solend_sdk::state::ObligationCollateral; -use solend_sdk::state::ObligationLiquidity; -use solend_sdk::state::PROGRAM_VERSION; use solana_sdk::instruction::InstructionError; use solana_sdk::transaction::TransactionError; @@ -157,7 +150,7 @@ async fn test_refresh_obligation() { &lending_market_owner, &reserves[0], ReserveConfig { - attributed_borrow_limit: 1, + attributed_borrow_limit_open: 1, ..reserves[0].account.config }, reserves[0].account.rate_limiter.config, @@ -295,7 +288,7 @@ async fn test_calculations() { &lending_market_owner, &reserves[0], ReserveConfig { - attributed_borrow_limit: 113, + attributed_borrow_limit_open: 113, ..reserves[0].account.config }, reserves[0].account.rate_limiter.config, @@ -333,7 +326,7 @@ async fn test_calculations() { &lending_market_owner, &reserves[0], ReserveConfig { - attributed_borrow_limit: 120, + attributed_borrow_limit_open: 120, ..reserves[0].account.config }, reserves[0].account.rate_limiter.config, @@ -386,7 +379,7 @@ async fn test_calculations() { }, attributed_borrow_value: Decimal::from(120u64), config: ReserveConfig { - attributed_borrow_limit: 120, + attributed_borrow_limit_open: 120, ..usdc_reserve.config }, ..usdc_reserve @@ -619,7 +612,7 @@ async fn test_withdraw() { &lending_market_owner, &reserves[0], ReserveConfig { - attributed_borrow_limit: 6, + attributed_borrow_limit_open: 6, ..reserves[0].account.config }, reserves[0].account.rate_limiter.config, @@ -656,7 +649,7 @@ async fn test_withdraw() { &lending_market_owner, &reserves[0], ReserveConfig { - attributed_borrow_limit: 10, + attributed_borrow_limit_open: 10, ..reserves[0].account.config }, reserves[0].account.rate_limiter.config, @@ -860,115 +853,3 @@ async fn test_liquidate() { Decimal::zero() ); } - -#[tokio::test] -async fn test_calculation_on_program_upgrade() { - let mut test = ProgramTest::new( - "solend_program", - solend_program::id(), - processor!(process_instruction), - ); - - let reserve_1 = Reserve { - version: PROGRAM_VERSION, - last_update: LastUpdate { - slot: 1, - stale: false, - }, - attributed_borrow_value: Decimal::from(10u64), - liquidity: ReserveLiquidity { - market_price: Decimal::from(10u64), - mint_decimals: 0, - ..ReserveLiquidity::default() - }, - ..Reserve::default() - }; - let reserve_1_pubkey = Pubkey::new_unique(); - - test.add_packable_account( - reserve_1_pubkey, - u32::MAX as u64, - &reserve_1, - &solend_program::id(), - ); - - let reserve_2 = Reserve { - version: PROGRAM_VERSION, - last_update: LastUpdate { - slot: 1, - stale: false, - }, - liquidity: ReserveLiquidity { - market_price: Decimal::from(10u64), - mint_decimals: 0, - ..ReserveLiquidity::default() - }, - ..Reserve::default() - }; - let reserve_2_pubkey = Pubkey::new_unique(); - test.add_packable_account( - reserve_2_pubkey, - u32::MAX as u64, - &reserve_2, - &solend_program::id(), - ); - - let obligation_pubkey = Pubkey::new_unique(); - let obligation = Obligation { - version: PROGRAM_VERSION, - deposits: vec![ObligationCollateral { - deposit_reserve: reserve_1_pubkey, - deposited_amount: 2u64, - market_value: Decimal::from(20u64), - attributed_borrow_value: Decimal::from(10u64), - }], - borrows: vec![ObligationLiquidity { - borrow_reserve: reserve_2_pubkey, - borrowed_amount_wads: Decimal::from(1u64), - ..ObligationLiquidity::default() - }], - updated_borrow_attribution_after_upgrade: false, - ..Obligation::default() - }; - - test.add_packable_account( - obligation_pubkey, - u32::MAX as u64, - &obligation, - &solend_program::id(), - ); - - let mut test = SolendProgramTest::start_with_test(test).await; - - let ix = [refresh_obligation( - solend_program::id(), - obligation_pubkey, - vec![reserve_1_pubkey, reserve_2_pubkey], - )]; - - test.process_transaction(&ix, None).await.unwrap(); - - let reserve_1 = test.load_account::(reserve_1_pubkey).await; - assert_eq!( - reserve_1.account.attributed_borrow_value, - Decimal::from(20u64) - ); - - // run it again, this time make sure the borrow attribution value gets correctly subtracted - let ix = [ - ComputeBudgetInstruction::set_compute_unit_price(1), - refresh_obligation( - solend_program::id(), - obligation_pubkey, - vec![reserve_1_pubkey, reserve_2_pubkey], - ), - ]; - - test.process_transaction(&ix, None).await.unwrap(); - - let reserve_1 = test.load_account::(reserve_1_pubkey).await; - assert_eq!( - reserve_1.account.attributed_borrow_value, - Decimal::from(20u64) - ); -} diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index 4eb662e9463..45d4e9af0c1 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -28,6 +28,38 @@ pub const QUOTE_CURRENCY: [u8; 32] = pub const LAMPORTS_TO_SOL: u64 = 1_000_000_000; pub const FRACTIONAL_TO_USDC: u64 = 1_000_000; +pub fn reserve_config_no_fees() -> ReserveConfig { + ReserveConfig { + optimal_utilization_rate: 80, + max_utilization_rate: 80, + loan_to_value_ratio: 50, + liquidation_bonus: 0, + max_liquidation_bonus: 0, + liquidation_threshold: 55, + max_liquidation_threshold: 65, + min_borrow_rate: 0, + optimal_borrow_rate: 0, + max_borrow_rate: 0, + super_max_borrow_rate: 0, + fees: ReserveFees { + borrow_fee_wad: 0, + flash_loan_fee_wad: 0, + host_fee_percentage: 0, + }, + deposit_limit: u64::MAX, + borrow_limit: u64::MAX, + fee_receiver: Keypair::new().pubkey(), + protocol_liquidation_fee: 0, + protocol_take_rate: 0, + added_borrow_weight_bps: 0, + reserve_type: ReserveType::Regular, + scaled_price_offset_bps: 0, + extra_oracle_pubkey: None, + attributed_borrow_limit_open: u64::MAX, + attributed_borrow_limit_close: u64::MAX, + } +} + pub fn test_reserve_config() -> ReserveConfig { ReserveConfig { optimal_utilization_rate: 80, @@ -55,7 +87,8 @@ pub fn test_reserve_config() -> ReserveConfig { reserve_type: ReserveType::Regular, scaled_price_offset_bps: 0, extra_oracle_pubkey: None, - attributed_borrow_limit: u64::MAX, + attributed_borrow_limit_open: u64::MAX, + attributed_borrow_limit_close: u64::MAX, } } diff --git a/token-lending/program/tests/helpers/solend_program_test.rs b/token-lending/program/tests/helpers/solend_program_test.rs index c799d047ade..ed41e80bdad 100644 --- a/token-lending/program/tests/helpers/solend_program_test.rs +++ b/token-lending/program/tests/helpers/solend_program_test.rs @@ -1,4 +1,5 @@ use bytemuck::checked::from_bytes; + use solend_sdk::instruction::*; use solend_sdk::pyth_mainnet; use solend_sdk::state::*; @@ -695,6 +696,32 @@ pub struct SwitchboardPriceArgs { } impl Info { + pub async fn set_obligation_closeability_status( + &self, + test: &mut SolendProgramTest, + obligation: &Info, + reserve: &Info, + risk_authority: &User, + closeable: bool, + ) -> Result<(), BanksClientError> { + let refresh_ixs = self + .build_refresh_instructions(test, obligation, None) + .await; + test.process_transaction(&refresh_ixs, None).await.unwrap(); + + let ix = vec![set_obligation_closeability_status( + solend_program::id(), + obligation.pubkey, + reserve.pubkey, + self.pubkey, + risk_authority.keypair.pubkey(), + closeable, + )]; + + test.process_transaction(&ix, Some(&[&risk_authority.keypair])) + .await + } + pub async fn deposit( &self, test: &mut SolendProgramTest, diff --git a/token-lending/program/tests/init_obligation.rs b/token-lending/program/tests/init_obligation.rs index bc802bc3954..943f5768d6a 100644 --- a/token-lending/program/tests/init_obligation.rs +++ b/token-lending/program/tests/init_obligation.rs @@ -51,7 +51,7 @@ async fn test_success() { unhealthy_borrow_value: Decimal::zero(), super_unhealthy_borrow_value: Decimal::zero(), borrowing_isolated_asset: false, - updated_borrow_attribution_after_upgrade: false + closeable: false, } ); } diff --git a/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs b/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs index 19f31a5fd6b..009fe817e8c 100644 --- a/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs +++ b/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs @@ -15,6 +15,7 @@ use solend_program::state::ObligationCollateral; use solend_program::state::ObligationLiquidity; use solend_program::state::ReserveConfig; use solend_program::state::ReserveFees; +use solend_sdk::state::Bonus; use solend_sdk::NULL_PUBKEY; mod helpers; @@ -458,7 +459,12 @@ async fn test_success_insufficient_liquidity() { .account .calculate_protocol_liquidation_fee( available_amount * FRACTIONAL_TO_USDC, - Decimal::from_percent(105), + &Bonus { + total_bonus: Decimal::from_percent(bonus as u8), + protocol_liquidation_fee: Decimal::from_deca_bps( + usdc_reserve.account.config.protocol_liquidation_fee, + ), + }, ) .unwrap(); @@ -660,3 +666,157 @@ async fn test_liquidity_ordering() { .await .unwrap(); } + +#[tokio::test] +async fn test_liquidate_closeable_obligation() { + let (mut test, lending_market, reserves, obligations, _users, lending_market_owner) = + custom_scenario( + &[ + ReserveArgs { + mint: usdc_mint::id(), + config: ReserveConfig { + liquidation_bonus: 5, + max_liquidation_bonus: 10, + protocol_liquidation_fee: 1, + ..reserve_config_no_fees() + }, + 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: reserve_config_no_fees(), + liquidity_amount: LAMPORTS_PER_SOL, + price: PriceArgs { + price: 10, + conf: 0, + expo: 0, + ema_price: 10, + ema_conf: 0, + }, + }, + ], + &[ObligationArgs { + deposits: vec![(usdc_mint::id(), 20 * FRACTIONAL_TO_USDC)], + borrows: vec![(wsol_mint::id(), LAMPORTS_PER_SOL)], + }], + ) + .await; + + let usdc_reserve = reserves + .iter() + .find(|r| r.account.liquidity.mint_pubkey == usdc_mint::id()) + .unwrap(); + let wsol_reserve = reserves + .iter() + .find(|r| r.account.liquidity.mint_pubkey == wsol_mint::id()) + .unwrap(); + + let liquidator = User::new_with_balances( + &mut test, + &[ + (&wsol_mint::id(), 100 * LAMPORTS_TO_SOL), + (&usdc_reserve.account.collateral.mint_pubkey, 0), + (&usdc_mint::id(), 0), + ], + ) + .await; + + let balance_checker = + BalanceChecker::start(&mut test, &[usdc_reserve, &liquidator, wsol_reserve]).await; + + lending_market + .update_reserve_config( + &mut test, + &lending_market_owner, + usdc_reserve, + ReserveConfig { + attributed_borrow_limit_open: 1, + attributed_borrow_limit_close: 1, + ..usdc_reserve.account.config + }, + usdc_reserve.account.rate_limiter.config, + None, + ) + .await + .unwrap(); + + lending_market + .set_obligation_closeability_status( + &mut test, + &obligations[0], + usdc_reserve, + &lending_market_owner, + true, + ) + .await + .unwrap(); + + test.advance_clock_by_slots(1).await; + + lending_market + .liquidate_obligation_and_redeem_reserve_collateral( + &mut test, + wsol_reserve, + usdc_reserve, + &obligations[0], + &liquidator, + u64::MAX, + ) + .await + .unwrap(); + + let (balance_changes, mint_supply_changes) = + balance_checker.find_balance_changes(&mut test).await; + + let expected_balance_changes = HashSet::from([ + // liquidator + TokenBalanceChange { + token_account: liquidator.get_account(&usdc_mint::id()).unwrap(), + mint: usdc_mint::id(), + diff: (2 * FRACTIONAL_TO_USDC - 1) as i128, + }, + TokenBalanceChange { + token_account: liquidator.get_account(&wsol_mint::id()).unwrap(), + mint: wsol_mint::id(), + diff: -((LAMPORTS_PER_SOL / 5) as i128), + }, + // usdc reserve + TokenBalanceChange { + token_account: usdc_reserve.account.collateral.supply_pubkey, + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: -((2 * FRACTIONAL_TO_USDC) as i128), + }, + TokenBalanceChange { + token_account: usdc_reserve.account.liquidity.supply_pubkey, + mint: usdc_mint::id(), + diff: -((2 * FRACTIONAL_TO_USDC) as i128), + }, + TokenBalanceChange { + token_account: usdc_reserve.account.config.fee_receiver, + mint: usdc_mint::id(), + diff: 1, + }, + // wsol reserve + TokenBalanceChange { + token_account: wsol_reserve.account.liquidity.supply_pubkey, + mint: wsol_mint::id(), + diff: (LAMPORTS_TO_SOL / 5) as i128, + }, + ]); + assert_eq!(balance_changes, expected_balance_changes); + + assert_eq!( + mint_supply_changes, + HashSet::from([MintSupplyChange { + mint: usdc_reserve.account.collateral.mint_pubkey, + diff: -((2 * FRACTIONAL_TO_USDC) as i128) + }]) + ); +} diff --git a/token-lending/program/tests/mark_obligation_as_closeable.rs b/token-lending/program/tests/mark_obligation_as_closeable.rs new file mode 100644 index 00000000000..16b81c45ff5 --- /dev/null +++ b/token-lending/program/tests/mark_obligation_as_closeable.rs @@ -0,0 +1,270 @@ +#![cfg(feature = "test-bpf")] + +use crate::solend_program_test::custom_scenario; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, +}; + +use crate::solend_program_test::User; + +use solana_sdk::signer::keypair::Keypair; +use solana_sdk::signer::Signer; + +use crate::solend_program_test::ObligationArgs; +use crate::solend_program_test::PriceArgs; +use crate::solend_program_test::ReserveArgs; + +use solana_program::native_token::LAMPORTS_PER_SOL; +use solana_sdk::instruction::InstructionError; +use solana_sdk::transaction::TransactionError; +use solend_program::error::LendingError; + +use solend_program::state::ReserveConfig; + +use solend_sdk::{instruction::LendingInstruction, solend_mainnet, state::*}; +mod helpers; + +use helpers::*; +use solana_program_test::*; + +#[tokio::test] +async fn test_mark_obligation_as_closeable_success() { + let (mut test, lending_market, reserves, obligations, _users, lending_market_owner) = + custom_scenario( + &[ + ReserveArgs { + mint: usdc_mint::id(), + config: reserve_config_no_fees(), + 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: reserve_config_no_fees(), + liquidity_amount: LAMPORTS_PER_SOL, + price: PriceArgs { + price: 10, + conf: 0, + expo: 0, + ema_price: 10, + ema_conf: 0, + }, + }, + ], + &[ObligationArgs { + deposits: vec![(usdc_mint::id(), 20 * FRACTIONAL_TO_USDC)], + borrows: vec![(wsol_mint::id(), LAMPORTS_PER_SOL)], + }], + ) + .await; + + let risk_authority = User::new_with_keypair(Keypair::new()); + lending_market + .set_lending_market_owner_and_config( + &mut test, + &lending_market_owner, + &lending_market_owner.keypair.pubkey(), + lending_market.account.rate_limiter.config, + lending_market.account.whitelisted_liquidator, + risk_authority.keypair.pubkey(), + ) + .await + .unwrap(); + + test.advance_clock_by_slots(1).await; + + let err = lending_market + .set_obligation_closeability_status( + &mut test, + &obligations[0], + &reserves[0], + &risk_authority, + true, + ) + .await + .unwrap_err() + .unwrap(); + + assert_eq!( + err, + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::BorrowAttributionLimitNotExceeded as u32) + ) + ); + + test.advance_clock_by_slots(1).await; + + lending_market + .update_reserve_config( + &mut test, + &lending_market_owner, + &reserves[0], + ReserveConfig { + attributed_borrow_limit_open: 1, + attributed_borrow_limit_close: 1, + ..reserves[0].account.config + }, + reserves[0].account.rate_limiter.config, + None, + ) + .await + .unwrap(); + + lending_market + .set_obligation_closeability_status( + &mut test, + &obligations[0], + &reserves[0], + &risk_authority, + true, + ) + .await + .unwrap(); + + let obligation_post = test.load_account::(obligations[0].pubkey).await; + assert_eq!( + obligation_post.account, + Obligation { + last_update: LastUpdate { + slot: 1002, + stale: false + }, + closeable: true, + ..obligations[0].account.clone() + } + ); +} + +#[tokio::test] +async fn invalid_signer() { + let (mut test, lending_market, reserves, obligations, _users, lending_market_owner) = + custom_scenario( + &[ + ReserveArgs { + mint: usdc_mint::id(), + config: reserve_config_no_fees(), + 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: reserve_config_no_fees(), + liquidity_amount: LAMPORTS_PER_SOL, + price: PriceArgs { + price: 10, + conf: 0, + expo: 0, + ema_price: 10, + ema_conf: 0, + }, + }, + ], + &[ObligationArgs { + deposits: vec![(usdc_mint::id(), 20 * FRACTIONAL_TO_USDC)], + borrows: vec![(wsol_mint::id(), LAMPORTS_PER_SOL)], + }], + ) + .await; + + let risk_authority = User::new_with_keypair(Keypair::new()); + lending_market + .set_lending_market_owner_and_config( + &mut test, + &lending_market_owner, + &lending_market_owner.keypair.pubkey(), + lending_market.account.rate_limiter.config, + lending_market.account.whitelisted_liquidator, + risk_authority.keypair.pubkey(), + ) + .await + .unwrap(); + + lending_market + .update_reserve_config( + &mut test, + &lending_market_owner, + &reserves[0], + ReserveConfig { + attributed_borrow_limit_open: 1, + attributed_borrow_limit_close: 1, + ..reserves[0].account.config + }, + reserves[0].account.rate_limiter.config, + None, + ) + .await + .unwrap(); + + let rando = User::new_with_keypair(Keypair::new()); + let err = lending_market + .set_obligation_closeability_status(&mut test, &obligations[0], &reserves[0], &rando, true) + .await + .unwrap_err() + .unwrap(); + + assert_eq!( + err, + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::InvalidAccountInput as u32) + ) + ); + + let err = test + .process_transaction( + &[malicious_set_obligation_closeability_status( + solend_mainnet::id(), + obligations[0].pubkey, + reserves[0].pubkey, + lending_market.pubkey, + risk_authority.keypair.pubkey(), + true, + )], + None, + ) + .await + .unwrap_err() + .unwrap(); + + assert_eq!( + err, + TransactionError::InstructionError( + 0, + InstructionError::Custom(LendingError::InvalidSigner as u32) + ) + ); +} + +pub fn malicious_set_obligation_closeability_status( + program_id: Pubkey, + obligation_pubkey: Pubkey, + reserve_pubkey: Pubkey, + lending_market_pubkey: Pubkey, + risk_authority: Pubkey, + closeable: bool, +) -> Instruction { + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(obligation_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(reserve_pubkey, false), + AccountMeta::new_readonly(risk_authority, false), + ], + data: LendingInstruction::SetObligationCloseabilityStatus { closeable }.pack(), + } +} diff --git a/token-lending/program/tests/two_prices.rs b/token-lending/program/tests/two_prices.rs index cbd224fb899..463b562fb06 100644 --- a/token-lending/program/tests/two_prices.rs +++ b/token-lending/program/tests/two_prices.rs @@ -478,7 +478,7 @@ async fn test_liquidation_doesnt_use_smoothed_price() { TokenBalanceChange { token_account: liquidator.get_account(&usdc_mint::id()).unwrap(), mint: usdc_mint::id(), - diff: (20 * FRACTIONAL_TO_USDC * 105 / 100) as i128 - 1, + diff: (20 * FRACTIONAL_TO_USDC * 105 / 100 - 1) as i128, }, TokenBalanceChange { token_account: liquidator.get_account(&wsol_mint::id()).unwrap(), diff --git a/token-lending/sdk/src/error.rs b/token-lending/sdk/src/error.rs index 8666d57e238..597521cd91c 100644 --- a/token-lending/sdk/src/error.rs +++ b/token-lending/sdk/src/error.rs @@ -206,6 +206,9 @@ pub enum LendingError { /// Borrow Attribution Limit Exceeded #[error("Borrow Attribution Limit Exceeded")] BorrowAttributionLimitExceeded, + /// Borrow Attribution Limit Not Exceeded + #[error("Borrow Attribution Limit Not Exceeded")] + BorrowAttributionLimitNotExceeded, } impl From for ProgramError { diff --git a/token-lending/sdk/src/instruction.rs b/token-lending/sdk/src/instruction.rs index d18cc9ac793..a993adf83a9 100644 --- a/token-lending/sdk/src/instruction.rs +++ b/token-lending/sdk/src/instruction.rs @@ -498,6 +498,19 @@ pub enum LendingInstruction { /// Must be a pda with seeds [lending_market, "MetaData"] /// 3. `[]` System program UpdateMarketMetadata, + + // 23 + /// MarkObligationAsClosable + /// + /// Accounts expected by this instruction + /// 0. `[writable]` Obligation account - refreshed. + /// 1. `[]` Lending market account. + /// 2. `[]` Reserve account - refreshed. + /// 3. `[signer]` risk authority of lending market or lending market owner + SetObligationCloseabilityStatus { + /// Obligation is closable + closeable: bool, + }, } impl LendingInstruction { @@ -571,7 +584,8 @@ impl LendingInstruction { } _ => return Err(LendingError::InstructionUnpackError.into()), }; - let (attributed_borrow_limit, _rest) = Self::unpack_u64(rest)?; + let (attributed_borrow_limit_open, rest) = Self::unpack_u64(rest)?; + let (attributed_borrow_limit_close, _rest) = Self::unpack_u64(rest)?; Self::InitReserve { liquidity_amount, config: ReserveConfig { @@ -600,7 +614,8 @@ impl LendingInstruction { reserve_type: ReserveType::from_u8(asset_type).unwrap(), scaled_price_offset_bps, extra_oracle_pubkey, - attributed_borrow_limit, + attributed_borrow_limit_open, + attributed_borrow_limit_close, }, } } @@ -678,7 +693,8 @@ impl LendingInstruction { } _ => return Err(LendingError::InstructionUnpackError.into()), }; - let (attributed_borrow_limit, rest) = Self::unpack_u64(rest)?; + let (attributed_borrow_limit_open, rest) = Self::unpack_u64(rest)?; + let (attributed_borrow_limit_close, rest) = Self::unpack_u64(rest)?; let (window_duration, rest) = Self::unpack_u64(rest)?; let (max_outflow, _rest) = Self::unpack_u64(rest)?; @@ -709,7 +725,8 @@ impl LendingInstruction { reserve_type: ReserveType::from_u8(asset_type).unwrap(), scaled_price_offset_bps, extra_oracle_pubkey, - attributed_borrow_limit, + attributed_borrow_limit_open, + attributed_borrow_limit_close, }, rate_limiter_config: RateLimiterConfig { window_duration, @@ -739,6 +756,15 @@ impl LendingInstruction { Self::ForgiveDebt { liquidity_amount } } 22 => Self::UpdateMarketMetadata, + 23 => { + let (closeable, _rest) = match Self::unpack_u8(rest)? { + (0, rest) => (false, rest), + (1, rest) => (true, rest), + _ => return Err(LendingError::InstructionUnpackError.into()), + }; + + Self::SetObligationCloseabilityStatus { closeable } + } _ => { msg!("Instruction cannot be unpacked"); return Err(LendingError::InstructionUnpackError.into()); @@ -875,7 +901,8 @@ impl LendingInstruction { reserve_type: asset_type, scaled_price_offset_bps, extra_oracle_pubkey, - attributed_borrow_limit, + attributed_borrow_limit_open, + attributed_borrow_limit_close, }, } => { buf.push(2); @@ -911,7 +938,8 @@ impl LendingInstruction { buf.push(0); } }; - buf.extend_from_slice(&attributed_borrow_limit.to_le_bytes()); + buf.extend_from_slice(&attributed_borrow_limit_open.to_le_bytes()); + buf.extend_from_slice(&attributed_borrow_limit_close.to_le_bytes()); } Self::RefreshReserve => { buf.push(3); @@ -998,7 +1026,8 @@ impl LendingInstruction { buf.push(0); } }; - buf.extend_from_slice(&config.attributed_borrow_limit.to_le_bytes()); + buf.extend_from_slice(&config.attributed_borrow_limit_open.to_le_bytes()); + buf.extend_from_slice(&config.attributed_borrow_limit_close.to_le_bytes()); buf.extend_from_slice(&rate_limiter_config.window_duration.to_le_bytes()); buf.extend_from_slice(&rate_limiter_config.max_outflow.to_le_bytes()); } @@ -1027,6 +1056,10 @@ impl LendingInstruction { } // special handling for this instruction, bc the instruction is too big to deserialize Self::UpdateMarketMetadata => {} + Self::SetObligationCloseabilityStatus { closeable } => { + buf.push(23); + buf.extend_from_slice(&(closeable as u8).to_le_bytes()); + } } buf } @@ -1794,6 +1827,27 @@ pub fn update_market_metadata( } } +/// Creates a `MarkObligationAsClosable` instruction +pub fn set_obligation_closeability_status( + program_id: Pubkey, + obligation_pubkey: Pubkey, + reserve_pubkey: Pubkey, + lending_market_pubkey: Pubkey, + risk_authority: Pubkey, + closeable: bool, +) -> Instruction { + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(obligation_pubkey, false), + AccountMeta::new_readonly(lending_market_pubkey, false), + AccountMeta::new_readonly(reserve_pubkey, false), + AccountMeta::new_readonly(risk_authority, true), + ], + data: LendingInstruction::SetObligationCloseabilityStatus { closeable }.pack(), + } +} + #[cfg(test)] mod test { use super::*; @@ -1869,7 +1923,8 @@ mod test { } else { Some(Pubkey::new_unique()) }, - attributed_borrow_limit: rng.gen() + attributed_borrow_limit_open: rng.gen(), + attributed_borrow_limit_close: rng.gen(), }, }; @@ -2036,7 +2091,8 @@ mod test { } else { None }, - attributed_borrow_limit: rng.gen() + attributed_borrow_limit_open: rng.gen(), + attributed_borrow_limit_close: rng.gen(), }, rate_limiter_config: RateLimiterConfig { window_duration: rng.gen::(), @@ -2103,6 +2159,17 @@ mod test { let unpacked = LendingInstruction::unpack(&packed).unwrap(); assert_eq!(instruction, unpacked); } + + // MarkObligationAsClosable + { + let instruction = LendingInstruction::SetObligationCloseabilityStatus { + closeable: rng.gen(), + }; + + let packed = instruction.pack(); + let unpacked = LendingInstruction::unpack(&packed).unwrap(); + assert_eq!(instruction, unpacked); + } } } } diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs index b6b49efc332..573ce3e9fb5 100644 --- a/token-lending/sdk/src/state/obligation.rs +++ b/token-lending/sdk/src/state/obligation.rs @@ -61,8 +61,8 @@ pub struct Obligation { pub super_unhealthy_borrow_value: Decimal, /// True if the obligation is currently borrowing an isolated tier asset pub borrowing_isolated_asset: bool, - /// Updated borrow attribution after upgrade. initially false when upgrading to v2.0.3 - pub updated_borrow_attribution_after_upgrade: bool, + /// Obligation can be marked as closeable + pub closeable: bool, } impl Obligation { @@ -436,7 +436,7 @@ impl Pack for Obligation { borrowing_isolated_asset, super_unhealthy_borrow_value, unweighted_borrowed_value, - updated_borrow_attribution_after_upgrade, + closeable, _padding, deposits_len, borrows_len, @@ -480,10 +480,7 @@ impl Pack for Obligation { super_unhealthy_borrow_value, ); pack_decimal(self.unweighted_borrowed_value, unweighted_borrowed_value); - pack_bool( - self.updated_borrow_attribution_after_upgrade, - updated_borrow_attribution_after_upgrade, - ); + pack_bool(self.closeable, closeable); *deposits_len = u8::try_from(self.deposits.len()).unwrap().to_le_bytes(); *borrows_len = u8::try_from(self.borrows.len()).unwrap().to_le_bytes(); @@ -548,7 +545,7 @@ impl Pack for Obligation { borrowing_isolated_asset, super_unhealthy_borrow_value, unweighted_borrowed_value, - updated_borrow_attribution_after_upgrade, + closeable, _padding, deposits_len, borrows_len, @@ -642,9 +639,7 @@ impl Pack for Obligation { unhealthy_borrow_value: unpack_decimal(unhealthy_borrow_value), super_unhealthy_borrow_value: unpack_decimal(super_unhealthy_borrow_value), borrowing_isolated_asset: unpack_bool(borrowing_isolated_asset)?, - updated_borrow_attribution_after_upgrade: unpack_bool( - updated_borrow_attribution_after_upgrade, - )?, + closeable: unpack_bool(closeable)?, }) } } @@ -695,7 +690,7 @@ mod test { unhealthy_borrow_value: rand_decimal(), super_unhealthy_borrow_value: rand_decimal(), borrowing_isolated_asset: rng.gen(), - updated_borrow_attribution_after_upgrade: rng.gen(), + closeable: rng.gen(), }; let mut packed = [0u8; OBLIGATION_LEN]; diff --git a/token-lending/sdk/src/state/reserve.rs b/token-lending/sdk/src/state/reserve.rs index 6e3ab31648d..9ed5e27f938 100644 --- a/token-lending/sdk/src/state/reserve.rs +++ b/token-lending/sdk/src/state/reserve.rs @@ -370,8 +370,15 @@ impl Reserve { /// Calculate bonus as a percentage /// the value will be in range [0, MAX_BONUS_PCT] - pub fn calculate_bonus(&self, obligation: &Obligation) -> Result { + pub fn calculate_bonus(&self, obligation: &Obligation) -> Result { if obligation.borrowed_value < obligation.unhealthy_borrow_value { + if obligation.closeable { + return Ok(Bonus { + total_bonus: Decimal::zero(), + protocol_liquidation_fee: Decimal::zero(), + }); + } + msg!("Obligation is healthy so a liquidation bonus can't be calculated"); return Err(LendingError::ObligationHealthy.into()); } @@ -383,10 +390,13 @@ impl Reserve { // could also return the average of liquidation bonus and max liquidation bonus here, but // i don't think it matters if obligation.unhealthy_borrow_value == obligation.super_unhealthy_borrow_value { - return Ok(min( - liquidation_bonus.try_add(protocol_liquidation_fee)?, - Decimal::from_percent(MAX_BONUS_PCT), - )); + return Ok(Bonus { + total_bonus: min( + liquidation_bonus.try_add(protocol_liquidation_fee)?, + Decimal::from_percent(MAX_BONUS_PCT), + ), + protocol_liquidation_fee, + }); } // safety: @@ -415,7 +425,10 @@ impl Reserve { .try_add(weight.try_mul(max_liquidation_bonus.try_sub(liquidation_bonus)?)?)? .try_add(protocol_liquidation_fee)?; - Ok(min(bonus, Decimal::from_percent(MAX_BONUS_PCT))) + Ok(Bonus { + total_bonus: min(bonus, Decimal::from_percent(MAX_BONUS_PCT)), + protocol_liquidation_fee, + }) } /// Liquidate some or all of an unhealthy obligation @@ -425,8 +438,14 @@ impl Reserve { obligation: &Obligation, liquidity: &ObligationLiquidity, collateral: &ObligationCollateral, + bonus: &Bonus, ) -> Result { - let bonus_rate = self.calculate_bonus(obligation)?.try_add(Decimal::one())?; + if bonus.total_bonus > Decimal::from_percent(MAX_BONUS_PCT) { + msg!("Bonus rate cannot exceed maximum bonus rate"); + return Err(LendingError::InvalidAmount.into()); + } + + let bonus_rate = bonus.total_bonus.try_add(Decimal::one())?; let max_amount = if amount_to_liquidate == u64::MAX { liquidity.borrowed_amount_wads @@ -517,29 +536,33 @@ impl Reserve { settle_amount, repay_amount, withdraw_amount, - bonus_rate, }) } /// Calculate protocol cut of liquidation bonus always at least 1 lamport - /// the bonus rate is always >=1 and includes both liquidator bonus and protocol fee. + /// the bonus rate is always <= MAX_BONUS_PCT /// the bonus rate has to be passed into this function because bonus calculations are dynamic /// and can't be recalculated after liquidation. pub fn calculate_protocol_liquidation_fee( &self, amount_liquidated: u64, - bonus_rate: Decimal, + bonus: &Bonus, ) -> Result { + if bonus.total_bonus > Decimal::from_percent(MAX_BONUS_PCT) { + msg!("Bonus rate cannot exceed maximum bonus rate"); + return Err(LendingError::InvalidAmount.into()); + } + let amount_liquidated_wads = Decimal::from(amount_liquidated); - let nonbonus_amount = amount_liquidated_wads.try_div(bonus_rate)?; - // After deploying must update all reserves to set liquidation fee then redeploy with this line instead of hardcode - let protocol_fee = std::cmp::max( + let nonbonus_amount = + amount_liquidated_wads.try_div(Decimal::one().try_add(bonus.total_bonus)?)?; + + Ok(std::cmp::max( nonbonus_amount - .try_mul(Decimal::from_deca_bps(self.config.protocol_liquidation_fee))? + .try_mul(bonus.protocol_liquidation_fee)? .try_ceil_u64()?, 1, - ); - Ok(protocol_fee) + )) } /// Calculate protocol fee redemption accounting for availible liquidity and accumulated fees @@ -601,9 +624,17 @@ pub struct CalculateLiquidationResult { pub repay_amount: u64, /// Amount of collateral to withdraw in exchange for repay amount pub withdraw_amount: u64, - /// Liquidator bonus as a percentage, including the protocol fee - /// always greater than or equal to 1. - pub bonus_rate: Decimal, +} + +/// Bonus +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Bonus { + /// Total bonus (liquidator bonus + protocol liquidation fee). 0 <= x <= MAX_BONUS_PCT + /// eg if the total bonus is 5%, this value is 0.05 + pub total_bonus: Decimal, + /// protocol liquidation fee pct. 0 <= x <= reserve.config.protocol_liquidation_fee / 10 + /// eg if the protocol liquidation fee is 1%, this value is 0.01 + pub protocol_liquidation_fee: Decimal, } /// Reserve liquidity @@ -943,8 +974,10 @@ pub struct ReserveConfig { pub scaled_price_offset_bps: i64, /// Extra oracle. Only used to limit borrows and withdrawals. pub extra_oracle_pubkey: Option, - /// Attributed Borrow limit in USD - pub attributed_borrow_limit: u64, + /// Open Attributed Borrow limit in USD + pub attributed_borrow_limit_open: u64, + /// Close Attributed Borrow limit in USD + pub attributed_borrow_limit_close: u64, } /// validates reserve configs @@ -1044,6 +1077,11 @@ pub fn validate_reserve_config(config: ReserveConfig) -> ProgramResult { return Err(LendingError::InvalidConfig.into()); } + if config.attributed_borrow_limit_open > config.attributed_borrow_limit_close { + msg!("open attributed borrow limit must be <= close attributed borrow limit"); + return Err(LendingError::InvalidConfig.into()); + } + Ok(()) } @@ -1235,7 +1273,8 @@ impl Pack for Reserve { liquidity_extra_market_price_flag, liquidity_extra_market_price, attributed_borrow_value, - config_attributed_borrow_limit, + config_attributed_borrow_limit_open, + config_attributed_borrow_limit_close, _padding, ) = mut_array_refs![ output, @@ -1285,7 +1324,8 @@ impl Pack for Reserve { 16, 16, 8, - 57 + 8, + 49 ]; // reserve @@ -1365,7 +1405,10 @@ impl Pack for Reserve { *config_added_borrow_weight_bps = self.config.added_borrow_weight_bps.to_le_bytes(); *config_max_liquidation_bonus = self.config.max_liquidation_bonus.to_le_bytes(); *config_max_liquidation_threshold = self.config.max_liquidation_threshold.to_le_bytes(); - *config_attributed_borrow_limit = self.config.attributed_borrow_limit.to_le_bytes(); + *config_attributed_borrow_limit_open = + self.config.attributed_borrow_limit_open.to_le_bytes(); + *config_attributed_borrow_limit_close = + self.config.attributed_borrow_limit_close.to_le_bytes(); pack_decimal(self.attributed_borrow_value, attributed_borrow_value); } @@ -1420,7 +1463,8 @@ impl Pack for Reserve { liquidity_extra_market_price_flag, liquidity_extra_market_price, attributed_borrow_value, - config_attributed_borrow_limit, + config_attributed_borrow_limit_open, + config_attributed_borrow_limit_close, _padding, ) = array_refs![ input, @@ -1470,7 +1514,8 @@ impl Pack for Reserve { 16, 16, 8, - 57 + 8, + 49 ]; let version = u8::from_le_bytes(*version); @@ -1576,10 +1621,19 @@ impl Pack for Reserve { Some(Pubkey::new_from_array(*config_extra_oracle_pubkey)) }, // this field is added in v2.0.3 and we will never set it to zero. only time it'll + // the following two fields are added in v2.0.3 and we will never set it to zero. only time they will // be zero is when we upgrade from v2.0.2 to v2.0.3. in that case, the correct // thing to do is set the value to u64::MAX. - attributed_borrow_limit: { - let value = u64::from_le_bytes(*config_attributed_borrow_limit); + attributed_borrow_limit_open: { + let value = u64::from_le_bytes(*config_attributed_borrow_limit_open); + if value == 0 { + u64::MAX + } else { + value + } + }, + attributed_borrow_limit_close: { + let value = u64::from_le_bytes(*config_attributed_borrow_limit_close); if value == 0 { u64::MAX } else { @@ -1677,7 +1731,8 @@ mod test { reserve_type: ReserveType::from_u8(rng.gen::() % 2).unwrap(), scaled_price_offset_bps: rng.gen(), extra_oracle_pubkey, - attributed_borrow_limit: rng.gen(), + attributed_borrow_limit_open: rng.gen(), + attributed_borrow_limit_close: rng.gen(), }, rate_limiter: rand_rate_limiter(), attributed_borrow_value: rand_decimal(), @@ -2080,7 +2135,7 @@ mod test { #[test] fn calculate_protocol_liquidation_fee() { - let mut reserve = Reserve { + let reserve = Reserve { config: ReserveConfig { protocol_liquidation_fee: 10, ..Default::default() @@ -2090,18 +2145,55 @@ mod test { assert_eq!( reserve - .calculate_protocol_liquidation_fee(105, Decimal::from_percent(105)) + .calculate_protocol_liquidation_fee( + 105, + &Bonus { + total_bonus: Decimal::from_percent(5), + protocol_liquidation_fee: Decimal::from_percent(1), + } + ) .unwrap(), 1 ); - reserve.config.protocol_liquidation_fee = 20; assert_eq!( reserve - .calculate_protocol_liquidation_fee(105, Decimal::from_percent(105)) + .calculate_protocol_liquidation_fee( + 105, + &Bonus { + total_bonus: Decimal::from_percent(5), + protocol_liquidation_fee: Decimal::from_percent(2), + } + ) .unwrap(), 2 ); + + assert_eq!( + reserve + .calculate_protocol_liquidation_fee( + 10000, + &Bonus { + total_bonus: Decimal::from_percent(5), + protocol_liquidation_fee: Decimal::from_percent(0), + } + ) + .unwrap(), + 1 + ); + + assert_eq!( + reserve + .calculate_protocol_liquidation_fee( + 10000, + &Bonus { + total_bonus: Decimal::from_percent(1), + protocol_liquidation_fee: Decimal::from_percent(1), + } + ) + .unwrap(), + 100 + ); } #[test] @@ -2287,6 +2379,8 @@ mod test { Just(ReserveConfigTestCase { config: ReserveConfig { scaled_price_offset_bps: 1999, + attributed_borrow_limit_open: 50, + attributed_borrow_limit_close: 51, ..ReserveConfig::default() }, result: Ok(()) @@ -2304,6 +2398,14 @@ mod test { ..ReserveConfig::default() }, result: Ok(()) + }), + Just(ReserveConfigTestCase { + config: ReserveConfig { + attributed_borrow_limit_open: 51, + attributed_borrow_limit_close: 50, + ..ReserveConfig::default() + }, + result: Err(LendingError::InvalidConfig.into()), }) ] } @@ -2320,12 +2422,13 @@ mod test { borrowed_value: Decimal, unhealthy_borrow_value: Decimal, super_unhealthy_borrow_value: Decimal, + closeable: bool, liquidation_bonus: u8, max_liquidation_bonus: u8, protocol_liquidation_fee: u8, - result: Result, + result: Result, } fn calculate_bonus_test_cases() -> impl Strategy { @@ -2335,73 +2438,130 @@ mod test { borrowed_value: Decimal::from(100u64), unhealthy_borrow_value: Decimal::from(101u64), super_unhealthy_borrow_value: Decimal::from(150u64), + closeable: false, liquidation_bonus: 10, max_liquidation_bonus: 20, protocol_liquidation_fee: 10, result: Err(LendingError::ObligationHealthy.into()), }), + // healthy but closeable + Just(LiquidationBonusTestCase { + borrowed_value: Decimal::from(100u64), + unhealthy_borrow_value: Decimal::from(101u64), + super_unhealthy_borrow_value: Decimal::from(150u64), + closeable: true, + liquidation_bonus: 10, + max_liquidation_bonus: 20, + protocol_liquidation_fee: 10, + result: Ok(Bonus { + total_bonus: Decimal::zero(), + protocol_liquidation_fee: Decimal::zero() + }), + }), + // unhealthy and also closeable Just(LiquidationBonusTestCase { borrowed_value: Decimal::from(100u64), unhealthy_borrow_value: Decimal::from(100u64), super_unhealthy_borrow_value: Decimal::from(150u64), + closeable: true, liquidation_bonus: 10, max_liquidation_bonus: 20, protocol_liquidation_fee: 10, - result: Ok(Decimal::from_percent(11)) + result: Ok(Bonus { + total_bonus: Decimal::from_percent(11), + protocol_liquidation_fee: Decimal::from_percent(1) + }), + }), + Just(LiquidationBonusTestCase { + borrowed_value: Decimal::from(100u64), + unhealthy_borrow_value: Decimal::from(100u64), + super_unhealthy_borrow_value: Decimal::from(150u64), + closeable: false, + liquidation_bonus: 10, + max_liquidation_bonus: 20, + protocol_liquidation_fee: 10, + result: Ok(Bonus { + total_bonus: Decimal::from_percent(11), + protocol_liquidation_fee: Decimal::from_percent(1) + }), }), Just(LiquidationBonusTestCase { borrowed_value: Decimal::from(100u64), unhealthy_borrow_value: Decimal::from(50u64), super_unhealthy_borrow_value: Decimal::from(150u64), + closeable: false, liquidation_bonus: 10, max_liquidation_bonus: 20, protocol_liquidation_fee: 10, - result: Ok(Decimal::from_percent(16)) + result: Ok(Bonus { + total_bonus: Decimal::from_percent(16), + protocol_liquidation_fee: Decimal::from_percent(1) + }), }), Just(LiquidationBonusTestCase { borrowed_value: Decimal::from(100u64), unhealthy_borrow_value: Decimal::from(50u64), super_unhealthy_borrow_value: Decimal::from(100u64), + closeable: false, liquidation_bonus: 10, max_liquidation_bonus: 20, protocol_liquidation_fee: 10, - result: Ok(Decimal::from_percent(21)) + result: Ok(Bonus { + total_bonus: Decimal::from_percent(21), + protocol_liquidation_fee: Decimal::from_percent(1) + }), }), Just(LiquidationBonusTestCase { borrowed_value: Decimal::from(200u64), unhealthy_borrow_value: Decimal::from(50u64), super_unhealthy_borrow_value: Decimal::from(100u64), + closeable: false, liquidation_bonus: 10, max_liquidation_bonus: 20, protocol_liquidation_fee: 10, - result: Ok(Decimal::from_percent(21)) + result: Ok(Bonus { + total_bonus: Decimal::from_percent(21), + protocol_liquidation_fee: Decimal::from_percent(1) + }), }), Just(LiquidationBonusTestCase { borrowed_value: Decimal::from(60u64), unhealthy_borrow_value: Decimal::from(50u64), super_unhealthy_borrow_value: Decimal::from(50u64), + closeable: false, liquidation_bonus: 10, max_liquidation_bonus: 20, protocol_liquidation_fee: 10, - result: Ok(Decimal::from_percent(11)) + result: Ok(Bonus { + total_bonus: Decimal::from_percent(11), + protocol_liquidation_fee: Decimal::from_percent(1) + }), }), Just(LiquidationBonusTestCase { borrowed_value: Decimal::from(60u64), unhealthy_borrow_value: Decimal::from(40u64), super_unhealthy_borrow_value: Decimal::from(60u64), + closeable: false, liquidation_bonus: 10, max_liquidation_bonus: 30, protocol_liquidation_fee: 10, - result: Ok(Decimal::from_percent(25)) + result: Ok(Bonus { + total_bonus: Decimal::from_percent(25), + protocol_liquidation_fee: Decimal::from_percent(1) + }), }), Just(LiquidationBonusTestCase { borrowed_value: Decimal::from(60u64), unhealthy_borrow_value: Decimal::from(40u64), super_unhealthy_borrow_value: Decimal::from(60u64), + closeable: false, liquidation_bonus: 30, max_liquidation_bonus: 30, protocol_liquidation_fee: 30, - result: Ok(Decimal::from_percent(25)) + result: Ok(Bonus { + total_bonus: Decimal::from_percent(25), + protocol_liquidation_fee: Decimal::from_percent(3) + }), }), ] } @@ -2423,6 +2583,7 @@ mod test { borrowed_value: test_case.borrowed_value, unhealthy_borrow_value: test_case.unhealthy_borrow_value, super_unhealthy_borrow_value: test_case.super_unhealthy_borrow_value, + closeable: test_case.closeable, ..Obligation::default() }; @@ -2439,6 +2600,7 @@ mod test { deposit_market_value: Decimal, borrow_amount: u64, borrow_market_value: Decimal, + bonus: Bonus, liquidation_result: CalculateLiquidationResult, } @@ -2459,6 +2621,10 @@ mod test { deposit_market_value: Decimal::from(100u64), borrow_amount: 800, borrow_market_value: Decimal::from(80u64), + bonus: Bonus { + total_bonus: Decimal::from_percent(5), + protocol_liquidation_fee: Decimal::from_percent(1) + }, liquidation_result: CalculateLiquidationResult { settle_amount: close_factor.try_mul(Decimal::from(800u64)).unwrap(), repay_amount: close_factor @@ -2473,7 +2639,6 @@ mod test { .unwrap() .try_floor_u64() .unwrap(), - bonus_rate: liquidation_bonus }, }), // collateral market value == liquidation_value @@ -2484,12 +2649,15 @@ mod test { deposit_market_value: Decimal::from( (8000 * LIQUIDATION_CLOSE_FACTOR as u64) * 105 / 10000 ), + bonus: Bonus { + total_bonus: Decimal::from_percent(5), + protocol_liquidation_fee: Decimal::from_percent(1) + }, liquidation_result: CalculateLiquidationResult { settle_amount: Decimal::from((8000 * LIQUIDATION_CLOSE_FACTOR as u64) / 100), repay_amount: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) / 100, withdraw_amount: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) * 105 / 10000, - bonus_rate: liquidation_bonus }, }), // collateral market value < liquidation_value @@ -2502,6 +2670,10 @@ mod test { deposit_market_value: Decimal::from( (8000 * LIQUIDATION_CLOSE_FACTOR as u64) * 105 / 10000 / 2 ), + bonus: Bonus { + total_bonus: Decimal::from_percent(5), + protocol_liquidation_fee: Decimal::from_percent(1) + }, liquidation_result: CalculateLiquidationResult { settle_amount: Decimal::from( @@ -2509,7 +2681,6 @@ mod test { ), repay_amount: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) / 100 / 2, withdraw_amount: (8000 * LIQUIDATION_CLOSE_FACTOR as u64) * 105 / 10000 / 2, - bonus_rate: liquidation_bonus }, }), // dust ObligationLiquidity where collateral market value > liquidation value @@ -2518,13 +2689,16 @@ mod test { borrow_market_value: Decimal::from_percent(50), deposit_amount: 100, deposit_market_value: Decimal::from(1u64), + bonus: Bonus { + total_bonus: Decimal::from_percent(5), + protocol_liquidation_fee: Decimal::from_percent(1) + }, liquidation_result: CalculateLiquidationResult { settle_amount: Decimal::from(100u64), repay_amount: 100, // $0.5 * 1.05 = $0.525 withdraw_amount: 52, - bonus_rate: liquidation_bonus }, }), // dust ObligationLiquidity where collateral market value == liquidation value @@ -2533,12 +2707,15 @@ mod test { borrow_market_value: Decimal::from(1u64), deposit_amount: 1000, deposit_market_value: Decimal::from_percent(105), + bonus: Bonus { + total_bonus: Decimal::from_percent(5), + protocol_liquidation_fee: Decimal::from_percent(1) + }, liquidation_result: CalculateLiquidationResult { settle_amount: Decimal::from(1u64), repay_amount: 1, withdraw_amount: 1000, - bonus_rate: liquidation_bonus }, }), // dust ObligationLiquidity where collateral market value < liquidation value @@ -2547,12 +2724,15 @@ mod test { borrow_market_value: Decimal::one(), deposit_amount: 10, deposit_market_value: Decimal::from_bps(5250), // $0.525 + bonus: Bonus { + total_bonus: Decimal::from_percent(5), + protocol_liquidation_fee: Decimal::from_percent(1) + }, liquidation_result: CalculateLiquidationResult { settle_amount: Decimal::from(5u64), repay_amount: 5, withdraw_amount: 10, - bonus_rate: liquidation_bonus }, }), // dust ObligationLiquidity where collateral market value > liquidation value and the @@ -2562,12 +2742,32 @@ mod test { borrow_market_value: Decimal::one(), deposit_amount: 1, deposit_market_value: Decimal::from(10u64), + bonus: Bonus { + total_bonus: Decimal::from_percent(5), + protocol_liquidation_fee: Decimal::from_percent(1) + }, liquidation_result: CalculateLiquidationResult { settle_amount: Decimal::from(1u64), repay_amount: 1, withdraw_amount: 1, - bonus_rate: liquidation_bonus + }, + }), + // zero bonus rate + Just(LiquidationTestCase { + borrow_amount: 100, + borrow_market_value: Decimal::from(100u64), + deposit_amount: 100, + deposit_market_value: Decimal::from(100u64), + bonus: Bonus { + total_bonus: Decimal::zero(), + protocol_liquidation_fee: Decimal::zero() + }, + + liquidation_result: CalculateLiquidationResult { + settle_amount: Decimal::from(20u64), + repay_amount: 20, + withdraw_amount: 20, }, }), ] @@ -2577,11 +2777,7 @@ mod test { #[test] fn calculate_liquidation(test_case in calculate_liquidation_test_cases()) { let reserve = Reserve { - config: ReserveConfig { - liquidation_bonus: 5, - max_liquidation_bonus: 5, - ..ReserveConfig::default() - }, + config: ReserveConfig::default(), ..Reserve::default() }; @@ -2606,7 +2802,12 @@ mod test { assert_eq!( reserve.calculate_liquidation( - u64::MAX, &obligation, &obligation.borrows[0], &obligation.deposits[0]).unwrap(), + u64::MAX, + &obligation, + &obligation.borrows[0], + &obligation.deposits[0], + &test_case.bonus, + ).unwrap(), test_case.liquidation_result); } } From 55dcdeee143f38c4875f9a970f6d62ea7f2cf3af Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Sun, 17 Dec 2023 16:26:53 -0500 Subject: [PATCH 25/25] clippy --- token-lending/program/src/processor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 33f22e80867..26c05cdd815 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -36,10 +36,10 @@ use solana_program::{ }; use solend_sdk::{ + math::SaturatingSub, oracles::{ get_oracle_type, get_pyth_price_unchecked, validate_pyth_price_account_info, OracleType, }, - math::SaturatingSub, state::{LendingMarketMetadata, RateLimiter, RateLimiterConfig, ReserveType}, }; use solend_sdk::{switchboard_v2_devnet, switchboard_v2_mainnet};