Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collateralization Limits Part 2 #172

Merged
merged 7 commits into from
Dec 16, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 114 additions & 15 deletions token-lending/program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::{
};
use bytemuck::bytes_of;
use pyth_sdk_solana::{self, state::ProductAccount};
use solana_program::slot_history::Slot;
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
Expand Down Expand Up @@ -205,6 +206,10 @@ pub fn process_instruction(
msg!("Instruction: Resize Reserve");
process_resize_reserve(program_id, accounts)
}
LendingInstruction::MarkObligationAsClosable { closeable_by } => {
msg!("Instruction: Mark Obligation As Closable");
process_mark_obligation_as_closeable(program_id, closeable_by, accounts)
}
}
}

Expand Down Expand Up @@ -1081,7 +1086,13 @@ 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 any_borrow_attribution_limit_exceeded =
update_borrow_attribution_values(&mut obligation, &accounts[1..], false)?;

// unmark obligation as closable after it's been liquidated enough times
if obligation.is_closeable(clock.slot) && !any_borrow_attribution_limit_exceeded {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

close vs open attribution limits

obligation.closeable_by = 0;
}

// move the ObligationLiquidity with the max borrow weight to the front
if let Some((_, max_borrow_weight_index)) = max_borrow_weight {
Expand Down Expand Up @@ -1109,13 +1120,16 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) ->
/// - the obligation's deposited_value must be refreshed
/// - the obligation's true_borrowed_value must be refreshed
///
/// Returns true if any of the borrow attribution limits were exceeded
///
/// Note that this function packs and unpacks deposit reserves.
fn update_borrow_attribution_values(
obligation: &mut Obligation,
deposit_reserve_infos: &[AccountInfo],
error_if_limit_exceeded: bool,
) -> ProgramResult {
) -> Result<bool, ProgramError> {
let deposit_infos = &mut deposit_reserve_infos.iter();
let mut any_attribution_limit_exceeded = false;

for collateral in obligation.deposits.iter_mut() {
let deposit_reserve_info = next_account_info(deposit_infos)?;
Expand Down Expand Up @@ -1146,24 +1160,27 @@ 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)
{
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());
any_attribution_limit_exceeded = true;

if error_if_limit_exceeded {
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());
}
}

Reserve::pack(deposit_reserve, &mut deposit_reserve_info.data.borrow_mut())?;
}

obligation.updated_borrow_attribution_after_upgrade = true;

Ok(())
Ok(any_attribution_limit_exceeded)
}

#[inline(never)] // avoid stack frame limit
Expand Down Expand Up @@ -2061,8 +2078,11 @@ 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.is_closeable(clock.slot)
{
msg!("Obligation must be unhealthy or marked as closeable to be liquidated");
return Err(LendingError::ObligationHealthy.into());
}

Expand Down Expand Up @@ -2104,16 +2124,17 @@ fn _liquidate_obligation<'a>(
return Err(LendingError::InvalidMarketAuthority.into());
}

let bonus_rate = withdraw_reserve.calculate_bonus(&obligation, clock.slot)?;
let CalculateLiquidationResult {
settle_amount,
repay_amount,
withdraw_amount,
bonus_rate,
} = withdraw_reserve.calculate_liquidation(
liquidity_amount,
&obligation,
liquidity,
collateral,
bonus_rate,
)?;

if repay_amount == 0 {
Expand Down Expand Up @@ -3115,6 +3136,84 @@ pub fn process_resize_reserve(_program_id: &Pubkey, accounts: &[AccountInfo]) ->
Ok(())
}

/// process mark obligation as closable
pub fn process_mark_obligation_as_closeable(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ig remove expiry time and change this to "set obligation close ability status"

program_id: &Pubkey,
closeable_by: Slot,
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 risk_authority_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) {
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 != risk_authority_info.key {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or owner

msg!("Lending market risk authority does not match the risk authority provided");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update msg

return Err(LendingError::InvalidAccountInput.into());
}

if !risk_authority_info.is_signer {
msg!("Risk authority provided 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_by = closeable_by;

Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;

Ok(())
}

fn assert_uninitialized<T: Pack + IsInitialized>(
account_info: &AccountInfo,
) -> Result<T, ProgramError> {
Expand Down
29 changes: 29 additions & 0 deletions token-lending/program/tests/helpers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,35 @@ 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,
attributed_borrow_limit: u64::MAX,
}
}

pub fn test_reserve_config() -> ReserveConfig {
ReserveConfig {
optimal_utilization_rate: 80,
Expand Down
27 changes: 27 additions & 0 deletions token-lending/program/tests/helpers/solend_program_test.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use bytemuck::checked::from_bytes;
use solana_sdk::slot_history::Slot;
use solend_sdk::instruction::*;
use solend_sdk::state::*;

Expand Down Expand Up @@ -679,6 +680,32 @@ pub struct SwitchboardPriceArgs {
}

impl Info<LendingMarket> {
pub async fn mark_obligation_as_closable(
&self,
test: &mut SolendProgramTest,
obligation: &Info<Obligation>,
reserve: &Info<Reserve>,
risk_authority: &User,
closeable_by: Slot,
) -> Result<(), BanksClientError> {
let refresh_ixs = self
.build_refresh_instructions(test, obligation, None)
.await;
test.process_transaction(&refresh_ixs, None).await.unwrap();

let ix = vec![mark_obligation_as_closeable(
solend_program::id(),
obligation.pubkey,
reserve.pubkey,
self.pubkey,
risk_authority.keypair.pubkey(),
closeable_by,
)];

test.process_transaction(&ix, Some(&[&risk_authority.keypair]))
.await
}

pub async fn deposit(
&self,
test: &mut SolendProgramTest,
Expand Down
3 changes: 2 additions & 1 deletion token-lending/program/tests/init_obligation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ 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
updated_borrow_attribution_after_upgrade: false,
closeable_by: 0,
}
);
}
Expand Down
Loading
Loading