Deposit limits (#806)
- limit deposits (via deposit, flash loan, tcs)
- limit potential deposits via openbook settle
by restricting placable orders via potential_serum_tokens
- introduce Serum3PlaceOrderV2 for this purpose
- account for new limits in liquidator, max_swap
(cherry picked from commit 42e31ae859
)
This commit is contained in:
parent
f533d65a58
commit
e7bfa4e03e
|
@ -309,7 +309,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
// TODO: This is where we could multiply in the liquidation fee factors
|
||||
let price = source_price / target_price;
|
||||
|
||||
util::max_swap_source(
|
||||
util::max_swap_source_ignoring_limits(
|
||||
self.client,
|
||||
self.account_fetcher,
|
||||
&liqor,
|
||||
|
|
|
@ -26,10 +26,9 @@ use crate::{token_swap_info, util, ErrorTracking};
|
|||
/// making the whole execution fail.
|
||||
const SLIPPAGE_BUFFER: f64 = 0.01; // 1%
|
||||
|
||||
/// If a tcs gets limited due to exhausted net borrows, don't trigger execution if
|
||||
/// the possible value is below this amount. This avoids spamming executions when net
|
||||
/// borrows are exhausted.
|
||||
const NET_BORROW_EXECUTION_THRESHOLD: u64 = 1_000_000; // 1 USD
|
||||
/// If a tcs gets limited due to exhausted net borrows or deposit limits, don't trigger execution if
|
||||
/// the possible value is below this amount. This avoids spamming executions when limits are exhausted.
|
||||
const EXECUTION_THRESHOLD: u64 = 1_000_000; // 1 USD
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
|
@ -440,12 +439,17 @@ impl Context {
|
|||
/// This includes
|
||||
/// - tcs restrictions (remaining buy/sell, create borrows/deposits)
|
||||
/// - reduce only banks
|
||||
/// - net borrow limits on BOTH sides, even though the buy side is technically
|
||||
/// a liqor limitation: the liqor could acquire the token before trying the
|
||||
/// execution... but in practice the liqor will work on margin
|
||||
/// - net borrow limits:
|
||||
/// - the account may borrow the sell token (and the liqor side may not be a repay)
|
||||
/// - the liqor may borrow the buy token (and the account side may not be a repay)
|
||||
/// this is technically a liqor limitation: the liqor could acquire the token before trying the
|
||||
/// execution... but in practice the liqor may work on margin
|
||||
/// - deposit limits:
|
||||
/// - the account may deposit the buy token (while the liqor borrowed it)
|
||||
/// - the liqor may deposit the sell token (while the account borrowed it)
|
||||
///
|
||||
/// Returns Some((native buy amount, native sell amount)) if execution is sensible
|
||||
/// Returns None if the execution should be skipped (due to net borrow limits...)
|
||||
/// Returns None if the execution should be skipped (due to limits)
|
||||
pub fn tcs_max_liqee_execution(
|
||||
&self,
|
||||
account: &MangoAccountValue,
|
||||
|
@ -458,18 +462,18 @@ impl Context {
|
|||
let premium_price = tcs.premium_price(base_price.to_num(), self.now_ts);
|
||||
let maker_price = tcs.maker_price(premium_price);
|
||||
|
||||
let buy_position = account
|
||||
let liqee_buy_position = account
|
||||
.token_position(tcs.buy_token_index)
|
||||
.map(|p| p.native(&buy_bank))
|
||||
.unwrap_or(I80F48::ZERO);
|
||||
let sell_position = account
|
||||
let liqee_sell_position = account
|
||||
.token_position(tcs.sell_token_index)
|
||||
.map(|p| p.native(&sell_bank))
|
||||
.unwrap_or(I80F48::ZERO);
|
||||
|
||||
// this is in "buy token received per sell token given" units
|
||||
let swap_price = I80F48::from_num((1.0 - SLIPPAGE_BUFFER) / maker_price);
|
||||
let max_sell_ignoring_net_borrows = util::max_swap_source_ignore_net_borrows(
|
||||
let max_sell_ignoring_limits = util::max_swap_source_ignoring_limits(
|
||||
&self.mango_client,
|
||||
&self.account_fetcher,
|
||||
account,
|
||||
|
@ -480,41 +484,31 @@ impl Context {
|
|||
)?
|
||||
.floor()
|
||||
.to_num::<u64>()
|
||||
.min(tcs.max_sell_for_position(sell_position, &sell_bank));
|
||||
.min(tcs.max_sell_for_position(liqee_sell_position, &sell_bank));
|
||||
|
||||
let max_buy_ignoring_net_borrows = tcs.max_buy_for_position(buy_position, &buy_bank);
|
||||
let max_buy_ignoring_limits = tcs.max_buy_for_position(liqee_buy_position, &buy_bank);
|
||||
|
||||
// What follows is a complex manual handling of net borrow limits, for the following reason:
|
||||
// What follows is a complex manual handling of net borrow/deposit limits, for
|
||||
// the following reason:
|
||||
// Usually, we want to execute tcs even for small amounts because that will close the
|
||||
// tcs order: either due to full execution or due to the health threshold being reached.
|
||||
//
|
||||
// However, when the net borrow limits are hit, it will not closed when no further execution
|
||||
// is possible, because net borrow limit issues are considered transient. Furthermore, we
|
||||
// don't even want to send a tiny tcs trigger transactions, because there's a good chance we
|
||||
// would then be sending lot of those as oracle prices fluctuate.
|
||||
// However, when the limits are hit, it will not closed when no further execution
|
||||
// is possible, because limit issues are transient. Furthermore, we don't want to send
|
||||
// tiny tcs trigger transactions, because there's a good chance we would then be sending
|
||||
// lot of those as oracle prices fluctuate.
|
||||
//
|
||||
// Thus, we need to detect if the possible execution amount is tiny _because_ of the
|
||||
// net borrow limits. Then skip. If it's tiny for other reasons we can proceed.
|
||||
// limits. Then skip. If it's tiny for other reasons we can proceed.
|
||||
|
||||
fn available_borrows(bank: &Bank, price: I80F48) -> u64 {
|
||||
(bank.remaining_net_borrows_quote(price) / price).clamp_to_u64()
|
||||
}
|
||||
let available_buy_borrows = available_borrows(&buy_bank, buy_token_price);
|
||||
let available_sell_borrows = available_borrows(&sell_bank, sell_token_price);
|
||||
|
||||
// New borrows if max_sell_ignoring_net_borrows was withdrawn on the liqee
|
||||
let sell_borrows = (I80F48::from(max_sell_ignoring_net_borrows)
|
||||
- sell_position.max(I80F48::ZERO))
|
||||
.clamp_to_u64();
|
||||
|
||||
// On the buy side, the liqor might need to borrow
|
||||
let buy_borrows = match self.config.mode {
|
||||
// Do the liqor buy tokens come from deposits or are they borrowed?
|
||||
let mut liqor_buy_borrows = match self.config.mode {
|
||||
Mode::BorrowBuyToken => {
|
||||
// Assume that the liqor has enough buy token if it's collateral
|
||||
if tcs.buy_token_index == self.config.collateral_token_index {
|
||||
0
|
||||
} else {
|
||||
max_buy_ignoring_net_borrows
|
||||
max_buy_ignoring_limits
|
||||
}
|
||||
}
|
||||
Mode::SwapCollateralIntoBuy { .. } => 0,
|
||||
|
@ -525,19 +519,77 @@ impl Context {
|
|||
}
|
||||
};
|
||||
|
||||
// New maximums adjusted for net borrow limits
|
||||
let max_sell =
|
||||
max_sell_ignoring_net_borrows - sell_borrows + sell_borrows.min(available_sell_borrows);
|
||||
let max_buy =
|
||||
max_buy_ignoring_net_borrows - buy_borrows + buy_borrows.min(available_buy_borrows);
|
||||
// First, net borrow limits
|
||||
let max_sell_net_borrows;
|
||||
let max_buy_net_borrows;
|
||||
{
|
||||
fn available_borrows(bank: &Bank, price: I80F48) -> u64 {
|
||||
bank.remaining_net_borrows_quote(price)
|
||||
.saturating_div(price)
|
||||
.clamp_to_u64()
|
||||
}
|
||||
let available_buy_borrows = available_borrows(&buy_bank, buy_token_price);
|
||||
let available_sell_borrows = available_borrows(&sell_bank, sell_token_price);
|
||||
|
||||
let tiny_due_to_net_borrows = {
|
||||
let buy_threshold = I80F48::from(NET_BORROW_EXECUTION_THRESHOLD) / buy_token_price;
|
||||
let sell_threshold = I80F48::from(NET_BORROW_EXECUTION_THRESHOLD) / sell_token_price;
|
||||
max_buy < buy_threshold && max_buy_ignoring_net_borrows > buy_threshold
|
||||
|| max_sell < sell_threshold && max_sell_ignoring_net_borrows > sell_threshold
|
||||
// New borrows if max_sell_ignoring_limits was withdrawn on the liqee
|
||||
// We assume that on the liqor side the position is >= 0, so these are true
|
||||
// new borrows.
|
||||
let sell_borrows = (I80F48::from(max_sell_ignoring_limits)
|
||||
- liqee_sell_position.max(I80F48::ZERO))
|
||||
.ceil()
|
||||
.clamp_to_u64();
|
||||
|
||||
// On the buy side, the liqor might need to borrow, see liqor_buy_borrows.
|
||||
// On the liqee side, the bought tokens may repay a borrow, reducing net borrows again
|
||||
let buy_borrows = (I80F48::from(liqor_buy_borrows)
|
||||
+ liqee_buy_position.min(I80F48::ZERO))
|
||||
.ceil()
|
||||
.clamp_to_u64();
|
||||
|
||||
// New maximums adjusted for net borrow limits
|
||||
max_sell_net_borrows = max_sell_ignoring_limits
|
||||
- (sell_borrows - sell_borrows.min(available_sell_borrows));
|
||||
max_buy_net_borrows =
|
||||
max_buy_ignoring_limits - (buy_borrows - buy_borrows.min(available_buy_borrows));
|
||||
liqor_buy_borrows = liqor_buy_borrows.min(max_buy_net_borrows);
|
||||
}
|
||||
|
||||
// Second, deposit limits
|
||||
let max_sell;
|
||||
let max_buy;
|
||||
{
|
||||
let available_buy_deposits = buy_bank.remaining_deposits_until_limit().clamp_to_u64();
|
||||
let available_sell_deposits = sell_bank.remaining_deposits_until_limit().clamp_to_u64();
|
||||
|
||||
// New deposits on the liqee side (reduced by repaid borrows)
|
||||
let liqee_buy_deposits = (I80F48::from(max_buy_net_borrows)
|
||||
+ liqee_buy_position.min(I80F48::ZERO))
|
||||
.ceil()
|
||||
.clamp_to_u64();
|
||||
// the net new deposits can only be as big as the liqor borrows
|
||||
// (assume no borrows, then deposits only move from liqor to liqee)
|
||||
let buy_deposits = liqee_buy_deposits.min(liqor_buy_borrows);
|
||||
|
||||
// We assume the liqor position is always >= 0, meaning there are new sell token deposits if
|
||||
// the sell token gets borrowed on the liqee side.
|
||||
let sell_deposits = (I80F48::from(max_sell_net_borrows)
|
||||
- liqee_sell_position.max(I80F48::ZERO))
|
||||
.ceil()
|
||||
.clamp_to_u64();
|
||||
|
||||
max_sell =
|
||||
max_sell_net_borrows - (sell_deposits - sell_deposits.min(available_sell_deposits));
|
||||
max_buy =
|
||||
max_buy_net_borrows - (buy_deposits - buy_deposits.min(available_buy_deposits));
|
||||
}
|
||||
|
||||
let tiny_due_to_limits = {
|
||||
let buy_threshold = I80F48::from(EXECUTION_THRESHOLD) / buy_token_price;
|
||||
let sell_threshold = I80F48::from(EXECUTION_THRESHOLD) / sell_token_price;
|
||||
max_buy < buy_threshold && max_buy_ignoring_limits > buy_threshold
|
||||
|| max_sell < sell_threshold && max_sell_ignoring_limits > sell_threshold
|
||||
};
|
||||
if tiny_due_to_net_borrows {
|
||||
if tiny_due_to_limits {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
|
@ -715,7 +767,7 @@ impl Context {
|
|||
.0
|
||||
.native(&buy_bank);
|
||||
let liqor_available_buy_token = match mode {
|
||||
Mode::BorrowBuyToken => util::max_swap_source(
|
||||
Mode::BorrowBuyToken => util::max_swap_source_with_limits(
|
||||
&self.mango_client,
|
||||
&self.account_fetcher,
|
||||
&liqor,
|
||||
|
@ -734,7 +786,7 @@ impl Context {
|
|||
self.token_bank_price_mint(collateral_token_index)?;
|
||||
let buy_per_collateral_price = (collateral_price / buy_token_price)
|
||||
* I80F48::from_num(jupiter_slippage_fraction);
|
||||
let collateral_amount = util::max_swap_source(
|
||||
let collateral_amount = util::max_swap_source_with_limits(
|
||||
&self.mango_client,
|
||||
&self.account_fetcher,
|
||||
&liqor,
|
||||
|
@ -751,7 +803,7 @@ impl Context {
|
|||
// How big can the sell -> buy swap be?
|
||||
let buy_per_sell_price =
|
||||
(I80F48::from(1) / taker_price) * I80F48::from_num(jupiter_slippage_fraction);
|
||||
let max_sell = util::max_swap_source(
|
||||
let max_sell = util::max_swap_source_with_limits(
|
||||
&self.mango_client,
|
||||
&self.account_fetcher,
|
||||
&liqor,
|
||||
|
|
|
@ -38,7 +38,9 @@ pub fn is_perp_market<'a>(
|
|||
}
|
||||
|
||||
/// Convenience wrapper for getting max swap amounts for a token pair
|
||||
pub fn max_swap_source(
|
||||
///
|
||||
/// This applies net borrow and deposit limits, which is useful for true swaps.
|
||||
pub fn max_swap_source_with_limits(
|
||||
client: &MangoClient,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
account: &MangoAccountValue,
|
||||
|
@ -66,7 +68,7 @@ pub fn max_swap_source(
|
|||
let source_price = health_cache.token_info(source).unwrap().prices.oracle;
|
||||
|
||||
let amount = health_cache
|
||||
.max_swap_source_for_health_ratio(
|
||||
.max_swap_source_for_health_ratio_with_limits(
|
||||
&account,
|
||||
&source_bank,
|
||||
source_price,
|
||||
|
@ -79,7 +81,10 @@ pub fn max_swap_source(
|
|||
}
|
||||
|
||||
/// Convenience wrapper for getting max swap amounts for a token pair
|
||||
pub fn max_swap_source_ignore_net_borrows(
|
||||
///
|
||||
/// This is useful for liquidations, which don't increase deposits or net borrows.
|
||||
/// Tcs execution can also increase deposits/net borrows.
|
||||
pub fn max_swap_source_ignoring_limits(
|
||||
client: &MangoClient,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
account: &MangoAccountValue,
|
||||
|
@ -99,17 +104,15 @@ pub fn max_swap_source_ignore_net_borrows(
|
|||
mango_v4_client::health_cache::new_sync(&client.context, account_fetcher, &account)
|
||||
.expect("always ok");
|
||||
|
||||
let mut source_bank: Bank =
|
||||
let source_bank: Bank =
|
||||
account_fetcher.fetch(&client.context.mint_info(source).first_bank())?;
|
||||
source_bank.net_borrow_limit_per_window_quote = -1;
|
||||
let mut target_bank: Bank =
|
||||
let target_bank: Bank =
|
||||
account_fetcher.fetch(&client.context.mint_info(target).first_bank())?;
|
||||
target_bank.net_borrow_limit_per_window_quote = -1;
|
||||
|
||||
let source_price = health_cache.token_info(source).unwrap().prices.oracle;
|
||||
|
||||
let amount = health_cache
|
||||
.max_swap_source_for_health_ratio(
|
||||
.max_swap_source_for_health_ratio_ignoring_limits(
|
||||
&account,
|
||||
&source_bank,
|
||||
source_price,
|
||||
|
|
|
@ -614,6 +614,10 @@
|
|||
{
|
||||
"name": "groupInsuranceFund",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "depositLimit",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -988,6 +992,12 @@
|
|||
{
|
||||
"name": "maintWeightShiftAbort",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "depositLimitOpt",
|
||||
"type": {
|
||||
"option": "u64"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -2357,6 +2367,10 @@
|
|||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "oraclePriceBand",
|
||||
"type": "f32"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -2400,6 +2414,12 @@
|
|||
"type": {
|
||||
"option": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "oraclePriceBandOpt",
|
||||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -2742,6 +2762,9 @@
|
|||
},
|
||||
{
|
||||
"name": "serum3PlaceOrderV2",
|
||||
"docs": [
|
||||
"requires the receiver_bank in the health account list to be writable"
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
|
@ -7266,11 +7289,9 @@
|
|||
"name": "potentialSerumTokens",
|
||||
"docs": [
|
||||
"Largest amount of tokens that might be added the the bank based on",
|
||||
"serum open order execution.",
|
||||
"",
|
||||
"Can be negative with multiple banks, then it'd need to be balanced in the keeper."
|
||||
"serum open order execution."
|
||||
],
|
||||
"type": "i64"
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "maintWeightShiftStart",
|
||||
|
@ -7298,12 +7319,19 @@
|
|||
"defined": "I80F48"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "depositLimit",
|
||||
"docs": [
|
||||
"zero means none, in token native"
|
||||
],
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
2008
|
||||
2000
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -8402,10 +8430,20 @@
|
|||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
5
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "oraclePriceBand",
|
||||
"docs": [
|
||||
"Limit orders must be <= oracle * (1+band) and >= oracle / (1+band)",
|
||||
"",
|
||||
"Zero value is the default due to migration and disables the limit,",
|
||||
"same as f32::MAX."
|
||||
],
|
||||
"type": "f32"
|
||||
},
|
||||
{
|
||||
"name": "registrationTime",
|
||||
"type": "u64"
|
||||
|
@ -13467,6 +13505,16 @@
|
|||
"code": 6060,
|
||||
"name": "HealthAccountBankNotWritable",
|
||||
"msg": "a bank in the health account list should be writable but is not"
|
||||
},
|
||||
{
|
||||
"code": 6061,
|
||||
"name": "Serum3PriceBandExceeded",
|
||||
"msg": "the market does not allow limit orders too far from the current oracle value"
|
||||
},
|
||||
{
|
||||
"code": 6062,
|
||||
"name": "BankDepositLimit",
|
||||
"msg": "deposit crosses the token's deposit limit"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -129,6 +129,8 @@ pub enum MangoError {
|
|||
HealthAccountBankNotWritable,
|
||||
#[msg("the market does not allow limit orders too far from the current oracle value")]
|
||||
Serum3PriceBandExceeded,
|
||||
#[msg("deposit crosses the token's deposit limit")]
|
||||
BankDepositLimit,
|
||||
}
|
||||
|
||||
impl MangoError {
|
||||
|
|
|
@ -42,7 +42,6 @@ impl HealthCache {
|
|||
|
||||
let mut source_bank = source_bank.clone();
|
||||
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)?;
|
||||
|
||||
|
@ -52,7 +51,40 @@ impl HealthCache {
|
|||
Ok(resulting_cache)
|
||||
}
|
||||
|
||||
pub fn max_swap_source_for_health_ratio(
|
||||
fn apply_limits_to_swap(
|
||||
account: &MangoAccountValue,
|
||||
source_bank: &Bank,
|
||||
source_oracle_price: I80F48,
|
||||
target_bank: &Bank,
|
||||
price: I80F48,
|
||||
source_unlimited: I80F48,
|
||||
) -> Result<I80F48> {
|
||||
let source_pos = account
|
||||
.token_position(source_bank.token_index)?
|
||||
.native(source_bank);
|
||||
let target_pos = account
|
||||
.token_position(target_bank.token_index)?
|
||||
.native(target_bank);
|
||||
|
||||
// net borrow limit on source
|
||||
let available_net_borrows = source_bank
|
||||
.remaining_net_borrows_quote(source_oracle_price)
|
||||
.saturating_div(source_oracle_price);
|
||||
let potential_source = source_unlimited
|
||||
.min(available_net_borrows.saturating_add(source_pos.max(I80F48::ZERO)));
|
||||
|
||||
// deposit limit on target
|
||||
let available_deposits = target_bank.remaining_deposits_until_limit();
|
||||
let potential_target_unlimited = potential_source.saturating_mul(price);
|
||||
let potential_target = potential_target_unlimited
|
||||
.min(available_deposits.saturating_add(-target_pos.min(I80F48::ZERO)));
|
||||
|
||||
let source = potential_source.min(potential_target.saturating_div(price));
|
||||
Ok(source)
|
||||
}
|
||||
|
||||
/// Verifies neither the net borrow or deposit limits
|
||||
pub fn max_swap_source_for_health_ratio_ignoring_limits(
|
||||
&self,
|
||||
account: &MangoAccountValue,
|
||||
source_bank: &Bank,
|
||||
|
@ -72,7 +104,7 @@ impl HealthCache {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn max_swap_source_for_health(
|
||||
pub fn max_swap_source_for_health_ratio_with_limits(
|
||||
&self,
|
||||
account: &MangoAccountValue,
|
||||
source_bank: &Bank,
|
||||
|
@ -81,14 +113,23 @@ impl HealthCache {
|
|||
price: I80F48,
|
||||
min_ratio: I80F48,
|
||||
) -> Result<I80F48> {
|
||||
self.max_swap_source_for_health_fn(
|
||||
let source_unlimited = self.max_swap_source_for_health_fn(
|
||||
account,
|
||||
source_bank,
|
||||
source_oracle_price,
|
||||
target_bank,
|
||||
price,
|
||||
min_ratio,
|
||||
|cache| cache.health(HealthType::Init),
|
||||
|cache| cache.health_ratio(HealthType::Init),
|
||||
)?;
|
||||
|
||||
Self::apply_limits_to_swap(
|
||||
account,
|
||||
source_bank,
|
||||
source_oracle_price,
|
||||
target_bank,
|
||||
price,
|
||||
source_unlimited,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -707,7 +748,7 @@ mod tests {
|
|||
assert_eq!(health_cache.health_ratio(HealthType::Init), I80F48::MAX);
|
||||
assert_eq!(
|
||||
health_cache
|
||||
.max_swap_source_for_health_ratio(
|
||||
.max_swap_source_for_health_ratio_with_limits(
|
||||
&account,
|
||||
&banks[0],
|
||||
I80F48::from(1),
|
||||
|
@ -748,7 +789,7 @@ mod tests {
|
|||
|
||||
let swap_price =
|
||||
I80F48::from_num(price_factor) * source_price.oracle / target_price.oracle;
|
||||
let source_amount = c
|
||||
let source_unlimited = c
|
||||
.max_swap_source_for_health_fn(
|
||||
&account,
|
||||
&source_bank,
|
||||
|
@ -759,6 +800,15 @@ mod tests {
|
|||
max_swap_fn,
|
||||
)
|
||||
.unwrap();
|
||||
let source_amount = HealthCache::apply_limits_to_swap(
|
||||
&account,
|
||||
&source_bank,
|
||||
source_price.oracle,
|
||||
&target_bank,
|
||||
swap_price,
|
||||
source_unlimited,
|
||||
)
|
||||
.unwrap();
|
||||
if source_amount == I80F48::MAX {
|
||||
return (f64::MAX, f64::MAX, f64::MAX, f64::MAX);
|
||||
}
|
||||
|
@ -865,10 +915,7 @@ mod tests {
|
|||
}
|
||||
|
||||
// At this unlikely price it's healthy to swap infinitely
|
||||
assert_eq!(
|
||||
find_max_swap(&health_cache, 0, 1, 50.0, 1.5, banks).0,
|
||||
f64::MAX
|
||||
);
|
||||
assert!(find_max_swap(&health_cache, 0, 1, 50.0, 1.5, banks).0 > 1e16);
|
||||
}
|
||||
|
||||
{
|
||||
|
|
|
@ -467,6 +467,10 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
|||
bank.check_net_borrows(*oracle_price)?;
|
||||
}
|
||||
|
||||
if change_amount > 0 && native_after_change > 0 {
|
||||
bank.check_deposit_and_oo_limit()?;
|
||||
}
|
||||
|
||||
bank.flash_loan_approved_amount = 0;
|
||||
bank.flash_loan_token_account_initial = u64::MAX;
|
||||
|
||||
|
|
|
@ -284,7 +284,8 @@ pub fn serum3_place_order(
|
|||
|
||||
let mut payer_bank = ctx.accounts.payer_bank.load_mut()?;
|
||||
|
||||
// Update the tracking in banks
|
||||
// Update the potential token tracking in banks
|
||||
// (for init weight scaling, deposit limit checks)
|
||||
if is_v2_instruction {
|
||||
let mut receiver_bank = receiver_bank_ai.load_mut::<Bank>()?;
|
||||
let (base_bank, quote_bank) = match side {
|
||||
|
@ -296,14 +297,14 @@ pub fn serum3_place_order(
|
|||
update_bank_potential_tokens_payer_only(serum, &mut payer_bank, &after_oo);
|
||||
}
|
||||
|
||||
// Enforce min vault to deposits ratio
|
||||
let withdrawn_from_vault = I80F48::from(before_vault - after_vault);
|
||||
let position_native = account
|
||||
// Track position before withdraw happens
|
||||
let before_position_native = account
|
||||
.token_position_mut(payer_bank.token_index)?
|
||||
.0
|
||||
.native(&payer_bank);
|
||||
|
||||
// Charge the difference in vault balance to the user's account
|
||||
// (must be done before limit checks like deposit limit)
|
||||
let vault_difference = {
|
||||
apply_vault_difference(
|
||||
ctx.accounts.account.key(),
|
||||
|
@ -315,10 +316,25 @@ pub fn serum3_place_order(
|
|||
)?
|
||||
};
|
||||
|
||||
// Deposit limit check: Placing an order can increase deposit limit use on both
|
||||
// the payer and receiver bank. Imagine placing a bid for 500 base @ 0.5: it would
|
||||
// use up 1000 quote and 500 base because either could be deposit on cancel/fill.
|
||||
// This is why this must happen after update_bank_potential_tokens() and any withdraws.
|
||||
{
|
||||
let receiver_bank = receiver_bank_ai.load::<Bank>()?;
|
||||
receiver_bank
|
||||
.check_deposit_and_oo_limit()
|
||||
.with_context(|| std::format!("on {}", receiver_bank.name()))?;
|
||||
payer_bank
|
||||
.check_deposit_and_oo_limit()
|
||||
.with_context(|| std::format!("on {}", payer_bank.name()))?;
|
||||
}
|
||||
|
||||
// Payer bank safety checks like reduce-only, net borrows, vault-to-deposits ratio
|
||||
let payer_bank_oracle =
|
||||
payer_bank.oracle_price(&AccountInfoRef::borrow(&ctx.accounts.payer_oracle)?, None)?;
|
||||
if withdrawn_from_vault > position_native {
|
||||
let withdrawn_from_vault = I80F48::from(before_vault - after_vault);
|
||||
if withdrawn_from_vault > before_position_native {
|
||||
require_msg_typed!(
|
||||
!payer_bank.are_borrows_reduce_only(),
|
||||
MangoError::TokenInReduceOnlyMode,
|
||||
|
|
|
@ -64,8 +64,13 @@ pub fn token_conditional_swap_start(
|
|||
// We allow the incentive to be < 1 native token because of tokens like BTC, where 1 native token
|
||||
// far exceeds the incentive value.
|
||||
let incentive = (I80F48::from(TCS_START_INCENTIVE) / sell_oracle_price)
|
||||
.min(I80F48::from(tcs.remaining_sell()));
|
||||
// However, the tcs tracking is in u64 units. We need to live with the fact of
|
||||
.min(I80F48::from(tcs.remaining_sell()))
|
||||
// Limiting to remaining deposits is too strict, since this could be a deposit
|
||||
// to deposit transfer, but this is good enough to make the incentive deposit
|
||||
// guaranteed to not exceed the limit.
|
||||
.min(sell_bank.remaining_deposits_until_limit())
|
||||
.max(I80F48::ZERO);
|
||||
// The tcs tracking is in u64 units. We need to live with the fact of
|
||||
// not accounting the incentive fee perfectly.
|
||||
let incentive_native = incentive.clamp_to_u64();
|
||||
|
||||
|
@ -73,22 +78,21 @@ pub fn token_conditional_swap_start(
|
|||
let (liqor_sell_token, liqor_sell_raw_index, _) =
|
||||
liqor.ensure_token_position(sell_token_index)?;
|
||||
|
||||
sell_bank.deposit(liqor_sell_token, incentive, now_ts)?;
|
||||
|
||||
// This withdraw might be a borrow, so can fail due to net borrows or reduce-only
|
||||
let liqee_sell_pre_balance = liqee_sell_token.native(sell_bank);
|
||||
sell_bank.withdraw_with_fee(liqee_sell_token, incentive, now_ts)?;
|
||||
sell_bank.checked_transfer_with_fee(
|
||||
liqee_sell_token,
|
||||
incentive,
|
||||
liqor_sell_token,
|
||||
incentive,
|
||||
now_ts,
|
||||
sell_oracle_price,
|
||||
)?;
|
||||
let liqee_sell_post_balance = liqee_sell_token.native(sell_bank);
|
||||
if liqee_sell_post_balance < 0 {
|
||||
require!(
|
||||
tcs.allow_creating_borrows(),
|
||||
MangoError::TokenConditionalSwapCantPayIncentive
|
||||
);
|
||||
require!(
|
||||
!sell_bank.are_borrows_reduce_only(),
|
||||
MangoError::TokenInReduceOnlyMode
|
||||
);
|
||||
sell_bank.check_net_borrows(sell_oracle_price)?;
|
||||
}
|
||||
|
||||
health_cache
|
||||
|
|
|
@ -294,9 +294,15 @@ fn action(
|
|||
|
||||
let (liqee_buy_token, liqee_buy_raw_index) = liqee.token_position_mut(tcs.buy_token_index)?;
|
||||
let (liqor_buy_token, liqor_buy_raw_index) = liqor.token_position_mut(tcs.buy_token_index)?;
|
||||
buy_bank.deposit(liqee_buy_token, buy_token_amount_i80f48, now_ts)?;
|
||||
let liqor_buy_withdraw =
|
||||
buy_bank.withdraw_with_fee(liqor_buy_token, buy_token_amount_i80f48, now_ts)?;
|
||||
let buy_transfer = buy_bank.checked_transfer_with_fee(
|
||||
liqor_buy_token,
|
||||
buy_token_amount_i80f48,
|
||||
liqee_buy_token,
|
||||
buy_token_amount_i80f48,
|
||||
now_ts,
|
||||
buy_token_price,
|
||||
)?;
|
||||
let liqor_buy_active = buy_transfer.source_is_active;
|
||||
|
||||
let post_liqee_buy_token = liqee_buy_token.native(&buy_bank);
|
||||
let post_liqor_buy_token = liqor_buy_token.native(&buy_bank);
|
||||
|
@ -307,28 +313,18 @@ fn action(
|
|||
liqee.token_position_mut(tcs.sell_token_index)?;
|
||||
let (liqor_sell_token, liqor_sell_raw_index) =
|
||||
liqor.token_position_mut(tcs.sell_token_index)?;
|
||||
let liqor_sell_active = sell_bank.deposit(
|
||||
let sell_transfer = sell_bank.checked_transfer_with_fee(
|
||||
liqee_sell_token,
|
||||
I80F48::from(sell_token_amount_from_liqee),
|
||||
liqor_sell_token,
|
||||
I80F48::from(sell_token_amount_to_liqor),
|
||||
now_ts,
|
||||
sell_token_price,
|
||||
)?;
|
||||
let liqee_sell_withdraw = sell_bank.withdraw_with_fee(
|
||||
liqee_sell_token,
|
||||
I80F48::from(sell_token_amount_from_liqee),
|
||||
now_ts,
|
||||
)?;
|
||||
let liqor_sell_active = sell_transfer.target_is_active;
|
||||
|
||||
sell_bank.collected_fees_native += I80F48::from(maker_fee + taker_fee);
|
||||
|
||||
// Check net borrows on both banks.
|
||||
//
|
||||
// While tcs triggering doesn't cause actual tokens to leave the platform, it can increase the amount
|
||||
// of borrows. For instance, if someone with USDC has a tcs to buy SOL and sell BTC, execution would
|
||||
// create BTC borrows (unless the executor had BTC borrows that get repaid by the execution, but
|
||||
// most executors will work on margin)
|
||||
buy_bank.check_net_borrows(buy_token_price)?;
|
||||
sell_bank.check_net_borrows(sell_token_price)?;
|
||||
|
||||
let post_liqee_sell_token = liqee_sell_token.native(&sell_bank);
|
||||
let post_liqor_sell_token = liqor_sell_token.native(&sell_bank);
|
||||
let liqee_sell_indexed_position = liqee_sell_token.indexed_position;
|
||||
|
@ -336,7 +332,7 @@ fn action(
|
|||
|
||||
// With a scanning account retriever, it's safe to deactivate inactive token positions immediately.
|
||||
// Liqee positions can only be deactivated if the tcs is closed (see below).
|
||||
if !liqor_buy_withdraw.position_is_active {
|
||||
if !liqor_buy_active {
|
||||
liqor.deactivate_token_position_and_log(liqor_buy_raw_index, liqor_key);
|
||||
}
|
||||
if !liqor_sell_active {
|
||||
|
@ -382,24 +378,24 @@ fn action(
|
|||
borrow_index: sell_bank.borrow_index.to_bits(),
|
||||
});
|
||||
|
||||
if liqor_buy_withdraw.has_loan() {
|
||||
if buy_transfer.has_loan() {
|
||||
emit_stack(WithdrawLoanLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
mango_account: liqor_key,
|
||||
token_index: tcs.buy_token_index,
|
||||
loan_amount: liqor_buy_withdraw.loan_amount.to_bits(),
|
||||
loan_origination_fee: liqor_buy_withdraw.loan_origination_fee.to_bits(),
|
||||
loan_amount: buy_transfer.loan_amount.to_bits(),
|
||||
loan_origination_fee: buy_transfer.loan_origination_fee.to_bits(),
|
||||
instruction: LoanOriginationFeeInstruction::TokenConditionalSwapTrigger,
|
||||
price: Some(buy_token_price.to_bits()),
|
||||
});
|
||||
}
|
||||
if liqee_sell_withdraw.has_loan() {
|
||||
if sell_transfer.has_loan() {
|
||||
emit_stack(WithdrawLoanLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
mango_account: liqee_key,
|
||||
token_index: tcs.sell_token_index,
|
||||
loan_amount: liqee_sell_withdraw.loan_amount.to_bits(),
|
||||
loan_origination_fee: liqee_sell_withdraw.loan_origination_fee.to_bits(),
|
||||
loan_amount: sell_transfer.loan_amount.to_bits(),
|
||||
loan_origination_fee: sell_transfer.loan_origination_fee.to_bits(),
|
||||
instruction: LoanOriginationFeeInstruction::TokenConditionalSwapTrigger,
|
||||
price: Some(sell_token_price.to_bits()),
|
||||
});
|
||||
|
|
|
@ -90,11 +90,16 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
|
|||
|
||||
// Get the oracle price, even if stale or unconfident: We want to allow users
|
||||
// to deposit to close borrows or do other fixes even if the oracle is bad.
|
||||
let unsafe_oracle_price = oracle_state_unchecked(
|
||||
let unsafe_oracle_state = oracle_state_unchecked(
|
||||
&AccountInfoRef::borrow(self.oracle.as_ref())?,
|
||||
bank.mint_decimals,
|
||||
)?
|
||||
.price;
|
||||
)?;
|
||||
let unsafe_oracle_price = unsafe_oracle_state.price;
|
||||
|
||||
// If increasing total deposits, check deposit limits
|
||||
if indexed_position > 0 {
|
||||
bank.check_deposit_and_oo_limit()?;
|
||||
}
|
||||
|
||||
// Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms)
|
||||
let amount_usd = (amount_i80f48 * unsafe_oracle_price).to_num::<i64>();
|
||||
|
|
|
@ -49,6 +49,8 @@ pub fn token_edit(
|
|||
maint_weight_shift_asset_target_opt: Option<f32>,
|
||||
maint_weight_shift_liab_target_opt: Option<f32>,
|
||||
maint_weight_shift_abort: bool,
|
||||
set_fallback_oracle: bool, // unused, introduced in v0.22
|
||||
deposit_limit_opt: Option<u64>,
|
||||
) -> Result<()> {
|
||||
let group = ctx.accounts.group.load()?;
|
||||
|
||||
|
@ -436,6 +438,16 @@ pub fn token_edit(
|
|||
bank.maint_weight_shift_duration_inv.is_positive(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(deposit_limit) = deposit_limit_opt {
|
||||
msg!(
|
||||
"Deposit limit old {:?}, new {:?}",
|
||||
bank.deposit_limit,
|
||||
deposit_limit
|
||||
);
|
||||
bank.deposit_limit = deposit_limit;
|
||||
require_group_admin = true;
|
||||
}
|
||||
}
|
||||
|
||||
// account constraint #1
|
||||
|
|
|
@ -41,6 +41,7 @@ pub fn token_register(
|
|||
interest_curve_scaling: f32,
|
||||
interest_target_utilization: f32,
|
||||
group_insurance_fund: bool,
|
||||
deposit_limit: u64,
|
||||
) -> Result<()> {
|
||||
// Require token 0 to be in the insurance token
|
||||
if token_index == INSURANCE_TOKEN_INDEX {
|
||||
|
@ -119,7 +120,9 @@ pub fn token_register(
|
|||
maint_weight_shift_duration_inv: I80F48::ZERO,
|
||||
maint_weight_shift_asset_target: I80F48::ZERO,
|
||||
maint_weight_shift_liab_target: I80F48::ZERO,
|
||||
reserved: [0; 2008],
|
||||
fallback_oracle: Pubkey::default(), // unused, introduced in v0.22
|
||||
deposit_limit,
|
||||
reserved: [0; 1968],
|
||||
};
|
||||
|
||||
if let Ok(oracle_price) =
|
||||
|
|
|
@ -102,7 +102,9 @@ pub fn token_register_trustless(
|
|||
maint_weight_shift_duration_inv: I80F48::ZERO,
|
||||
maint_weight_shift_asset_target: I80F48::ZERO,
|
||||
maint_weight_shift_liab_target: I80F48::ZERO,
|
||||
reserved: [0; 2008],
|
||||
fallback_oracle: Pubkey::default(), // unused, introduced in v0.22
|
||||
deposit_limit: 0,
|
||||
reserved: [0; 1968],
|
||||
};
|
||||
|
||||
if let Ok(oracle_price) =
|
||||
|
|
|
@ -153,6 +153,7 @@ pub mod mango_v4 {
|
|||
interest_curve_scaling: f32,
|
||||
interest_target_utilization: f32,
|
||||
group_insurance_fund: bool,
|
||||
deposit_limit: u64,
|
||||
) -> Result<()> {
|
||||
#[cfg(feature = "enable-gpl")]
|
||||
instructions::token_register(
|
||||
|
@ -183,6 +184,7 @@ pub mod mango_v4 {
|
|||
interest_curve_scaling,
|
||||
interest_target_utilization,
|
||||
group_insurance_fund,
|
||||
deposit_limit,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -234,6 +236,8 @@ pub mod mango_v4 {
|
|||
maint_weight_shift_asset_target_opt: Option<f32>,
|
||||
maint_weight_shift_liab_target_opt: Option<f32>,
|
||||
maint_weight_shift_abort: bool,
|
||||
set_fallback_oracle: bool, // unused, introduced in v0.22
|
||||
deposit_limit_opt: Option<u64>,
|
||||
) -> Result<()> {
|
||||
#[cfg(feature = "enable-gpl")]
|
||||
instructions::token_edit(
|
||||
|
@ -272,6 +276,8 @@ pub mod mango_v4 {
|
|||
maint_weight_shift_asset_target_opt,
|
||||
maint_weight_shift_liab_target_opt,
|
||||
maint_weight_shift_abort,
|
||||
set_fallback_oracle,
|
||||
deposit_limit_opt,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -636,6 +642,7 @@ pub mod mango_v4 {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// requires the receiver_bank in the health account list to be writable
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn serum3_place_order_v2(
|
||||
ctx: Context<Serum3PlaceOrder>,
|
||||
|
|
|
@ -167,8 +167,12 @@ pub struct Bank {
|
|||
pub maint_weight_shift_asset_target: I80F48,
|
||||
pub maint_weight_shift_liab_target: I80F48,
|
||||
|
||||
#[derivative(Debug = "ignore")]
|
||||
pub reserved: [u8; 2008],
|
||||
pub fallback_oracle: Pubkey, // unused, introduced in v0.22
|
||||
|
||||
/// zero means none, in token native
|
||||
pub deposit_limit: u64,
|
||||
|
||||
pub reserved: [u8; 1968],
|
||||
}
|
||||
const_assert_eq!(
|
||||
size_of::<Bank>(),
|
||||
|
@ -203,7 +207,9 @@ const_assert_eq!(
|
|||
+ 8 * 2
|
||||
+ 8 * 2
|
||||
+ 16 * 3
|
||||
+ 2008
|
||||
+ 32
|
||||
+ 8
|
||||
+ 1968
|
||||
);
|
||||
const_assert_eq!(size_of::<Bank>(), 3064);
|
||||
const_assert_eq!(size_of::<Bank>() % 8, 0);
|
||||
|
@ -220,6 +226,19 @@ impl WithdrawResult {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct TransferResult {
|
||||
pub source_is_active: bool,
|
||||
pub target_is_active: bool,
|
||||
pub loan_origination_fee: I80F48,
|
||||
pub loan_amount: I80F48,
|
||||
}
|
||||
|
||||
impl TransferResult {
|
||||
pub fn has_loan(&self) -> bool {
|
||||
self.loan_amount.is_positive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Bank {
|
||||
pub fn from_existing_bank(
|
||||
existing_bank: &Bank,
|
||||
|
@ -292,7 +311,9 @@ impl Bank {
|
|||
maint_weight_shift_duration_inv: existing_bank.maint_weight_shift_duration_inv,
|
||||
maint_weight_shift_asset_target: existing_bank.maint_weight_shift_asset_target,
|
||||
maint_weight_shift_liab_target: existing_bank.maint_weight_shift_liab_target,
|
||||
reserved: [0; 2008],
|
||||
fallback_oracle: existing_bank.oracle,
|
||||
deposit_limit: existing_bank.deposit_limit,
|
||||
reserved: [0; 1968],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -724,6 +745,64 @@ impl Bank {
|
|||
}
|
||||
}
|
||||
|
||||
/// Generic "transfer" from source to target.
|
||||
///
|
||||
/// Amounts for source and target can differ and can be zero.
|
||||
/// Checks reduce-only, net borrow limits and deposit limits.
|
||||
pub fn checked_transfer_with_fee(
|
||||
&mut self,
|
||||
source: &mut TokenPosition,
|
||||
source_amount: I80F48,
|
||||
target: &mut TokenPosition,
|
||||
target_amount: I80F48,
|
||||
now_ts: u64,
|
||||
oracle_price: I80F48,
|
||||
) -> Result<TransferResult> {
|
||||
let before_borrows = self.indexed_borrows;
|
||||
let before_deposits = self.indexed_deposits;
|
||||
|
||||
let withdraw_result = if !source_amount.is_zero() {
|
||||
let withdraw_result = self.withdraw_with_fee(source, source_amount, now_ts)?;
|
||||
require!(
|
||||
source.indexed_position >= 0 || !self.are_borrows_reduce_only(),
|
||||
MangoError::TokenInReduceOnlyMode
|
||||
);
|
||||
withdraw_result
|
||||
} else {
|
||||
WithdrawResult {
|
||||
position_is_active: true,
|
||||
loan_amount: I80F48::ZERO,
|
||||
loan_origination_fee: I80F48::ZERO,
|
||||
}
|
||||
};
|
||||
|
||||
let target_is_active = if !target_amount.is_zero() {
|
||||
let active = self.deposit(target, target_amount, now_ts)?;
|
||||
require!(
|
||||
target.indexed_position <= 0 || !self.are_deposits_reduce_only(),
|
||||
MangoError::TokenInReduceOnlyMode
|
||||
);
|
||||
active
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
// Adding DELTA here covers the case where we add slightly more than we withdraw
|
||||
if self.indexed_borrows > before_borrows + I80F48::DELTA {
|
||||
self.check_net_borrows(oracle_price)?;
|
||||
}
|
||||
if self.indexed_deposits > before_deposits + I80F48::DELTA {
|
||||
self.check_deposit_and_oo_limit()?;
|
||||
}
|
||||
|
||||
Ok(TransferResult {
|
||||
source_is_active: withdraw_result.position_is_active,
|
||||
target_is_active,
|
||||
loan_origination_fee: withdraw_result.loan_origination_fee,
|
||||
loan_amount: withdraw_result.loan_amount,
|
||||
})
|
||||
}
|
||||
|
||||
/// Update the bank's net_borrows fields.
|
||||
///
|
||||
/// If oracle_price is set, also do a net borrows check and error if the threshold is exceeded.
|
||||
|
@ -769,6 +848,49 @@ impl Bank {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remaining_deposits_until_limit(&self) -> I80F48 {
|
||||
if self.deposit_limit == 0 {
|
||||
return I80F48::MAX;
|
||||
}
|
||||
|
||||
// Assuming slightly higher deposits than true allows the returned value
|
||||
// to be deposit()ed safely into this bank without triggering limits.
|
||||
// (because deposit() will round up in favor of the user)
|
||||
let deposits = self.deposit_index * (self.indexed_deposits + I80F48::DELTA);
|
||||
|
||||
let serum = I80F48::from(self.potential_serum_tokens);
|
||||
let total = deposits + serum;
|
||||
|
||||
I80F48::from(self.deposit_limit) - total
|
||||
}
|
||||
|
||||
pub fn check_deposit_and_oo_limit(&self) -> Result<()> {
|
||||
if self.deposit_limit == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Intentionally does not use remaining_deposits_until_limit(): That function
|
||||
// returns slightly less than the true limit to make sure depositing that amount
|
||||
// will not cause a limit overrun.
|
||||
let deposits = self.native_deposits();
|
||||
let serum = I80F48::from(self.potential_serum_tokens);
|
||||
let total = deposits + serum;
|
||||
let remaining = I80F48::from(self.deposit_limit) - total;
|
||||
if remaining < 0 {
|
||||
return Err(error_msg_typed!(
|
||||
MangoError::BankDepositLimit,
|
||||
"deposit limit exceeded: remaining: {}, total: {}, limit: {}, deposits: {}, serum: {}",
|
||||
remaining,
|
||||
total,
|
||||
self.deposit_limit,
|
||||
deposits,
|
||||
serum,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_cumulative_interest(
|
||||
&self,
|
||||
position: &mut TokenPosition,
|
||||
|
@ -1177,6 +1299,148 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bank_transfer() {
|
||||
//
|
||||
// SETUP
|
||||
//
|
||||
|
||||
let mut bank_proto = Bank::zeroed();
|
||||
bank_proto.net_borrow_limit_window_size_ts = 1; // dummy
|
||||
bank_proto.net_borrow_limit_per_window_quote = i64::MAX; // max since we don't want this to interfere
|
||||
bank_proto.deposit_index = I80F48::from(1_234_567);
|
||||
bank_proto.borrow_index = I80F48::from(1_234_567);
|
||||
bank_proto.loan_origination_fee_rate = I80F48::from_num(0.1);
|
||||
|
||||
let account_proto = TokenPosition {
|
||||
indexed_position: I80F48::ZERO,
|
||||
token_index: 0,
|
||||
in_use_count: 1,
|
||||
cumulative_deposit_interest: 0.0,
|
||||
cumulative_borrow_interest: 0.0,
|
||||
previous_index: I80F48::ZERO,
|
||||
padding: Default::default(),
|
||||
reserved: [0; 128],
|
||||
};
|
||||
|
||||
//
|
||||
// TESTS
|
||||
//
|
||||
|
||||
// simple transfer
|
||||
{
|
||||
let mut bank = bank_proto.clone();
|
||||
let mut a1 = account_proto.clone();
|
||||
let mut a2 = account_proto.clone();
|
||||
|
||||
let amount = I80F48::from(100);
|
||||
bank.deposit(&mut a1, amount, 0).unwrap();
|
||||
let damount = a1.native(&bank);
|
||||
let r = bank
|
||||
.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE)
|
||||
.unwrap();
|
||||
assert_eq!(a2.native(&bank), damount);
|
||||
assert!(r.source_is_active);
|
||||
assert!(r.target_is_active);
|
||||
}
|
||||
|
||||
// borrow limits
|
||||
{
|
||||
let mut bank = bank_proto.clone();
|
||||
bank.net_borrow_limit_per_window_quote = 100;
|
||||
bank.loan_origination_fee_rate = I80F48::ZERO;
|
||||
let mut a1 = account_proto.clone();
|
||||
let mut a2 = account_proto.clone();
|
||||
|
||||
{
|
||||
let mut b = bank.clone();
|
||||
let amount = I80F48::from(101);
|
||||
assert!(b
|
||||
.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
{
|
||||
let mut b = bank.clone();
|
||||
let amount = I80F48::from(100);
|
||||
b.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
{
|
||||
let mut b = bank.clone();
|
||||
let amount = b.remaining_net_borrows_quote(I80F48::ONE);
|
||||
b.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// deposit limits
|
||||
{
|
||||
let mut bank = bank_proto.clone();
|
||||
bank.deposit_limit = 100;
|
||||
let mut a1 = account_proto.clone();
|
||||
let mut a2 = account_proto.clone();
|
||||
|
||||
{
|
||||
let mut b = bank.clone();
|
||||
let amount = I80F48::from(101);
|
||||
assert!(b
|
||||
.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
{
|
||||
// still bad because deposit() adds DELTA more than requested
|
||||
let mut b = bank.clone();
|
||||
let amount = I80F48::from(100);
|
||||
assert!(b
|
||||
.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
{
|
||||
let mut b = bank.clone();
|
||||
let amount = I80F48::from_num(99.999);
|
||||
b.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
{
|
||||
let mut b = bank.clone();
|
||||
let amount = b.remaining_deposits_until_limit();
|
||||
b.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// reducing transfer while limits exceeded
|
||||
{
|
||||
let mut bank = bank_proto.clone();
|
||||
bank.loan_origination_fee_rate = I80F48::ZERO;
|
||||
|
||||
let amount = I80F48::from(100);
|
||||
let mut a1 = account_proto.clone();
|
||||
bank.deposit(&mut a1, amount, 0).unwrap();
|
||||
let mut a2 = account_proto.clone();
|
||||
bank.withdraw_with_fee(&mut a2, amount, 0).unwrap();
|
||||
|
||||
bank.net_borrow_limit_per_window_quote = 100;
|
||||
bank.net_borrows_in_window = 200;
|
||||
bank.deposit_limit = 100;
|
||||
bank.potential_serum_tokens = 200;
|
||||
|
||||
let half = I80F48::from(50);
|
||||
bank.checked_transfer_with_fee(&mut a1, half, &mut a2, half, 0, I80F48::ONE)
|
||||
.unwrap();
|
||||
bank.checked_transfer_with_fee(&mut a1, half, &mut a2, half, 0, I80F48::ONE)
|
||||
.unwrap();
|
||||
assert!(bank
|
||||
.checked_transfer_with_fee(&mut a1, half, &mut a2, half, 0, I80F48::ONE)
|
||||
.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_new_avg_utilization() {
|
||||
let mut bank = Bank::zeroed();
|
||||
|
|
|
@ -511,3 +511,174 @@ async fn test_bank_maint_weight_shift() -> Result<(), TransportError> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_bank_deposit_limit() -> Result<(), TransportError> {
|
||||
let context = TestContext::new().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
let payer_token_account = context.users[1].token_accounts[0];
|
||||
let mints = &context.mints[0..1];
|
||||
|
||||
let mango_setup::GroupWithTokens { group, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints: mints.to_vec(),
|
||||
zero_token_is_quote: true,
|
||||
..mango_setup::GroupWithTokensConfig::default()
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
|
||||
let funding_amount = 0;
|
||||
let account1 = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
1,
|
||||
&context.users[1],
|
||||
&mints[0..0],
|
||||
funding_amount,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
let account2 = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
2,
|
||||
&context.users[1],
|
||||
&mints[0..0],
|
||||
funding_amount,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenEdit {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[0].pubkey,
|
||||
fallback_oracle: Pubkey::default(),
|
||||
options: mango_v4::instruction::TokenEdit {
|
||||
deposit_limit_opt: Some(2000),
|
||||
..token_edit_instruction_default()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let default_deposit_ix = TokenDepositInstruction {
|
||||
amount: 0,
|
||||
reduce_only: false,
|
||||
account: Pubkey::default(),
|
||||
owner,
|
||||
token_account: payer_token_account,
|
||||
token_authority: payer,
|
||||
bank_index: 0,
|
||||
};
|
||||
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: 2001,
|
||||
account: account1,
|
||||
..default_deposit_ix
|
||||
},
|
||||
MangoError::BankDepositLimit
|
||||
);
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: 1001,
|
||||
account: account1,
|
||||
..default_deposit_ix
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: 1000,
|
||||
account: account1,
|
||||
..default_deposit_ix
|
||||
},
|
||||
MangoError::BankDepositLimit
|
||||
);
|
||||
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: 1000,
|
||||
account: account2,
|
||||
..default_deposit_ix
|
||||
},
|
||||
MangoError::BankDepositLimit
|
||||
);
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: 998, // 999 does not work due to rounding
|
||||
account: account2,
|
||||
..default_deposit_ix
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: 1,
|
||||
account: account2,
|
||||
..default_deposit_ix
|
||||
},
|
||||
MangoError::BankDepositLimit
|
||||
);
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: 5,
|
||||
allow_borrow: false,
|
||||
account: account2,
|
||||
owner,
|
||||
token_account: payer_token_account,
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: 6,
|
||||
account: account2,
|
||||
..default_deposit_ix
|
||||
},
|
||||
MangoError::BankDepositLimit
|
||||
);
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: 5,
|
||||
account: account2,
|
||||
..default_deposit_ix
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -613,3 +613,118 @@ async fn test_flash_loan_creates_ata_accounts() -> Result<(), BanksClientError>
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_margin_trade_deposit_limit() -> Result<(), BanksClientError> {
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(100_000);
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
let mints = &context.mints[0..2];
|
||||
let payer_mint0_account = context.users[1].token_accounts[0];
|
||||
|
||||
//
|
||||
// SETUP: Create a group, account, register a token (mint0)
|
||||
//
|
||||
|
||||
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints: mints.to_vec(),
|
||||
..GroupWithTokensConfig::default()
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
let bank = tokens[0].bank;
|
||||
|
||||
//
|
||||
// SETUP: deposit limit
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
TokenEdit {
|
||||
group,
|
||||
admin,
|
||||
mint: tokens[0].mint.pubkey,
|
||||
fallback_oracle: Pubkey::default(),
|
||||
options: mango_v4::instruction::TokenEdit {
|
||||
deposit_limit_opt: Some(1000),
|
||||
..token_edit_instruction_default()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// create the test user account
|
||||
//
|
||||
|
||||
let deposit_amount_initial = 100;
|
||||
let account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
0,
|
||||
&context.users[1],
|
||||
&mints[..1],
|
||||
deposit_amount_initial,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
//
|
||||
// TEST: Margin trade
|
||||
//
|
||||
let margin_account = payer_mint0_account;
|
||||
let target_token_account = context.users[0].token_accounts[0];
|
||||
let make_flash_loan_tx = |solana, deposit_amount| async move {
|
||||
let mut tx = ClientTransaction::new(solana);
|
||||
let loans = vec![FlashLoanPart {
|
||||
bank,
|
||||
token_account: target_token_account,
|
||||
withdraw_amount: 0,
|
||||
}];
|
||||
tx.add_instruction(FlashLoanBeginInstruction {
|
||||
account,
|
||||
owner,
|
||||
loans: loans.clone(),
|
||||
})
|
||||
.await;
|
||||
tx.add_instruction_direct(
|
||||
spl_token::instruction::transfer(
|
||||
&spl_token::ID,
|
||||
&margin_account,
|
||||
&target_token_account,
|
||||
&payer.pubkey(),
|
||||
&[&payer.pubkey()],
|
||||
deposit_amount,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
tx.add_signer(payer);
|
||||
tx.add_instruction(FlashLoanEndInstruction {
|
||||
account,
|
||||
owner,
|
||||
loans,
|
||||
// the test only accesses a single token: not a swap
|
||||
flash_loan_type: mango_v4::accounts_ix::FlashLoanType::Unknown,
|
||||
})
|
||||
.await;
|
||||
tx
|
||||
};
|
||||
|
||||
make_flash_loan_tx(solana, 901)
|
||||
.await
|
||||
.send_expect_error(MangoError::BankDepositLimit)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
make_flash_loan_tx(solana, 899).await.send().await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1551,6 +1551,166 @@ async fn test_serum_bands() -> Result<(), TransportError> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_serum_deposit_limits() -> Result<(), TransportError> {
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(150_000); // Serum3PlaceOrder needs lots
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
//
|
||||
// SETUP: Create a group, accounts, market etc
|
||||
//
|
||||
let deposit_amount = 5000; // for 10k tokens over both order_placers
|
||||
let CommonSetup {
|
||||
group_with_tokens,
|
||||
mut order_placer,
|
||||
quote_token,
|
||||
base_token,
|
||||
..
|
||||
} = common_setup2(&context, deposit_amount, 0).await;
|
||||
|
||||
//
|
||||
// SETUP: Set oracle price for market to 2
|
||||
//
|
||||
set_bank_stub_oracle_price(
|
||||
solana,
|
||||
group_with_tokens.group,
|
||||
&base_token,
|
||||
group_with_tokens.admin,
|
||||
4.0,
|
||||
)
|
||||
.await;
|
||||
set_bank_stub_oracle_price(
|
||||
solana,
|
||||
group_with_tokens.group,
|
||||
"e_token,
|
||||
group_with_tokens.admin,
|
||||
2.0,
|
||||
)
|
||||
.await;
|
||||
|
||||
//
|
||||
// SETUP: Base token: add deposit limit
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
TokenEdit {
|
||||
group: group_with_tokens.group,
|
||||
admin: group_with_tokens.admin,
|
||||
mint: base_token.mint.pubkey,
|
||||
fallback_oracle: Pubkey::default(),
|
||||
options: mango_v4::instruction::TokenEdit {
|
||||
deposit_limit_opt: Some(13000),
|
||||
..token_edit_instruction_default()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let solana2 = context.solana.clone();
|
||||
let base_bank = base_token.bank;
|
||||
let remaining_base = {
|
||||
|| async {
|
||||
let b: Bank = solana2.get_account(base_bank).await;
|
||||
b.remaining_deposits_until_limit().round().to_num::<u64>()
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// TEST: even when placing all base tokens into an ask, they still count
|
||||
//
|
||||
|
||||
order_placer.ask(2.0, 5000).await.unwrap();
|
||||
assert_eq!(remaining_base().await, 3000);
|
||||
|
||||
//
|
||||
// TEST: if we bid to buy more base, the limit reduces
|
||||
//
|
||||
|
||||
order_placer.bid_maker(1.5, 1000).await.unwrap();
|
||||
assert_eq!(remaining_base().await, 2000);
|
||||
|
||||
//
|
||||
// TEST: if we bid too much for the limit, the order does not go through
|
||||
//
|
||||
|
||||
let r = order_placer.try_bid(1.5, 2001, false).await;
|
||||
assert_mango_error(&r, MangoError::BankDepositLimit.into(), "dep limit".into());
|
||||
order_placer.try_bid(1.5, 1999, false).await.unwrap(); // not 2000 due to rounding
|
||||
|
||||
//
|
||||
// SETUP: Switch deposit limit to quote token
|
||||
//
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenEdit {
|
||||
group: group_with_tokens.group,
|
||||
admin: group_with_tokens.admin,
|
||||
mint: base_token.mint.pubkey,
|
||||
fallback_oracle: Pubkey::default(),
|
||||
options: mango_v4::instruction::TokenEdit {
|
||||
deposit_limit_opt: Some(0),
|
||||
..token_edit_instruction_default()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
TokenEdit {
|
||||
group: group_with_tokens.group,
|
||||
admin: group_with_tokens.admin,
|
||||
mint: quote_token.mint.pubkey,
|
||||
fallback_oracle: Pubkey::default(),
|
||||
options: mango_v4::instruction::TokenEdit {
|
||||
deposit_limit_opt: Some(13000),
|
||||
..token_edit_instruction_default()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let solana2 = context.solana.clone();
|
||||
let quote_bank = quote_token.bank;
|
||||
let remaining_quote = {
|
||||
|| async {
|
||||
let b: Bank = solana2.get_account(quote_bank).await;
|
||||
b.remaining_deposits_until_limit().round().to_num::<u64>()
|
||||
}
|
||||
};
|
||||
|
||||
order_placer.cancel_all().await;
|
||||
|
||||
//
|
||||
// TEST: even when placing all quote tokens into a bid, they still count
|
||||
//
|
||||
|
||||
order_placer.bid_maker(2.0, 2500).await.unwrap();
|
||||
assert_eq!(remaining_quote().await, 3000);
|
||||
|
||||
//
|
||||
// TEST: if we ask to get more quote, the limit reduces
|
||||
//
|
||||
|
||||
order_placer.ask(5.0, 200).await.unwrap();
|
||||
assert_eq!(remaining_quote().await, 2000);
|
||||
|
||||
//
|
||||
// TEST: if we bid too much for the limit, the order does not go through
|
||||
//
|
||||
|
||||
let r = order_placer.try_ask(5.0, 401).await;
|
||||
assert_mango_error(&r, MangoError::BankDepositLimit.into(), "dep limit".into());
|
||||
order_placer.try_ask(5.0, 399).await.unwrap(); // not 400 due to rounding
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct CommonSetup {
|
||||
group_with_tokens: GroupWithTokens,
|
||||
serum_market_cookie: SpotMarketCookie,
|
||||
|
@ -1561,6 +1721,14 @@ struct CommonSetup {
|
|||
}
|
||||
|
||||
async fn common_setup(context: &TestContext, deposit_amount: u64) -> CommonSetup {
|
||||
common_setup2(context, deposit_amount, 10000000).await
|
||||
}
|
||||
|
||||
async fn common_setup2(
|
||||
context: &TestContext,
|
||||
deposit_amount: u64,
|
||||
vault_funding: u64,
|
||||
) -> CommonSetup {
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
|
@ -1635,17 +1803,19 @@ async fn common_setup(context: &TestContext, deposit_amount: u64) -> CommonSetup
|
|||
)
|
||||
.await;
|
||||
// to have enough funds in the vaults
|
||||
create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
3,
|
||||
&context.users[1],
|
||||
mints,
|
||||
10000000,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
if vault_funding > 0 {
|
||||
create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
3,
|
||||
&context.users[1],
|
||||
mints,
|
||||
10000000,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let open_orders = send_tx(
|
||||
solana,
|
||||
|
|
|
@ -1013,3 +1013,146 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_token_conditional_swap_deposit_limit() -> Result<(), TransportError> {
|
||||
pub use utils::assert_equal_f64_f64 as assert_equal_f_f;
|
||||
|
||||
let context = TestContext::new().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
let mints = &context.mints[0..2];
|
||||
|
||||
//
|
||||
// SETUP: Create a group, account, tokens
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints: mints.to_vec(),
|
||||
..mango_setup::GroupWithTokensConfig::default()
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
let quote_token = &tokens[0];
|
||||
let base_token = &tokens[1];
|
||||
|
||||
// total deposits on quote and base is 2x this value
|
||||
let deposit_amount = 1_000f64;
|
||||
let account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
0,
|
||||
&context.users[1],
|
||||
&mints[..1],
|
||||
deposit_amount as u64,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
let liqor = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
1,
|
||||
&context.users[1],
|
||||
mints,
|
||||
deposit_amount as u64,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
99,
|
||||
&context.users[1],
|
||||
&mints[1..],
|
||||
deposit_amount as u64,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
//
|
||||
// SETUP: A base deposit limit
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
TokenEdit {
|
||||
group,
|
||||
admin,
|
||||
mint: base_token.mint.pubkey,
|
||||
fallback_oracle: Pubkey::default(),
|
||||
options: mango_v4::instruction::TokenEdit {
|
||||
deposit_limit_opt: Some(2500),
|
||||
..token_edit_instruction_default()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// SETUP: Sell base token from "account" creating borrows that are deposited into liqor,
|
||||
// increasing the total deposits
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
TokenConditionalSwapCreateInstruction {
|
||||
account,
|
||||
owner,
|
||||
buy_mint: quote_token.mint.pubkey,
|
||||
sell_mint: base_token.mint.pubkey,
|
||||
max_buy: 900,
|
||||
max_sell: 900,
|
||||
price_lower_limit: 0.0,
|
||||
price_upper_limit: 10.0,
|
||||
price_premium_rate: 0.01,
|
||||
allow_creating_deposits: true,
|
||||
allow_creating_borrows: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// TEST: Large execution fails, a bit smaller is ok
|
||||
//
|
||||
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
TokenConditionalSwapTriggerInstruction {
|
||||
liqee: account,
|
||||
liqor,
|
||||
liqor_owner: owner,
|
||||
index: 0,
|
||||
max_buy_token_to_liqee: 100000,
|
||||
max_sell_token_to_liqor: 501,
|
||||
min_buy_token: 1,
|
||||
min_taker_price: 0.0,
|
||||
},
|
||||
MangoError::BankDepositLimit,
|
||||
);
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenConditionalSwapTriggerInstruction {
|
||||
liqee: account,
|
||||
liqor,
|
||||
liqor_owner: owner,
|
||||
index: 0,
|
||||
max_buy_token_to_liqee: 100000,
|
||||
max_sell_token_to_liqor: 499,
|
||||
min_buy_token: 1,
|
||||
min_taker_price: 0.0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -69,6 +69,27 @@ pub async fn send_tx_get_metadata<CI: ClientInstruction>(
|
|||
.await
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! send_tx_expect_error {
|
||||
($solana:expr, $ix:expr, $err:expr $(,)?) => {
|
||||
let result = send_tx($solana, $ix).await;
|
||||
let expected_err: u32 = $err.into();
|
||||
match result {
|
||||
Ok(_) => assert!(false, "no error returned"),
|
||||
Err(TransportError::TransactionError(
|
||||
solana_sdk::transaction::TransactionError::InstructionError(
|
||||
_,
|
||||
solana_program::instruction::InstructionError::Custom(err_num),
|
||||
),
|
||||
)) => {
|
||||
assert_eq!(err_num, expected_err, "wrong error code");
|
||||
}
|
||||
_ => assert!(false, "not a mango error"),
|
||||
}
|
||||
};
|
||||
}
|
||||
pub use send_tx_expect_error;
|
||||
|
||||
/// Build a transaction from multiple instructions
|
||||
pub struct ClientTransaction {
|
||||
solana: Arc<SolanaCookie>,
|
||||
|
@ -111,6 +132,28 @@ impl<'a> ClientTransaction {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_expect_error(
|
||||
&self,
|
||||
error: mango_v4::error::MangoError,
|
||||
) -> std::result::Result<(), BanksClientError> {
|
||||
let tx_result = self
|
||||
.solana
|
||||
.process_transaction(&self.instructions, Some(&self.signers))
|
||||
.await?;
|
||||
match tx_result.result {
|
||||
Ok(_) => assert!(false, "no error returned"),
|
||||
Err(solana_sdk::transaction::TransactionError::InstructionError(
|
||||
_,
|
||||
solana_program::instruction::InstructionError::Custom(err_num),
|
||||
)) => {
|
||||
let expected_err: u32 = error.into();
|
||||
assert_eq!(err_num, expected_err, "wrong error code");
|
||||
}
|
||||
_ => assert!(false, "not a mango error"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Tx error still returns success
|
||||
pub async fn send_get_metadata(
|
||||
&self,
|
||||
|
@ -1011,6 +1054,7 @@ impl ClientInstruction for TokenRegisterInstruction {
|
|||
interest_curve_scaling: 1.0,
|
||||
interest_target_utilization: 0.5,
|
||||
group_insurance_fund: true,
|
||||
deposit_limit: 0,
|
||||
};
|
||||
|
||||
let bank = Pubkey::find_program_address(
|
||||
|
@ -1262,6 +1306,8 @@ pub fn token_edit_instruction_default() -> mango_v4::instruction::TokenEdit {
|
|||
maint_weight_shift_asset_target_opt: None,
|
||||
maint_weight_shift_liab_target_opt: None,
|
||||
maint_weight_shift_abort: false,
|
||||
set_fallback_oracle: false,
|
||||
deposit_limit_opt: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -87,15 +87,12 @@ pub fn assert_mango_error<T>(
|
|||
) {
|
||||
match result {
|
||||
Ok(_) => assert!(false, "No error returned"),
|
||||
Err(TransportError::TransactionError(tx_err)) => match tx_err {
|
||||
TransactionError::InstructionError(_, err) => match err {
|
||||
InstructionError::Custom(err_num) => {
|
||||
assert_eq!(*err_num, expected_error, "{}", comment);
|
||||
}
|
||||
_ => assert!(false, "Not a mango error"),
|
||||
},
|
||||
_ => assert!(false, "Not a mango error"),
|
||||
},
|
||||
Err(TransportError::TransactionError(TransactionError::InstructionError(
|
||||
_,
|
||||
InstructionError::Custom(err_num),
|
||||
))) => {
|
||||
assert_eq!(*err_num, expected_error, "{}", comment);
|
||||
}
|
||||
_ => assert!(false, "Not a mango error"),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -132,12 +132,13 @@ export class Bank implements BankForHealth {
|
|||
flashLoanSwapFeeRate: number;
|
||||
interestTargetUtilization: number;
|
||||
interestCurveScaling: number;
|
||||
depositsInSerum: BN;
|
||||
potentialSerumTokens: BN;
|
||||
maintWeightShiftStart: BN;
|
||||
maintWeightShiftEnd: BN;
|
||||
maintWeightShiftDurationInv: I80F48Dto;
|
||||
maintWeightShiftAssetTarget: I80F48Dto;
|
||||
maintWeightShiftLiabTarget: I80F48Dto;
|
||||
depositLimit: BN;
|
||||
},
|
||||
): Bank {
|
||||
return new Bank(
|
||||
|
@ -191,12 +192,13 @@ export class Bank implements BankForHealth {
|
|||
obj.flashLoanSwapFeeRate,
|
||||
obj.interestTargetUtilization,
|
||||
obj.interestCurveScaling,
|
||||
obj.depositsInSerum,
|
||||
obj.potentialSerumTokens,
|
||||
obj.maintWeightShiftStart,
|
||||
obj.maintWeightShiftEnd,
|
||||
obj.maintWeightShiftDurationInv,
|
||||
obj.maintWeightShiftAssetTarget,
|
||||
obj.maintWeightShiftLiabTarget,
|
||||
obj.depositLimit,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -251,12 +253,13 @@ export class Bank implements BankForHealth {
|
|||
public flashLoanSwapFeeRate: number,
|
||||
public interestTargetUtilization: number,
|
||||
public interestCurveScaling: number,
|
||||
public depositsInSerum: BN,
|
||||
public potentialSerumTokens: BN,
|
||||
public maintWeightShiftStart: BN,
|
||||
public maintWeightShiftEnd: BN,
|
||||
maintWeightShiftDurationInv: I80F48Dto,
|
||||
maintWeightShiftAssetTarget: I80F48Dto,
|
||||
maintWeightShiftLiabTarget: I80F48Dto,
|
||||
public depositLimit: BN,
|
||||
) {
|
||||
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
|
||||
this.oracleConfig = {
|
||||
|
|
|
@ -419,6 +419,7 @@ export class MangoClient {
|
|||
params.interestCurveScaling,
|
||||
params.interestTargetUtilization,
|
||||
params.groupInsuranceFund,
|
||||
params.depositLimit,
|
||||
)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
|
@ -501,6 +502,8 @@ export class MangoClient {
|
|||
params.maintWeightShiftAssetTarget,
|
||||
params.maintWeightShiftLiabTarget,
|
||||
params.maintWeightShiftAbort ?? false,
|
||||
false, // setFallbackOracle, unused
|
||||
params.depositLimit,
|
||||
)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
|
@ -1568,9 +1571,10 @@ export class MangoClient {
|
|||
quoteBank: Bank,
|
||||
marketIndex: number,
|
||||
name: string,
|
||||
oraclePriceBand: number,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
const ix = await this.program.methods
|
||||
.serum3RegisterMarket(marketIndex, name)
|
||||
.serum3RegisterMarket(marketIndex, name, oraclePriceBand)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
|
@ -1590,11 +1594,12 @@ export class MangoClient {
|
|||
reduceOnly: boolean | null,
|
||||
forceClose: boolean | null,
|
||||
name: string | null,
|
||||
oraclePriceBand: number | null,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
const serum3Market =
|
||||
group.serum3MarketsMapByMarketIndex.get(serum3MarketIndex);
|
||||
const ix = await this.program.methods
|
||||
.serum3EditMarket(reduceOnly, forceClose, name)
|
||||
.serum3EditMarket(reduceOnly, forceClose, name, oraclePriceBand)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
|
|
|
@ -27,6 +27,7 @@ export interface TokenRegisterParams {
|
|||
flashLoanSwapFeeRate: number;
|
||||
interestCurveScaling: number;
|
||||
interestTargetUtilization: number;
|
||||
depositLimit: BN;
|
||||
}
|
||||
|
||||
export const DefaultTokenRegisterParams: TokenRegisterParams = {
|
||||
|
@ -64,6 +65,7 @@ export const DefaultTokenRegisterParams: TokenRegisterParams = {
|
|||
flashLoanSwapFeeRate: 0.0005,
|
||||
interestCurveScaling: 4.0,
|
||||
interestTargetUtilization: 0.5,
|
||||
depositLimit: new BN(0),
|
||||
};
|
||||
|
||||
export interface TokenEditParams {
|
||||
|
@ -101,6 +103,7 @@ export interface TokenEditParams {
|
|||
maintWeightShiftAssetTarget: number | null;
|
||||
maintWeightShiftLiabTarget: number | null;
|
||||
maintWeightShiftAbort: boolean | null;
|
||||
depositLimit: BN | null;
|
||||
}
|
||||
|
||||
export const NullTokenEditParams: TokenEditParams = {
|
||||
|
@ -138,6 +141,7 @@ export const NullTokenEditParams: TokenEditParams = {
|
|||
maintWeightShiftAssetTarget: null,
|
||||
maintWeightShiftLiabTarget: null,
|
||||
maintWeightShiftAbort: null,
|
||||
depositLimit: null,
|
||||
};
|
||||
|
||||
export interface PerpEditParams {
|
||||
|
|
|
@ -614,6 +614,10 @@ export type MangoV4 = {
|
|||
{
|
||||
"name": "groupInsuranceFund",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "depositLimit",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -988,6 +992,12 @@ export type MangoV4 = {
|
|||
{
|
||||
"name": "maintWeightShiftAbort",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "depositLimitOpt",
|
||||
"type": {
|
||||
"option": "u64"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -2357,6 +2367,10 @@ export type MangoV4 = {
|
|||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "oraclePriceBand",
|
||||
"type": "f32"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -2400,6 +2414,12 @@ export type MangoV4 = {
|
|||
"type": {
|
||||
"option": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "oraclePriceBandOpt",
|
||||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -2742,6 +2762,9 @@ export type MangoV4 = {
|
|||
},
|
||||
{
|
||||
"name": "serum3PlaceOrderV2",
|
||||
"docs": [
|
||||
"requires the receiver_bank in the health account list to be writable"
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
|
@ -7266,11 +7289,9 @@ export type MangoV4 = {
|
|||
"name": "potentialSerumTokens",
|
||||
"docs": [
|
||||
"Largest amount of tokens that might be added the the bank based on",
|
||||
"serum open order execution.",
|
||||
"",
|
||||
"Can be negative with multiple banks, then it'd need to be balanced in the keeper."
|
||||
"serum open order execution."
|
||||
],
|
||||
"type": "i64"
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "maintWeightShiftStart",
|
||||
|
@ -7298,12 +7319,19 @@ export type MangoV4 = {
|
|||
"defined": "I80F48"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "depositLimit",
|
||||
"docs": [
|
||||
"zero means none, in token native"
|
||||
],
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
2008
|
||||
2000
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -8402,10 +8430,20 @@ export type MangoV4 = {
|
|||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
5
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "oraclePriceBand",
|
||||
"docs": [
|
||||
"Limit orders must be <= oracle * (1+band) and >= oracle / (1+band)",
|
||||
"",
|
||||
"Zero value is the default due to migration and disables the limit,",
|
||||
"same as f32::MAX."
|
||||
],
|
||||
"type": "f32"
|
||||
},
|
||||
{
|
||||
"name": "registrationTime",
|
||||
"type": "u64"
|
||||
|
@ -13467,6 +13505,16 @@ export type MangoV4 = {
|
|||
"code": 6060,
|
||||
"name": "HealthAccountBankNotWritable",
|
||||
"msg": "a bank in the health account list should be writable but is not"
|
||||
},
|
||||
{
|
||||
"code": 6061,
|
||||
"name": "Serum3PriceBandExceeded",
|
||||
"msg": "the market does not allow limit orders too far from the current oracle value"
|
||||
},
|
||||
{
|
||||
"code": 6062,
|
||||
"name": "BankDepositLimit",
|
||||
"msg": "deposit crosses the token's deposit limit"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@ -14087,6 +14135,10 @@ export const IDL: MangoV4 = {
|
|||
{
|
||||
"name": "groupInsuranceFund",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "depositLimit",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -14461,6 +14513,12 @@ export const IDL: MangoV4 = {
|
|||
{
|
||||
"name": "maintWeightShiftAbort",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "depositLimitOpt",
|
||||
"type": {
|
||||
"option": "u64"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -15830,6 +15888,10 @@ export const IDL: MangoV4 = {
|
|||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "oraclePriceBand",
|
||||
"type": "f32"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -15873,6 +15935,12 @@ export const IDL: MangoV4 = {
|
|||
"type": {
|
||||
"option": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "oraclePriceBandOpt",
|
||||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -16215,6 +16283,9 @@ export const IDL: MangoV4 = {
|
|||
},
|
||||
{
|
||||
"name": "serum3PlaceOrderV2",
|
||||
"docs": [
|
||||
"requires the receiver_bank in the health account list to be writable"
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "group",
|
||||
|
@ -20739,11 +20810,9 @@ export const IDL: MangoV4 = {
|
|||
"name": "potentialSerumTokens",
|
||||
"docs": [
|
||||
"Largest amount of tokens that might be added the the bank based on",
|
||||
"serum open order execution.",
|
||||
"",
|
||||
"Can be negative with multiple banks, then it'd need to be balanced in the keeper."
|
||||
"serum open order execution."
|
||||
],
|
||||
"type": "i64"
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "maintWeightShiftStart",
|
||||
|
@ -20771,12 +20840,19 @@ export const IDL: MangoV4 = {
|
|||
"defined": "I80F48"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "depositLimit",
|
||||
"docs": [
|
||||
"zero means none, in token native"
|
||||
],
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
2008
|
||||
2000
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -21875,10 +21951,20 @@ export const IDL: MangoV4 = {
|
|||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
5
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "oraclePriceBand",
|
||||
"docs": [
|
||||
"Limit orders must be <= oracle * (1+band) and >= oracle / (1+band)",
|
||||
"",
|
||||
"Zero value is the default due to migration and disables the limit,",
|
||||
"same as f32::MAX."
|
||||
],
|
||||
"type": "f32"
|
||||
},
|
||||
{
|
||||
"name": "registrationTime",
|
||||
"type": "u64"
|
||||
|
@ -26940,6 +27026,16 @@ export const IDL: MangoV4 = {
|
|||
"code": 6060,
|
||||
"name": "HealthAccountBankNotWritable",
|
||||
"msg": "a bank in the health account list should be writable but is not"
|
||||
},
|
||||
{
|
||||
"code": 6061,
|
||||
"name": "Serum3PriceBandExceeded",
|
||||
"msg": "the market does not allow limit orders too far from the current oracle value"
|
||||
},
|
||||
{
|
||||
"code": 6062,
|
||||
"name": "BankDepositLimit",
|
||||
"msg": "deposit crosses the token's deposit limit"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue