Skip to content

Commit

Permalink
Collateralization Limits Part 2 (#172)
Browse files Browse the repository at this point in the history
* closable instruction

* add more tests around signer

* liquidate closeable obligations

* test fix

* pr fixes

* adding a close borrow attribution limit

* PR fixes
  • Loading branch information
0xripleys authored Dec 16, 2023
1 parent e0807ca commit 922ee1d
Show file tree
Hide file tree
Showing 13 changed files with 996 additions and 239 deletions.
8 changes: 6 additions & 2 deletions token-lending/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1153,7 +1153,10 @@ fn main() {

let added_borrow_weight_bps = value_of(arg_matches, "added_borrow_weight_bps").unwrap();
let reserve_type = value_of(arg_matches, "reserve_type").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();

Check warning on line 1159 in token-lending/cli/src/main.rs

View check run for this annotation

Codecov / codecov/patch

token-lending/cli/src/main.rs#L1156-L1159

Added lines #L1156 - L1159 were not covered by tests

let borrow_fee_wad = (borrow_fee * WAD as f64) as u64;
let flash_loan_fee_wad = (flash_loan_fee * WAD as f64) as u64;
Expand Down Expand Up @@ -1207,7 +1210,8 @@ fn main() {
protocol_take_rate,
added_borrow_weight_bps,
reserve_type,
attributed_borrow_limit,
attributed_borrow_limit_open,
attributed_borrow_limit_close,

Check warning on line 1214 in token-lending/cli/src/main.rs

View check run for this annotation

Codecov / codecov/patch

token-lending/cli/src/main.rs#L1213-L1214

Added lines #L1213 - L1214 were not covered by tests
},
source_liquidity_pubkey,
source_liquidity_owner_keypair,
Expand Down
164 changes: 134 additions & 30 deletions token-lending/program/src/processor.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Program state processor
use crate::state::Bonus;
use crate::{
self as solend_program,
error::LendingError,
Expand All @@ -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,
Expand Down Expand Up @@ -205,6 +207,10 @@ pub fn process_instruction(
msg!("Instruction: Resize Reserve");
process_resize_reserve(program_id, accounts)
}
LendingInstruction::SetObligationCloseabilityStatus { closeable } => {
msg!("Instruction: Mark Obligation As Closable");
process_set_obligation_closeability_status(program_id, closeable, accounts)
}
}
}

Expand Down Expand Up @@ -1081,7 +1087,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 {
Expand Down Expand Up @@ -1113,10 +1122,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<Pubkey>, Option<Pubkey>), 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())?;
Expand All @@ -1127,11 +1138,9 @@ fn update_borrow_attribution_values(
return Err(LendingError::InvalidAccountInput.into());

Check warning on line 1138 in token-lending/program/src/processor.rs

View check run for this annotation

Codecov / codecov/patch

token-lending/program/src/processor.rs#L1137-L1138

Added lines #L1137 - L1138 were not covered by tests
}

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
Expand All @@ -1146,24 +1155,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
Expand Down Expand Up @@ -1550,7 +1556,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
Expand Down Expand Up @@ -1805,8 +1819,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)?;
}
Expand Down Expand Up @@ -1975,7 +1997,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");
Expand Down Expand Up @@ -2061,8 +2083,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());
}

Expand Down Expand Up @@ -2104,16 +2127,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 {
Expand Down Expand Up @@ -2168,7 +2192,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
Expand Down Expand Up @@ -2200,7 +2224,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,
Expand Down Expand Up @@ -2247,7 +2271,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(),
Expand Down Expand Up @@ -3115,6 +3139,86 @@ pub fn process_resize_reserve(_program_id: &Pubkey, accounts: &[AccountInfo]) ->
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());

Check warning on line 3158 in token-lending/program/src/processor.rs

View check run for this annotation

Codecov / codecov/patch

token-lending/program/src/processor.rs#L3157-L3158

Added lines #L3157 - L3158 were not covered by tests
}

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());

Check warning on line 3164 in token-lending/program/src/processor.rs

View check run for this annotation

Codecov / codecov/patch

token-lending/program/src/processor.rs#L3163-L3164

Added lines #L3163 - L3164 were not covered by tests
}
if &reserve.lending_market != lending_market_info.key {
msg!("Reserve lending market does not match the lending market provided");
return Err(LendingError::InvalidAccountInput.into());

Check warning on line 3168 in token-lending/program/src/processor.rs

View check run for this annotation

Codecov / codecov/patch

token-lending/program/src/processor.rs#L3167-L3168

Added lines #L3167 - L3168 were not covered by tests
}

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());

Check warning on line 3180 in token-lending/program/src/processor.rs

View check run for this annotation

Codecov / codecov/patch

token-lending/program/src/processor.rs#L3179-L3180

Added lines #L3179 - L3180 were not covered by tests
}

if &obligation.lending_market != lending_market_info.key {
msg!("Obligation lending market does not match the lending market provided");
return Err(LendingError::InvalidAccountInput.into());

Check warning on line 3185 in token-lending/program/src/processor.rs

View check run for this annotation

Codecov / codecov/patch

token-lending/program/src/processor.rs#L3184-L3185

Added lines #L3184 - L3185 were not covered by tests
}
if obligation.last_update.is_stale(clock.slot)? {
msg!("Obligation is stale and must be refreshed");
return Err(LendingError::ObligationStale.into());

Check warning on line 3189 in token-lending/program/src/processor.rs

View check run for this annotation

Codecov / codecov/patch

token-lending/program/src/processor.rs#L3188-L3189

Added lines #L3188 - L3189 were not covered by tests
}

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());

Check warning on line 3205 in token-lending/program/src/processor.rs

View check run for this annotation

Codecov / codecov/patch

token-lending/program/src/processor.rs#L3204-L3205

Added lines #L3204 - L3205 were not covered by tests
}

obligation
.find_collateral_in_deposits(*reserve_info.key)
.map_err(|_| {
msg!("Obligation does not have a deposit for the reserve provided");
LendingError::ObligationCollateralEmpty

Check warning on line 3212 in token-lending/program/src/processor.rs

View check run for this annotation

Codecov / codecov/patch

token-lending/program/src/processor.rs#L3211-L3212

Added lines #L3211 - L3212 were not covered by tests
})?;

obligation.closeable = closeable;

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

Ok(())
}

fn assert_uninitialized<T: Pack + IsInitialized>(
account_info: &AccountInfo,
) -> Result<T, ProgramError> {
Expand Down
Loading

0 comments on commit 922ee1d

Please sign in to comment.