Skip to content

Commit

Permalink
liquidate closeable obligations
Browse files Browse the repository at this point in the history
  • Loading branch information
0xripleys committed Dec 4, 2023
1 parent 3f68f83 commit 009e13a
Show file tree
Hide file tree
Showing 4 changed files with 287 additions and 39 deletions.
46 changes: 31 additions & 15 deletions token-lending/program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1086,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 {
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 @@ -1114,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 @@ -1151,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 @@ -2066,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 @@ -2109,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
Original file line number Diff line number Diff line change
Expand Up @@ -660,3 +660,155 @@ 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,
..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: 1,
..usdc_reserve.account.config
},
usdc_reserve.account.rate_limiter.config,
None,
)
.await
.unwrap();

lending_market
.mark_obligation_as_closable(
&mut test,
&obligations[0],
usdc_reserve,
&lending_market_owner,
10_000,
)
.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)
}])
);
}
5 changes: 5 additions & 0 deletions token-lending/sdk/src/state/obligation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ impl Obligation {
self.borrows = params.borrows;
}

/// Check if obligation is marked to be closeable
pub fn is_closeable(&self, current_slot: Slot) -> bool {
current_slot <= self.closeable_by
}

/// Calculate the current ratio of borrowed value to deposited value
pub fn loan_to_value(&self) -> Result<Decimal, ProgramError> {
self.borrowed_value.try_div(self.deposited_value)
Expand Down
Loading

0 comments on commit 009e13a

Please sign in to comment.