Net borrow limit: Separate out tracking and checking (#534)

That way it's easier to be specific about where the limit should be
checked.
This commit is contained in:
Christian Kamm 2023-04-13 08:56:33 +02:00 committed by GitHub
parent e612be219d
commit da2a7f4e0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 94 additions and 175 deletions

View File

@ -859,7 +859,6 @@ mod tests {
account.ensure_token_position(4).unwrap().0,
I80F48::from(10),
DUMMY_NOW_TS,
DUMMY_PRICE,
)
.unwrap();
@ -943,7 +942,6 @@ mod tests {
account.ensure_token_position(1).unwrap().0,
I80F48::from(testcase.token1),
DUMMY_NOW_TS,
DUMMY_PRICE,
)
.unwrap();
bank2
@ -952,7 +950,6 @@ mod tests {
account.ensure_token_position(4).unwrap().0,
I80F48::from(testcase.token2),
DUMMY_NOW_TS,
DUMMY_PRICE,
)
.unwrap();
bank3
@ -961,7 +958,6 @@ mod tests {
account.ensure_token_position(5).unwrap().0,
I80F48::from(testcase.token3),
DUMMY_NOW_TS,
DUMMY_PRICE,
)
.unwrap();
for (settings, bank) in testcase

View File

@ -59,7 +59,8 @@ impl HealthCache {
let target_amount = amount * price;
let mut source_bank = source_bank.clone();
source_bank.withdraw_with_fee(&mut source_position, amount, now_ts, source_oracle_price)?;
source_bank.withdraw_with_fee(&mut source_position, amount, now_ts)?;
source_bank.check_net_borrows(source_oracle_price)?;
let mut target_bank = target_bank.clone();
target_bank.deposit(&mut target_position, target_amount, now_ts)?;
@ -403,7 +404,8 @@ impl HealthCache {
let mut position = account.token_position(bank.token_index)?.clone();
let mut bank = bank.clone();
bank.withdraw_with_fee(&mut position, amount, now_ts, token.prices.oracle)?;
bank.withdraw_with_fee(&mut position, amount, now_ts)?;
bank.check_net_borrows(token.prices.oracle)?;
let mut resulting_cache = self.clone();
resulting_cache.adjust_token_balance(&bank, -amount)?;
@ -1133,7 +1135,6 @@ mod tests {
account.ensure_token_position(1).unwrap().0,
I80F48::from(100),
DUMMY_NOW_TS,
DUMMY_PRICE,
)
.unwrap();
@ -1216,7 +1217,6 @@ mod tests {
account.ensure_token_position(1).unwrap().0,
I80F48::from(100),
DUMMY_NOW_TS,
DUMMY_PRICE,
)
.unwrap();
bank1
@ -1225,7 +1225,6 @@ mod tests {
account2.ensure_token_position(1).unwrap().0,
I80F48::from(-100),
DUMMY_NOW_TS,
DUMMY_PRICE,
)
.unwrap();

View File

@ -103,12 +103,8 @@ pub fn account_buyback_fees_with_mngo(
dao_mngo_token_position.indexed_position >= I80F48::ZERO,
MangoError::SomeError
);
let in_use = mngo_bank.withdraw_without_fee(
account_mngo_token_position,
max_buyback_mngo,
now_ts,
mngo_oracle_price,
)?;
let in_use =
mngo_bank.withdraw_without_fee(account_mngo_token_position, max_buyback_mngo, now_ts)?;
emit!(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
@ -132,12 +128,8 @@ pub fn account_buyback_fees_with_mngo(
dao_account.ensure_token_position(fees_bank.token_index)?;
let dao_fees = dao_fees_token_position.native(&fees_bank);
assert!(dao_fees >= max_buyback_fees);
let in_use = fees_bank.withdraw_without_fee(
dao_fees_token_position,
max_buyback_fees,
now_ts,
fees_oracle_price,
)?;
let in_use =
fees_bank.withdraw_without_fee(dao_fees_token_position, max_buyback_fees, now_ts)?;
if !in_use {
dao_account.deactivate_token_position_and_log(
dao_fees_raw_token_index,

View File

@ -351,23 +351,22 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
);
}
// Enforce min vault to deposits ratio
if native_after_change < 0 {
let is_active = bank.change_without_fee(
position,
change_amount,
Clock::get()?.unix_timestamp.try_into().unwrap(),
)?;
if !is_active {
deactivated_token_positions.push(change.raw_token_index);
}
if change_amount < 0 && native_after_change < 0 {
let vault_ai = vaults
.iter()
.find(|vault_ai| vault_ai.key == &bank.vault)
.unwrap();
bank.enforce_min_vault_to_deposits_ratio(vault_ai)?;
}
let is_active = bank.change_without_fee(
position,
change.amount - loan_origination_fee,
Clock::get()?.unix_timestamp.try_into().unwrap(),
*oracle_price,
)?;
if !is_active {
deactivated_token_positions.push(change.raw_token_index);
bank.check_net_borrows(*oracle_price)?;
}
bank.flash_loan_approved_amount = 0;

View File

@ -451,12 +451,7 @@ pub(crate) fn liquidation_action(
let liqor_token_position = liqor.token_position_mut(settle_token_index)?.0;
let liqee_token_position = liqee.token_position_mut(settle_token_index)?.0;
settle_bank.deposit(liqee_token_position, token_transfer, now_ts)?;
settle_bank.withdraw_without_fee(
liqor_token_position,
token_transfer,
now_ts,
settle_token_oracle_price,
)?;
settle_bank.withdraw_without_fee(liqor_token_position, token_transfer, now_ts)?;
liqee_health_cache.adjust_token_balance(&settle_bank, token_transfer)?;
}
msg!(
@ -756,16 +751,10 @@ mod tests {
token_p(&mut setup.liqee),
I80F48::from_num(init_liqee_spot),
0,
I80F48::from(1),
)
.unwrap();
settle_bank
.change_without_fee(
token_p(&mut setup.liqor),
I80F48::from_num(1000.0),
0,
I80F48::from(1),
)
.change_without_fee(token_p(&mut setup.liqor), I80F48::from_num(1000.0), 0)
.unwrap();
}
@ -817,20 +806,10 @@ mod tests {
let settle_bank = setup.settle_bank.data();
settle_bank
.change_without_fee(
token_p(&mut setup.liqee),
I80F48::from_num(5.0),
0,
I80F48::from(1),
)
.change_without_fee(token_p(&mut setup.liqee), I80F48::from_num(5.0), 0)
.unwrap();
settle_bank
.change_without_fee(
token_p(&mut setup.liqor),
I80F48::from_num(1000.0),
0,
I80F48::from(1),
)
.change_without_fee(token_p(&mut setup.liqor), I80F48::from_num(1000.0), 0)
.unwrap();
}

View File

@ -125,12 +125,7 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
let liqor_token_position = liqor.token_position_mut(settle_token_index)?.0;
let liqee_token_position = liqee.token_position_mut(settle_token_index)?.0;
settle_bank.deposit(liqor_token_position, settlement, now_ts)?;
settle_bank.withdraw_without_fee(
liqee_token_position,
settlement,
now_ts,
settle_token_oracle_price,
)?;
settle_bank.withdraw_without_fee(liqee_token_position, settlement, now_ts)?;
liqee_health_cache.adjust_token_balance(&settle_bank, -settlement)?;
emit!(PerpLiqNegativePnlOrBankruptcyLog {

View File

@ -96,7 +96,6 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
token_position,
settlement,
Clock::get()?.unix_timestamp.try_into().unwrap(),
settle_token_oracle_price,
)?;
// Update the settled balance on the market itself
perp_market.fees_settled += settlement;

View File

@ -176,12 +176,7 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
// Don't charge loan origination fees on borrows created via settling:
// Even small loan origination fees could accumulate if a perp position is
// settled back and forth repeatedly.
settle_bank.withdraw_without_fee(
b_token_position,
settlement,
now_ts,
settle_token_oracle_price,
)?;
settle_bank.withdraw_without_fee(b_token_position, settlement, now_ts)?;
emit!(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),

View File

@ -250,14 +250,9 @@ pub fn serum3_place_order(
.token_position_mut(payer_bank.token_index)?
.0
.native(&payer_bank);
if withdrawn_from_vault > position_native {
payer_bank.enforce_min_vault_to_deposits_ratio((*ctx.accounts.payer_vault).as_ref())?;
}
// Charge the difference in vault balance to the user's account
let vault_difference = {
let oracle_price =
payer_bank.oracle_price(&AccountInfoRef::borrow(&ctx.accounts.payer_oracle)?, None)?;
apply_vault_difference(
ctx.accounts.account.key(),
&mut account.borrow_mut(),
@ -265,10 +260,16 @@ pub fn serum3_place_order(
&mut payer_bank,
after_vault,
before_vault,
Some(oracle_price),
)?
};
if withdrawn_from_vault > position_native {
let oracle_price =
payer_bank.oracle_price(&AccountInfoRef::borrow(&ctx.accounts.payer_oracle)?, None)?;
payer_bank.enforce_min_vault_to_deposits_ratio((*ctx.accounts.payer_vault).as_ref())?;
payer_bank.check_net_borrows(oracle_price)?;
}
//
// Health check
//
@ -348,7 +349,6 @@ fn apply_vault_difference(
bank: &mut Bank,
vault_after: u64,
vault_before: u64,
oracle_price: Option<I80F48>,
) -> Result<VaultDifference> {
let needed_change = I80F48::from(vault_after) - I80F48::from(vault_before);
@ -358,12 +358,7 @@ fn apply_vault_difference(
if needed_change >= 0 {
bank.deposit(position, needed_change, now_ts)?;
} else {
bank.withdraw_without_fee(
position,
-needed_change,
now_ts,
oracle_price.unwrap(), // required for withdraws
)?;
bank.withdraw_without_fee(position, -needed_change, now_ts)?;
}
let native_after = position.native(bank);
let native_change = native_after - native_before;
@ -470,7 +465,6 @@ pub fn apply_settle_changes(
base_bank,
after_base_vault,
before_base_vault,
None, // guaranteed to deposit into bank
)?;
let quote_difference = apply_vault_difference(
account_pk,
@ -479,7 +473,6 @@ pub fn apply_settle_changes(
quote_bank,
after_quote_vault_adjusted,
before_quote_vault,
None, // guaranteed to deposit into bank
)?;
if let Some(health_cache) = health_cache {

View File

@ -137,12 +137,8 @@ pub fn token_liq_bankruptcy(
// transfer liab from liqee to liqor
let (liqor_liab, liqor_liab_raw_token_index, _) =
liqor.ensure_token_position(liab_token_index)?;
let (liqor_liab_active, loan_origination_fee) = liab_bank.withdraw_with_fee(
liqor_liab,
liab_transfer,
now_ts,
liab_oracle_price,
)?;
let (liqor_liab_active, loan_origination_fee) =
liab_bank.withdraw_with_fee(liqor_liab, liab_transfer, now_ts)?;
// liqor liab
emit!(TokenBalanceLog {

View File

@ -192,12 +192,8 @@ pub(crate) fn liquidation_action(
let (liqor_liab_position, liqor_liab_raw_index, _) =
liqor.ensure_token_position(liab_token_index)?;
let (liqor_liab_active, loan_origination_fee) = liab_bank.withdraw_with_fee(
liqor_liab_position,
liab_transfer,
now_ts,
liab_oracle_price,
)?;
let (liqor_liab_active, loan_origination_fee) =
liab_bank.withdraw_with_fee(liqor_liab_position, liab_transfer, now_ts)?;
let liqor_liab_indexed_position = liqor_liab_position.indexed_position;
let liqee_liab_native_after = liqee_liab_position.native(liab_bank);
@ -211,7 +207,6 @@ pub(crate) fn liquidation_action(
liqee_asset_position,
asset_transfer,
now_ts,
asset_oracle_price,
)?;
let liqee_asset_indexed_position = liqee_asset_position.indexed_position;
let liqee_assets_native_after = liqee_asset_position.native(asset_bank);
@ -444,38 +439,18 @@ mod tests {
{
let asset_bank = setup.asset_bank.data();
asset_bank
.change_without_fee(
asset_p(&mut setup.liqee),
I80F48::from_num(10.0),
0,
I80F48::from(1),
)
.change_without_fee(asset_p(&mut setup.liqee), I80F48::from_num(10.0), 0)
.unwrap();
asset_bank
.change_without_fee(
asset_p(&mut setup.liqor),
I80F48::from_num(1000.0),
0,
I80F48::from(1),
)
.change_without_fee(asset_p(&mut setup.liqor), I80F48::from_num(1000.0), 0)
.unwrap();
let liab_bank = setup.liab_bank.data();
liab_bank
.change_without_fee(
liab_p(&mut setup.liqor),
I80F48::from_num(1000.0),
0,
I80F48::from(1),
)
.change_without_fee(liab_p(&mut setup.liqor), I80F48::from_num(1000.0), 0)
.unwrap();
liab_bank
.change_without_fee(
liab_p(&mut setup.liqee),
I80F48::from_num(-9.0),
0,
I80F48::from(1),
)
.change_without_fee(liab_p(&mut setup.liqee), I80F48::from_num(-9.0), 0)
.unwrap();
}

View File

@ -69,7 +69,6 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
position,
amount_i80f48,
Clock::get()?.unix_timestamp.try_into().unwrap(),
oracle_price,
)?;
// Provide a readable error message in case the vault doesn't have enough tokens
@ -140,10 +139,11 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
});
}
// Enforce min vault to deposits ratio
// Enforce min vault to deposits ratio and net borrow limits
if is_borrow {
ctx.accounts.vault.reload()?;
bank.enforce_min_vault_to_deposits_ratio(ctx.accounts.vault.as_ref())?;
bank.check_net_borrows(oracle_price)?;
}
Ok(())

View File

@ -386,7 +386,6 @@ impl Bank {
position: &mut TokenPosition,
native_amount: I80F48,
now_ts: u64,
oracle_price: I80F48,
) -> Result<bool> {
let (position_is_active, _) = self.withdraw_internal_wrapper(
position,
@ -394,7 +393,6 @@ impl Bank {
false,
!position.is_in_use(),
now_ts,
Some(oracle_price),
)?;
Ok(position_is_active)
@ -408,16 +406,8 @@ impl Bank {
position: &mut TokenPosition,
native_amount: I80F48,
now_ts: u64,
oracle_price: I80F48,
) -> Result<bool> {
self.withdraw_internal_wrapper(
position,
native_amount,
false,
true,
now_ts,
Some(oracle_price),
)
self.withdraw_internal_wrapper(position, native_amount, false, true, now_ts)
.map(|(not_dusted, _)| not_dusted || position.is_in_use())
}
@ -434,16 +424,8 @@ impl Bank {
position: &mut TokenPosition,
native_amount: I80F48,
now_ts: u64,
oracle_price: I80F48,
) -> Result<(bool, I80F48)> {
self.withdraw_internal_wrapper(
position,
native_amount,
true,
!position.is_in_use(),
now_ts,
Some(oracle_price),
)
self.withdraw_internal_wrapper(position, native_amount, true, !position.is_in_use(), now_ts)
}
/// Internal function to withdraw funds
@ -454,7 +436,6 @@ impl Bank {
with_loan_origination_fee: bool,
allow_dusting: bool,
now_ts: u64,
oracle_price: Option<I80F48>,
) -> Result<(bool, I80F48)> {
let opening_indexed_position = position.indexed_position;
let res = self.withdraw_internal(
@ -463,7 +444,6 @@ impl Bank {
with_loan_origination_fee,
allow_dusting,
now_ts,
oracle_price,
);
self.update_cumulative_interest(position, opening_indexed_position);
res
@ -477,7 +457,6 @@ impl Bank {
with_loan_origination_fee: bool,
allow_dusting: bool,
now_ts: u64,
oracle_price: Option<I80F48>,
) -> Result<(bool, I80F48)> {
require_gte!(native_amount, 0);
let native_position = position.native(self);
@ -523,9 +502,6 @@ impl Bank {
// net borrows requires updating in only this case, since other branches of the method deal with
// withdraws and not borrows
self.update_net_borrows(native_amount, now_ts);
if let Some(oracle_price) = oracle_price {
self.check_net_borrows(oracle_price)?;
}
Ok((true, loan_origination_fee))
}
@ -546,7 +522,6 @@ impl Bank {
false,
!position.is_in_use(),
now_ts,
None,
)?;
Ok((position_is_active, loan_origination_fee))
@ -558,12 +533,11 @@ impl Bank {
position: &mut TokenPosition,
native_amount: I80F48,
now_ts: u64,
oracle_price: I80F48,
) -> Result<bool> {
if native_amount >= 0 {
self.deposit(position, native_amount, now_ts)
} else {
self.withdraw_without_fee(position, -native_amount, now_ts, oracle_price)
self.withdraw_without_fee(position, -native_amount, now_ts)
}
}
@ -573,12 +547,11 @@ impl Bank {
position: &mut TokenPosition,
native_amount: I80F48,
now_ts: u64,
oracle_price: I80F48,
) -> Result<(bool, I80F48)> {
if native_amount >= 0 {
Ok((self.deposit(position, native_amount, now_ts)?, I80F48::ZERO))
} else {
self.withdraw_with_fee(position, -native_amount, now_ts, oracle_price)
self.withdraw_with_fee(position, -native_amount, now_ts)
}
}
@ -957,8 +930,7 @@ mod tests {
let change = I80F48::from(change);
let dummy_now_ts = 1 as u64;
let dummy_price = I80F48::ZERO;
let (is_active, _) =
bank.change_with_fee(&mut account, change, dummy_now_ts, dummy_price)?;
let (is_active, _) = bank.change_with_fee(&mut account, change, dummy_now_ts)?;
let mut expected_native = start_native + change;
if expected_native >= 0.0 && expected_native < 1.0 && !is_in_use {
@ -1034,40 +1006,41 @@ mod tests {
let mut account = TokenPosition::default();
bank.change_without_fee(&mut account, I80F48::from(100), 0, price)
bank.change_without_fee(&mut account, I80F48::from(100), 0)
.unwrap();
assert_eq!(bank.net_borrows_in_window, 0);
bank.change_without_fee(&mut account, I80F48::from(-100), 0, price)
bank.change_without_fee(&mut account, I80F48::from(-100), 0)
.unwrap();
assert_eq!(bank.net_borrows_in_window, 0);
account = TokenPosition::default();
bank.change_without_fee(&mut account, I80F48::from(10), 0, price)
bank.change_without_fee(&mut account, I80F48::from(10), 0)
.unwrap();
bank.change_without_fee(&mut account, I80F48::from(-110), 0, price)
bank.change_without_fee(&mut account, I80F48::from(-110), 0)
.unwrap();
assert_eq!(bank.net_borrows_in_window, 100);
bank.change_without_fee(&mut account, I80F48::from(50), 0, price)
bank.change_without_fee(&mut account, I80F48::from(50), 0)
.unwrap();
assert_eq!(bank.net_borrows_in_window, 50);
bank.change_without_fee(&mut account, I80F48::from(100), 0, price)
bank.change_without_fee(&mut account, I80F48::from(100), 0)
.unwrap();
assert_eq!(bank.net_borrows_in_window, 1); // rounding
account = TokenPosition::default();
bank.net_borrows_in_window = 0;
bank.change_without_fee(&mut account, I80F48::from(-450), 0, price)
bank.change_without_fee(&mut account, I80F48::from(-450), 0)
.unwrap();
bank.change_without_fee(&mut account, I80F48::from(-51), 0, price)
.unwrap_err();
bank.change_without_fee(&mut account, I80F48::from(-51), 0)
.unwrap();
bank.check_net_borrows(price).unwrap_err();
account = TokenPosition::default();
bank.net_borrows_in_window = 0;
bank.change_without_fee(&mut account, I80F48::from(-450), 0, price)
bank.change_without_fee(&mut account, I80F48::from(-450), 0)
.unwrap();
bank.change_without_fee(&mut account, I80F48::from(-50), 0, price)
bank.change_without_fee(&mut account, I80F48::from(-50), 0)
.unwrap();
bank.change_without_fee(&mut account, I80F48::from(-50), 101, price)
bank.change_without_fee(&mut account, I80F48::from(-50), 101)
.unwrap();
Ok(())

View File

@ -219,7 +219,11 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
},
)
.await;
assert!(res.is_err());
assert_mango_error(
&res,
MangoError::BankNetBorrowsLimitReached.into(),
"".into(),
);
// succeeds because is not a borrow
send_tx(
@ -308,7 +312,26 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
},
)
.await;
assert!(res.is_err());
assert_mango_error(
&res,
MangoError::BankNetBorrowsLimitReached.into(),
"".into(),
);
// can still withdraw
send_tx(
solana,
TokenWithdrawInstruction {
amount: 4000,
allow_borrow: false,
account: account_0,
owner,
token_account: payer_mint_accounts[0],
bank_index: 0,
},
)
.await
.unwrap();
set_bank_stub_oracle_price(solana, group, &tokens[0], admin, 5.0).await;
@ -325,7 +348,11 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
},
)
.await;
assert!(res.is_err());
assert_mango_error(
&res,
MangoError::BankNetBorrowsLimitReached.into(),
"".into(),
);
// can borrow smaller amounts: (net borrowed 1000 + new borrow 199) * price 5.0 < limit 6000
send_tx(

View File

@ -337,7 +337,8 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
max_settle_amount: u64::MAX,
},
)
.await;
.await
.unwrap();
// No change
{
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;