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
|
// TODO: This is where we could multiply in the liquidation fee factors
|
||||||
let price = source_price / target_price;
|
let price = source_price / target_price;
|
||||||
|
|
||||||
util::max_swap_source(
|
util::max_swap_source_ignoring_limits(
|
||||||
self.client,
|
self.client,
|
||||||
self.account_fetcher,
|
self.account_fetcher,
|
||||||
&liqor,
|
&liqor,
|
||||||
|
|
|
@ -26,10 +26,9 @@ use crate::{token_swap_info, util, ErrorTracking};
|
||||||
/// making the whole execution fail.
|
/// making the whole execution fail.
|
||||||
const SLIPPAGE_BUFFER: f64 = 0.01; // 1%
|
const SLIPPAGE_BUFFER: f64 = 0.01; // 1%
|
||||||
|
|
||||||
/// If a tcs gets limited due to exhausted net borrows, don't trigger execution if
|
/// 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 net
|
/// the possible value is below this amount. This avoids spamming executions when limits are exhausted.
|
||||||
/// borrows are exhausted.
|
const EXECUTION_THRESHOLD: u64 = 1_000_000; // 1 USD
|
||||||
const NET_BORROW_EXECUTION_THRESHOLD: u64 = 1_000_000; // 1 USD
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
|
@ -440,12 +439,17 @@ impl Context {
|
||||||
/// This includes
|
/// This includes
|
||||||
/// - tcs restrictions (remaining buy/sell, create borrows/deposits)
|
/// - tcs restrictions (remaining buy/sell, create borrows/deposits)
|
||||||
/// - reduce only banks
|
/// - reduce only banks
|
||||||
/// - net borrow limits on BOTH sides, even though the buy side is technically
|
/// - net borrow limits:
|
||||||
/// a liqor limitation: the liqor could acquire the token before trying the
|
/// - the account may borrow the sell token (and the liqor side may not be a repay)
|
||||||
/// execution... but in practice the liqor will work on margin
|
/// - 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 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(
|
pub fn tcs_max_liqee_execution(
|
||||||
&self,
|
&self,
|
||||||
account: &MangoAccountValue,
|
account: &MangoAccountValue,
|
||||||
|
@ -458,18 +462,18 @@ impl Context {
|
||||||
let premium_price = tcs.premium_price(base_price.to_num(), self.now_ts);
|
let premium_price = tcs.premium_price(base_price.to_num(), self.now_ts);
|
||||||
let maker_price = tcs.maker_price(premium_price);
|
let maker_price = tcs.maker_price(premium_price);
|
||||||
|
|
||||||
let buy_position = account
|
let liqee_buy_position = account
|
||||||
.token_position(tcs.buy_token_index)
|
.token_position(tcs.buy_token_index)
|
||||||
.map(|p| p.native(&buy_bank))
|
.map(|p| p.native(&buy_bank))
|
||||||
.unwrap_or(I80F48::ZERO);
|
.unwrap_or(I80F48::ZERO);
|
||||||
let sell_position = account
|
let liqee_sell_position = account
|
||||||
.token_position(tcs.sell_token_index)
|
.token_position(tcs.sell_token_index)
|
||||||
.map(|p| p.native(&sell_bank))
|
.map(|p| p.native(&sell_bank))
|
||||||
.unwrap_or(I80F48::ZERO);
|
.unwrap_or(I80F48::ZERO);
|
||||||
|
|
||||||
// this is in "buy token received per sell token given" units
|
// this is in "buy token received per sell token given" units
|
||||||
let swap_price = I80F48::from_num((1.0 - SLIPPAGE_BUFFER) / maker_price);
|
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.mango_client,
|
||||||
&self.account_fetcher,
|
&self.account_fetcher,
|
||||||
account,
|
account,
|
||||||
|
@ -480,41 +484,31 @@ impl Context {
|
||||||
)?
|
)?
|
||||||
.floor()
|
.floor()
|
||||||
.to_num::<u64>()
|
.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
|
// 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.
|
// 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
|
// However, when the limits are hit, it will not closed when no further execution
|
||||||
// is possible, because net borrow limit issues are considered transient. Furthermore, we
|
// is possible, because limit issues are transient. Furthermore, we don't want to send
|
||||||
// don't even want to send a tiny tcs trigger transactions, because there's a good chance we
|
// tiny tcs trigger transactions, because there's a good chance we would then be sending
|
||||||
// would then be sending lot of those as oracle prices fluctuate.
|
// lot of those as oracle prices fluctuate.
|
||||||
//
|
//
|
||||||
// Thus, we need to detect if the possible execution amount is tiny _because_ of the
|
// 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 {
|
// Do the liqor buy tokens come from deposits or are they borrowed?
|
||||||
(bank.remaining_net_borrows_quote(price) / price).clamp_to_u64()
|
let mut liqor_buy_borrows = match self.config.mode {
|
||||||
}
|
|
||||||
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 {
|
|
||||||
Mode::BorrowBuyToken => {
|
Mode::BorrowBuyToken => {
|
||||||
// Assume that the liqor has enough buy token if it's collateral
|
// Assume that the liqor has enough buy token if it's collateral
|
||||||
if tcs.buy_token_index == self.config.collateral_token_index {
|
if tcs.buy_token_index == self.config.collateral_token_index {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
max_buy_ignoring_net_borrows
|
max_buy_ignoring_limits
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Mode::SwapCollateralIntoBuy { .. } => 0,
|
Mode::SwapCollateralIntoBuy { .. } => 0,
|
||||||
|
@ -525,19 +519,77 @@ impl Context {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// New maximums adjusted for net borrow limits
|
// First, net borrow limits
|
||||||
let max_sell =
|
let max_sell_net_borrows;
|
||||||
max_sell_ignoring_net_borrows - sell_borrows + sell_borrows.min(available_sell_borrows);
|
let max_buy_net_borrows;
|
||||||
let max_buy =
|
{
|
||||||
max_buy_ignoring_net_borrows - buy_borrows + buy_borrows.min(available_buy_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 = {
|
// New borrows if max_sell_ignoring_limits was withdrawn on the liqee
|
||||||
let buy_threshold = I80F48::from(NET_BORROW_EXECUTION_THRESHOLD) / buy_token_price;
|
// We assume that on the liqor side the position is >= 0, so these are true
|
||||||
let sell_threshold = I80F48::from(NET_BORROW_EXECUTION_THRESHOLD) / sell_token_price;
|
// new borrows.
|
||||||
max_buy < buy_threshold && max_buy_ignoring_net_borrows > buy_threshold
|
let sell_borrows = (I80F48::from(max_sell_ignoring_limits)
|
||||||
|| max_sell < sell_threshold && max_sell_ignoring_net_borrows > sell_threshold
|
- 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);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -715,7 +767,7 @@ impl Context {
|
||||||
.0
|
.0
|
||||||
.native(&buy_bank);
|
.native(&buy_bank);
|
||||||
let liqor_available_buy_token = match mode {
|
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.mango_client,
|
||||||
&self.account_fetcher,
|
&self.account_fetcher,
|
||||||
&liqor,
|
&liqor,
|
||||||
|
@ -734,7 +786,7 @@ impl Context {
|
||||||
self.token_bank_price_mint(collateral_token_index)?;
|
self.token_bank_price_mint(collateral_token_index)?;
|
||||||
let buy_per_collateral_price = (collateral_price / buy_token_price)
|
let buy_per_collateral_price = (collateral_price / buy_token_price)
|
||||||
* I80F48::from_num(jupiter_slippage_fraction);
|
* 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.mango_client,
|
||||||
&self.account_fetcher,
|
&self.account_fetcher,
|
||||||
&liqor,
|
&liqor,
|
||||||
|
@ -751,7 +803,7 @@ impl Context {
|
||||||
// How big can the sell -> buy swap be?
|
// How big can the sell -> buy swap be?
|
||||||
let buy_per_sell_price =
|
let buy_per_sell_price =
|
||||||
(I80F48::from(1) / taker_price) * I80F48::from_num(jupiter_slippage_fraction);
|
(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.mango_client,
|
||||||
&self.account_fetcher,
|
&self.account_fetcher,
|
||||||
&liqor,
|
&liqor,
|
||||||
|
|
|
@ -38,7 +38,9 @@ pub fn is_perp_market<'a>(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience wrapper for getting max swap amounts for a token pair
|
/// 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,
|
client: &MangoClient,
|
||||||
account_fetcher: &chain_data::AccountFetcher,
|
account_fetcher: &chain_data::AccountFetcher,
|
||||||
account: &MangoAccountValue,
|
account: &MangoAccountValue,
|
||||||
|
@ -66,7 +68,7 @@ pub fn max_swap_source(
|
||||||
let source_price = health_cache.token_info(source).unwrap().prices.oracle;
|
let source_price = health_cache.token_info(source).unwrap().prices.oracle;
|
||||||
|
|
||||||
let amount = health_cache
|
let amount = health_cache
|
||||||
.max_swap_source_for_health_ratio(
|
.max_swap_source_for_health_ratio_with_limits(
|
||||||
&account,
|
&account,
|
||||||
&source_bank,
|
&source_bank,
|
||||||
source_price,
|
source_price,
|
||||||
|
@ -79,7 +81,10 @@ pub fn max_swap_source(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience wrapper for getting max swap amounts for a token pair
|
/// 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,
|
client: &MangoClient,
|
||||||
account_fetcher: &chain_data::AccountFetcher,
|
account_fetcher: &chain_data::AccountFetcher,
|
||||||
account: &MangoAccountValue,
|
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)
|
mango_v4_client::health_cache::new_sync(&client.context, account_fetcher, &account)
|
||||||
.expect("always ok");
|
.expect("always ok");
|
||||||
|
|
||||||
let mut source_bank: Bank =
|
let source_bank: Bank =
|
||||||
account_fetcher.fetch(&client.context.mint_info(source).first_bank())?;
|
account_fetcher.fetch(&client.context.mint_info(source).first_bank())?;
|
||||||
source_bank.net_borrow_limit_per_window_quote = -1;
|
let target_bank: Bank =
|
||||||
let mut target_bank: Bank =
|
|
||||||
account_fetcher.fetch(&client.context.mint_info(target).first_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 source_price = health_cache.token_info(source).unwrap().prices.oracle;
|
||||||
|
|
||||||
let amount = health_cache
|
let amount = health_cache
|
||||||
.max_swap_source_for_health_ratio(
|
.max_swap_source_for_health_ratio_ignoring_limits(
|
||||||
&account,
|
&account,
|
||||||
&source_bank,
|
&source_bank,
|
||||||
source_price,
|
source_price,
|
||||||
|
|
|
@ -614,6 +614,10 @@
|
||||||
{
|
{
|
||||||
"name": "groupInsuranceFund",
|
"name": "groupInsuranceFund",
|
||||||
"type": "bool"
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "depositLimit",
|
||||||
|
"type": "u64"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -988,6 +992,12 @@
|
||||||
{
|
{
|
||||||
"name": "maintWeightShiftAbort",
|
"name": "maintWeightShiftAbort",
|
||||||
"type": "bool"
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "depositLimitOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "u64"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2357,6 +2367,10 @@
|
||||||
{
|
{
|
||||||
"name": "name",
|
"name": "name",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "oraclePriceBand",
|
||||||
|
"type": "f32"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2400,6 +2414,12 @@
|
||||||
"type": {
|
"type": {
|
||||||
"option": "string"
|
"option": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "oraclePriceBandOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "f32"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2742,6 +2762,9 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "serum3PlaceOrderV2",
|
"name": "serum3PlaceOrderV2",
|
||||||
|
"docs": [
|
||||||
|
"requires the receiver_bank in the health account list to be writable"
|
||||||
|
],
|
||||||
"accounts": [
|
"accounts": [
|
||||||
{
|
{
|
||||||
"name": "group",
|
"name": "group",
|
||||||
|
@ -7266,11 +7289,9 @@
|
||||||
"name": "potentialSerumTokens",
|
"name": "potentialSerumTokens",
|
||||||
"docs": [
|
"docs": [
|
||||||
"Largest amount of tokens that might be added the the bank based on",
|
"Largest amount of tokens that might be added the the bank based on",
|
||||||
"serum open order execution.",
|
"serum open order execution."
|
||||||
"",
|
|
||||||
"Can be negative with multiple banks, then it'd need to be balanced in the keeper."
|
|
||||||
],
|
],
|
||||||
"type": "i64"
|
"type": "u64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "maintWeightShiftStart",
|
"name": "maintWeightShiftStart",
|
||||||
|
@ -7298,12 +7319,19 @@
|
||||||
"defined": "I80F48"
|
"defined": "I80F48"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "depositLimit",
|
||||||
|
"docs": [
|
||||||
|
"zero means none, in token native"
|
||||||
|
],
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "reserved",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
2008
|
2000
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8402,10 +8430,20 @@
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"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",
|
"name": "registrationTime",
|
||||||
"type": "u64"
|
"type": "u64"
|
||||||
|
@ -13467,6 +13505,16 @@
|
||||||
"code": 6060,
|
"code": 6060,
|
||||||
"name": "HealthAccountBankNotWritable",
|
"name": "HealthAccountBankNotWritable",
|
||||||
"msg": "a bank in the health account list should be writable but is not"
|
"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,
|
HealthAccountBankNotWritable,
|
||||||
#[msg("the market does not allow limit orders too far from the current oracle value")]
|
#[msg("the market does not allow limit orders too far from the current oracle value")]
|
||||||
Serum3PriceBandExceeded,
|
Serum3PriceBandExceeded,
|
||||||
|
#[msg("deposit crosses the token's deposit limit")]
|
||||||
|
BankDepositLimit,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MangoError {
|
impl MangoError {
|
||||||
|
|
|
@ -42,7 +42,6 @@ impl HealthCache {
|
||||||
|
|
||||||
let mut source_bank = source_bank.clone();
|
let mut source_bank = source_bank.clone();
|
||||||
source_bank.withdraw_with_fee(&mut source_position, amount, now_ts)?;
|
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();
|
let mut target_bank = target_bank.clone();
|
||||||
target_bank.deposit(&mut target_position, target_amount, now_ts)?;
|
target_bank.deposit(&mut target_position, target_amount, now_ts)?;
|
||||||
|
|
||||||
|
@ -52,7 +51,40 @@ impl HealthCache {
|
||||||
Ok(resulting_cache)
|
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,
|
&self,
|
||||||
account: &MangoAccountValue,
|
account: &MangoAccountValue,
|
||||||
source_bank: &Bank,
|
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,
|
&self,
|
||||||
account: &MangoAccountValue,
|
account: &MangoAccountValue,
|
||||||
source_bank: &Bank,
|
source_bank: &Bank,
|
||||||
|
@ -81,14 +113,23 @@ impl HealthCache {
|
||||||
price: I80F48,
|
price: I80F48,
|
||||||
min_ratio: I80F48,
|
min_ratio: I80F48,
|
||||||
) -> Result<I80F48> {
|
) -> Result<I80F48> {
|
||||||
self.max_swap_source_for_health_fn(
|
let source_unlimited = self.max_swap_source_for_health_fn(
|
||||||
account,
|
account,
|
||||||
source_bank,
|
source_bank,
|
||||||
source_oracle_price,
|
source_oracle_price,
|
||||||
target_bank,
|
target_bank,
|
||||||
price,
|
price,
|
||||||
min_ratio,
|
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.health_ratio(HealthType::Init), I80F48::MAX);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
health_cache
|
health_cache
|
||||||
.max_swap_source_for_health_ratio(
|
.max_swap_source_for_health_ratio_with_limits(
|
||||||
&account,
|
&account,
|
||||||
&banks[0],
|
&banks[0],
|
||||||
I80F48::from(1),
|
I80F48::from(1),
|
||||||
|
@ -748,7 +789,7 @@ mod tests {
|
||||||
|
|
||||||
let swap_price =
|
let swap_price =
|
||||||
I80F48::from_num(price_factor) * source_price.oracle / target_price.oracle;
|
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(
|
.max_swap_source_for_health_fn(
|
||||||
&account,
|
&account,
|
||||||
&source_bank,
|
&source_bank,
|
||||||
|
@ -759,6 +800,15 @@ mod tests {
|
||||||
max_swap_fn,
|
max_swap_fn,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.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 {
|
if source_amount == I80F48::MAX {
|
||||||
return (f64::MAX, f64::MAX, f64::MAX, f64::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
|
// At this unlikely price it's healthy to swap infinitely
|
||||||
assert_eq!(
|
assert!(find_max_swap(&health_cache, 0, 1, 50.0, 1.5, banks).0 > 1e16);
|
||||||
find_max_swap(&health_cache, 0, 1, 50.0, 1.5, banks).0,
|
|
||||||
f64::MAX
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
@ -467,6 +467,10 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
||||||
bank.check_net_borrows(*oracle_price)?;
|
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_approved_amount = 0;
|
||||||
bank.flash_loan_token_account_initial = u64::MAX;
|
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()?;
|
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 {
|
if is_v2_instruction {
|
||||||
let mut receiver_bank = receiver_bank_ai.load_mut::<Bank>()?;
|
let mut receiver_bank = receiver_bank_ai.load_mut::<Bank>()?;
|
||||||
let (base_bank, quote_bank) = match side {
|
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);
|
update_bank_potential_tokens_payer_only(serum, &mut payer_bank, &after_oo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enforce min vault to deposits ratio
|
// Track position before withdraw happens
|
||||||
let withdrawn_from_vault = I80F48::from(before_vault - after_vault);
|
let before_position_native = account
|
||||||
let position_native = account
|
|
||||||
.token_position_mut(payer_bank.token_index)?
|
.token_position_mut(payer_bank.token_index)?
|
||||||
.0
|
.0
|
||||||
.native(&payer_bank);
|
.native(&payer_bank);
|
||||||
|
|
||||||
// Charge the difference in vault balance to the user's account
|
// Charge the difference in vault balance to the user's account
|
||||||
|
// (must be done before limit checks like deposit limit)
|
||||||
let vault_difference = {
|
let vault_difference = {
|
||||||
apply_vault_difference(
|
apply_vault_difference(
|
||||||
ctx.accounts.account.key(),
|
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
|
// Payer bank safety checks like reduce-only, net borrows, vault-to-deposits ratio
|
||||||
let payer_bank_oracle =
|
let payer_bank_oracle =
|
||||||
payer_bank.oracle_price(&AccountInfoRef::borrow(&ctx.accounts.payer_oracle)?, None)?;
|
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!(
|
require_msg_typed!(
|
||||||
!payer_bank.are_borrows_reduce_only(),
|
!payer_bank.are_borrows_reduce_only(),
|
||||||
MangoError::TokenInReduceOnlyMode,
|
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
|
// We allow the incentive to be < 1 native token because of tokens like BTC, where 1 native token
|
||||||
// far exceeds the incentive value.
|
// far exceeds the incentive value.
|
||||||
let incentive = (I80F48::from(TCS_START_INCENTIVE) / sell_oracle_price)
|
let incentive = (I80F48::from(TCS_START_INCENTIVE) / sell_oracle_price)
|
||||||
.min(I80F48::from(tcs.remaining_sell()));
|
.min(I80F48::from(tcs.remaining_sell()))
|
||||||
// However, the tcs tracking is in u64 units. We need to live with the fact of
|
// 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.
|
// not accounting the incentive fee perfectly.
|
||||||
let incentive_native = incentive.clamp_to_u64();
|
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, _) =
|
let (liqor_sell_token, liqor_sell_raw_index, _) =
|
||||||
liqor.ensure_token_position(sell_token_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);
|
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);
|
let liqee_sell_post_balance = liqee_sell_token.native(sell_bank);
|
||||||
if liqee_sell_post_balance < 0 {
|
if liqee_sell_post_balance < 0 {
|
||||||
require!(
|
require!(
|
||||||
tcs.allow_creating_borrows(),
|
tcs.allow_creating_borrows(),
|
||||||
MangoError::TokenConditionalSwapCantPayIncentive
|
MangoError::TokenConditionalSwapCantPayIncentive
|
||||||
);
|
);
|
||||||
require!(
|
|
||||||
!sell_bank.are_borrows_reduce_only(),
|
|
||||||
MangoError::TokenInReduceOnlyMode
|
|
||||||
);
|
|
||||||
sell_bank.check_net_borrows(sell_oracle_price)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
health_cache
|
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 (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)?;
|
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 buy_transfer = buy_bank.checked_transfer_with_fee(
|
||||||
let liqor_buy_withdraw =
|
liqor_buy_token,
|
||||||
buy_bank.withdraw_with_fee(liqor_buy_token, buy_token_amount_i80f48, now_ts)?;
|
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_liqee_buy_token = liqee_buy_token.native(&buy_bank);
|
||||||
let post_liqor_buy_token = liqor_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)?;
|
liqee.token_position_mut(tcs.sell_token_index)?;
|
||||||
let (liqor_sell_token, liqor_sell_raw_index) =
|
let (liqor_sell_token, liqor_sell_raw_index) =
|
||||||
liqor.token_position_mut(tcs.sell_token_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,
|
liqor_sell_token,
|
||||||
I80F48::from(sell_token_amount_to_liqor),
|
I80F48::from(sell_token_amount_to_liqor),
|
||||||
now_ts,
|
now_ts,
|
||||||
|
sell_token_price,
|
||||||
)?;
|
)?;
|
||||||
let liqee_sell_withdraw = sell_bank.withdraw_with_fee(
|
let liqor_sell_active = sell_transfer.target_is_active;
|
||||||
liqee_sell_token,
|
|
||||||
I80F48::from(sell_token_amount_from_liqee),
|
|
||||||
now_ts,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
sell_bank.collected_fees_native += I80F48::from(maker_fee + taker_fee);
|
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_liqee_sell_token = liqee_sell_token.native(&sell_bank);
|
||||||
let post_liqor_sell_token = liqor_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;
|
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.
|
// 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).
|
// 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);
|
liqor.deactivate_token_position_and_log(liqor_buy_raw_index, liqor_key);
|
||||||
}
|
}
|
||||||
if !liqor_sell_active {
|
if !liqor_sell_active {
|
||||||
|
@ -382,24 +378,24 @@ fn action(
|
||||||
borrow_index: sell_bank.borrow_index.to_bits(),
|
borrow_index: sell_bank.borrow_index.to_bits(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if liqor_buy_withdraw.has_loan() {
|
if buy_transfer.has_loan() {
|
||||||
emit_stack(WithdrawLoanLog {
|
emit_stack(WithdrawLoanLog {
|
||||||
mango_group: liqee.fixed.group,
|
mango_group: liqee.fixed.group,
|
||||||
mango_account: liqor_key,
|
mango_account: liqor_key,
|
||||||
token_index: tcs.buy_token_index,
|
token_index: tcs.buy_token_index,
|
||||||
loan_amount: liqor_buy_withdraw.loan_amount.to_bits(),
|
loan_amount: buy_transfer.loan_amount.to_bits(),
|
||||||
loan_origination_fee: liqor_buy_withdraw.loan_origination_fee.to_bits(),
|
loan_origination_fee: buy_transfer.loan_origination_fee.to_bits(),
|
||||||
instruction: LoanOriginationFeeInstruction::TokenConditionalSwapTrigger,
|
instruction: LoanOriginationFeeInstruction::TokenConditionalSwapTrigger,
|
||||||
price: Some(buy_token_price.to_bits()),
|
price: Some(buy_token_price.to_bits()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if liqee_sell_withdraw.has_loan() {
|
if sell_transfer.has_loan() {
|
||||||
emit_stack(WithdrawLoanLog {
|
emit_stack(WithdrawLoanLog {
|
||||||
mango_group: liqee.fixed.group,
|
mango_group: liqee.fixed.group,
|
||||||
mango_account: liqee_key,
|
mango_account: liqee_key,
|
||||||
token_index: tcs.sell_token_index,
|
token_index: tcs.sell_token_index,
|
||||||
loan_amount: liqee_sell_withdraw.loan_amount.to_bits(),
|
loan_amount: sell_transfer.loan_amount.to_bits(),
|
||||||
loan_origination_fee: liqee_sell_withdraw.loan_origination_fee.to_bits(),
|
loan_origination_fee: sell_transfer.loan_origination_fee.to_bits(),
|
||||||
instruction: LoanOriginationFeeInstruction::TokenConditionalSwapTrigger,
|
instruction: LoanOriginationFeeInstruction::TokenConditionalSwapTrigger,
|
||||||
price: Some(sell_token_price.to_bits()),
|
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
|
// 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.
|
// 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())?,
|
&AccountInfoRef::borrow(self.oracle.as_ref())?,
|
||||||
bank.mint_decimals,
|
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)
|
// 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>();
|
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_asset_target_opt: Option<f32>,
|
||||||
maint_weight_shift_liab_target_opt: Option<f32>,
|
maint_weight_shift_liab_target_opt: Option<f32>,
|
||||||
maint_weight_shift_abort: bool,
|
maint_weight_shift_abort: bool,
|
||||||
|
set_fallback_oracle: bool, // unused, introduced in v0.22
|
||||||
|
deposit_limit_opt: Option<u64>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let group = ctx.accounts.group.load()?;
|
let group = ctx.accounts.group.load()?;
|
||||||
|
|
||||||
|
@ -436,6 +438,16 @@ pub fn token_edit(
|
||||||
bank.maint_weight_shift_duration_inv.is_positive(),
|
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
|
// account constraint #1
|
||||||
|
|
|
@ -41,6 +41,7 @@ pub fn token_register(
|
||||||
interest_curve_scaling: f32,
|
interest_curve_scaling: f32,
|
||||||
interest_target_utilization: f32,
|
interest_target_utilization: f32,
|
||||||
group_insurance_fund: bool,
|
group_insurance_fund: bool,
|
||||||
|
deposit_limit: u64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Require token 0 to be in the insurance token
|
// Require token 0 to be in the insurance token
|
||||||
if token_index == INSURANCE_TOKEN_INDEX {
|
if token_index == INSURANCE_TOKEN_INDEX {
|
||||||
|
@ -119,7 +120,9 @@ pub fn token_register(
|
||||||
maint_weight_shift_duration_inv: I80F48::ZERO,
|
maint_weight_shift_duration_inv: I80F48::ZERO,
|
||||||
maint_weight_shift_asset_target: I80F48::ZERO,
|
maint_weight_shift_asset_target: I80F48::ZERO,
|
||||||
maint_weight_shift_liab_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) =
|
if let Ok(oracle_price) =
|
||||||
|
|
|
@ -102,7 +102,9 @@ pub fn token_register_trustless(
|
||||||
maint_weight_shift_duration_inv: I80F48::ZERO,
|
maint_weight_shift_duration_inv: I80F48::ZERO,
|
||||||
maint_weight_shift_asset_target: I80F48::ZERO,
|
maint_weight_shift_asset_target: I80F48::ZERO,
|
||||||
maint_weight_shift_liab_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) =
|
if let Ok(oracle_price) =
|
||||||
|
|
|
@ -153,6 +153,7 @@ pub mod mango_v4 {
|
||||||
interest_curve_scaling: f32,
|
interest_curve_scaling: f32,
|
||||||
interest_target_utilization: f32,
|
interest_target_utilization: f32,
|
||||||
group_insurance_fund: bool,
|
group_insurance_fund: bool,
|
||||||
|
deposit_limit: u64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
#[cfg(feature = "enable-gpl")]
|
#[cfg(feature = "enable-gpl")]
|
||||||
instructions::token_register(
|
instructions::token_register(
|
||||||
|
@ -183,6 +184,7 @@ pub mod mango_v4 {
|
||||||
interest_curve_scaling,
|
interest_curve_scaling,
|
||||||
interest_target_utilization,
|
interest_target_utilization,
|
||||||
group_insurance_fund,
|
group_insurance_fund,
|
||||||
|
deposit_limit,
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -234,6 +236,8 @@ pub mod mango_v4 {
|
||||||
maint_weight_shift_asset_target_opt: Option<f32>,
|
maint_weight_shift_asset_target_opt: Option<f32>,
|
||||||
maint_weight_shift_liab_target_opt: Option<f32>,
|
maint_weight_shift_liab_target_opt: Option<f32>,
|
||||||
maint_weight_shift_abort: bool,
|
maint_weight_shift_abort: bool,
|
||||||
|
set_fallback_oracle: bool, // unused, introduced in v0.22
|
||||||
|
deposit_limit_opt: Option<u64>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
#[cfg(feature = "enable-gpl")]
|
#[cfg(feature = "enable-gpl")]
|
||||||
instructions::token_edit(
|
instructions::token_edit(
|
||||||
|
@ -272,6 +276,8 @@ pub mod mango_v4 {
|
||||||
maint_weight_shift_asset_target_opt,
|
maint_weight_shift_asset_target_opt,
|
||||||
maint_weight_shift_liab_target_opt,
|
maint_weight_shift_liab_target_opt,
|
||||||
maint_weight_shift_abort,
|
maint_weight_shift_abort,
|
||||||
|
set_fallback_oracle,
|
||||||
|
deposit_limit_opt,
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -636,6 +642,7 @@ pub mod mango_v4 {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// requires the receiver_bank in the health account list to be writable
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn serum3_place_order_v2(
|
pub fn serum3_place_order_v2(
|
||||||
ctx: Context<Serum3PlaceOrder>,
|
ctx: Context<Serum3PlaceOrder>,
|
||||||
|
|
|
@ -167,8 +167,12 @@ pub struct Bank {
|
||||||
pub maint_weight_shift_asset_target: I80F48,
|
pub maint_weight_shift_asset_target: I80F48,
|
||||||
pub maint_weight_shift_liab_target: I80F48,
|
pub maint_weight_shift_liab_target: I80F48,
|
||||||
|
|
||||||
#[derivative(Debug = "ignore")]
|
pub fallback_oracle: Pubkey, // unused, introduced in v0.22
|
||||||
pub reserved: [u8; 2008],
|
|
||||||
|
/// zero means none, in token native
|
||||||
|
pub deposit_limit: u64,
|
||||||
|
|
||||||
|
pub reserved: [u8; 1968],
|
||||||
}
|
}
|
||||||
const_assert_eq!(
|
const_assert_eq!(
|
||||||
size_of::<Bank>(),
|
size_of::<Bank>(),
|
||||||
|
@ -203,7 +207,9 @@ const_assert_eq!(
|
||||||
+ 8 * 2
|
+ 8 * 2
|
||||||
+ 8 * 2
|
+ 8 * 2
|
||||||
+ 16 * 3
|
+ 16 * 3
|
||||||
+ 2008
|
+ 32
|
||||||
|
+ 8
|
||||||
|
+ 1968
|
||||||
);
|
);
|
||||||
const_assert_eq!(size_of::<Bank>(), 3064);
|
const_assert_eq!(size_of::<Bank>(), 3064);
|
||||||
const_assert_eq!(size_of::<Bank>() % 8, 0);
|
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 {
|
impl Bank {
|
||||||
pub fn from_existing_bank(
|
pub fn from_existing_bank(
|
||||||
existing_bank: &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_duration_inv: existing_bank.maint_weight_shift_duration_inv,
|
||||||
maint_weight_shift_asset_target: existing_bank.maint_weight_shift_asset_target,
|
maint_weight_shift_asset_target: existing_bank.maint_weight_shift_asset_target,
|
||||||
maint_weight_shift_liab_target: existing_bank.maint_weight_shift_liab_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.
|
/// 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.
|
/// 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(())
|
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(
|
pub fn update_cumulative_interest(
|
||||||
&self,
|
&self,
|
||||||
position: &mut TokenPosition,
|
position: &mut TokenPosition,
|
||||||
|
@ -1177,6 +1299,148 @@ mod tests {
|
||||||
Ok(())
|
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]
|
#[test]
|
||||||
fn test_compute_new_avg_utilization() {
|
fn test_compute_new_avg_utilization() {
|
||||||
let mut bank = Bank::zeroed();
|
let mut bank = Bank::zeroed();
|
||||||
|
|
|
@ -511,3 +511,174 @@ async fn test_bank_maint_weight_shift() -> Result<(), TransportError> {
|
||||||
|
|
||||||
Ok(())
|
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(())
|
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(())
|
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 {
|
struct CommonSetup {
|
||||||
group_with_tokens: GroupWithTokens,
|
group_with_tokens: GroupWithTokens,
|
||||||
serum_market_cookie: SpotMarketCookie,
|
serum_market_cookie: SpotMarketCookie,
|
||||||
|
@ -1561,6 +1721,14 @@ struct CommonSetup {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn common_setup(context: &TestContext, deposit_amount: u64) -> 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 admin = TestKeypair::new();
|
||||||
let owner = context.users[0].key;
|
let owner = context.users[0].key;
|
||||||
let payer = context.users[1].key;
|
let payer = context.users[1].key;
|
||||||
|
@ -1635,17 +1803,19 @@ async fn common_setup(context: &TestContext, deposit_amount: u64) -> CommonSetup
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
// to have enough funds in the vaults
|
// to have enough funds in the vaults
|
||||||
create_funded_account(
|
if vault_funding > 0 {
|
||||||
&solana,
|
create_funded_account(
|
||||||
group,
|
&solana,
|
||||||
owner,
|
group,
|
||||||
3,
|
owner,
|
||||||
&context.users[1],
|
3,
|
||||||
mints,
|
&context.users[1],
|
||||||
10000000,
|
mints,
|
||||||
0,
|
10000000,
|
||||||
)
|
0,
|
||||||
.await;
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
let open_orders = send_tx(
|
let open_orders = send_tx(
|
||||||
solana,
|
solana,
|
||||||
|
|
|
@ -1013,3 +1013,146 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
||||||
|
|
||||||
Ok(())
|
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
|
.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
|
/// Build a transaction from multiple instructions
|
||||||
pub struct ClientTransaction {
|
pub struct ClientTransaction {
|
||||||
solana: Arc<SolanaCookie>,
|
solana: Arc<SolanaCookie>,
|
||||||
|
@ -111,6 +132,28 @@ impl<'a> ClientTransaction {
|
||||||
Ok(())
|
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
|
// Tx error still returns success
|
||||||
pub async fn send_get_metadata(
|
pub async fn send_get_metadata(
|
||||||
&self,
|
&self,
|
||||||
|
@ -1011,6 +1054,7 @@ impl ClientInstruction for TokenRegisterInstruction {
|
||||||
interest_curve_scaling: 1.0,
|
interest_curve_scaling: 1.0,
|
||||||
interest_target_utilization: 0.5,
|
interest_target_utilization: 0.5,
|
||||||
group_insurance_fund: true,
|
group_insurance_fund: true,
|
||||||
|
deposit_limit: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let bank = Pubkey::find_program_address(
|
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_asset_target_opt: None,
|
||||||
maint_weight_shift_liab_target_opt: None,
|
maint_weight_shift_liab_target_opt: None,
|
||||||
maint_weight_shift_abort: false,
|
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 {
|
match result {
|
||||||
Ok(_) => assert!(false, "No error returned"),
|
Ok(_) => assert!(false, "No error returned"),
|
||||||
Err(TransportError::TransactionError(tx_err)) => match tx_err {
|
Err(TransportError::TransactionError(TransactionError::InstructionError(
|
||||||
TransactionError::InstructionError(_, err) => match err {
|
_,
|
||||||
InstructionError::Custom(err_num) => {
|
InstructionError::Custom(err_num),
|
||||||
assert_eq!(*err_num, expected_error, "{}", comment);
|
))) => {
|
||||||
}
|
assert_eq!(*err_num, expected_error, "{}", comment);
|
||||||
_ => assert!(false, "Not a mango error"),
|
}
|
||||||
},
|
|
||||||
_ => assert!(false, "Not a mango error"),
|
|
||||||
},
|
|
||||||
_ => assert!(false, "Not a mango error"),
|
_ => assert!(false, "Not a mango error"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -132,12 +132,13 @@ export class Bank implements BankForHealth {
|
||||||
flashLoanSwapFeeRate: number;
|
flashLoanSwapFeeRate: number;
|
||||||
interestTargetUtilization: number;
|
interestTargetUtilization: number;
|
||||||
interestCurveScaling: number;
|
interestCurveScaling: number;
|
||||||
depositsInSerum: BN;
|
potentialSerumTokens: BN;
|
||||||
maintWeightShiftStart: BN;
|
maintWeightShiftStart: BN;
|
||||||
maintWeightShiftEnd: BN;
|
maintWeightShiftEnd: BN;
|
||||||
maintWeightShiftDurationInv: I80F48Dto;
|
maintWeightShiftDurationInv: I80F48Dto;
|
||||||
maintWeightShiftAssetTarget: I80F48Dto;
|
maintWeightShiftAssetTarget: I80F48Dto;
|
||||||
maintWeightShiftLiabTarget: I80F48Dto;
|
maintWeightShiftLiabTarget: I80F48Dto;
|
||||||
|
depositLimit: BN;
|
||||||
},
|
},
|
||||||
): Bank {
|
): Bank {
|
||||||
return new Bank(
|
return new Bank(
|
||||||
|
@ -191,12 +192,13 @@ export class Bank implements BankForHealth {
|
||||||
obj.flashLoanSwapFeeRate,
|
obj.flashLoanSwapFeeRate,
|
||||||
obj.interestTargetUtilization,
|
obj.interestTargetUtilization,
|
||||||
obj.interestCurveScaling,
|
obj.interestCurveScaling,
|
||||||
obj.depositsInSerum,
|
obj.potentialSerumTokens,
|
||||||
obj.maintWeightShiftStart,
|
obj.maintWeightShiftStart,
|
||||||
obj.maintWeightShiftEnd,
|
obj.maintWeightShiftEnd,
|
||||||
obj.maintWeightShiftDurationInv,
|
obj.maintWeightShiftDurationInv,
|
||||||
obj.maintWeightShiftAssetTarget,
|
obj.maintWeightShiftAssetTarget,
|
||||||
obj.maintWeightShiftLiabTarget,
|
obj.maintWeightShiftLiabTarget,
|
||||||
|
obj.depositLimit,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,12 +253,13 @@ export class Bank implements BankForHealth {
|
||||||
public flashLoanSwapFeeRate: number,
|
public flashLoanSwapFeeRate: number,
|
||||||
public interestTargetUtilization: number,
|
public interestTargetUtilization: number,
|
||||||
public interestCurveScaling: number,
|
public interestCurveScaling: number,
|
||||||
public depositsInSerum: BN,
|
public potentialSerumTokens: BN,
|
||||||
public maintWeightShiftStart: BN,
|
public maintWeightShiftStart: BN,
|
||||||
public maintWeightShiftEnd: BN,
|
public maintWeightShiftEnd: BN,
|
||||||
maintWeightShiftDurationInv: I80F48Dto,
|
maintWeightShiftDurationInv: I80F48Dto,
|
||||||
maintWeightShiftAssetTarget: I80F48Dto,
|
maintWeightShiftAssetTarget: I80F48Dto,
|
||||||
maintWeightShiftLiabTarget: I80F48Dto,
|
maintWeightShiftLiabTarget: I80F48Dto,
|
||||||
|
public depositLimit: BN,
|
||||||
) {
|
) {
|
||||||
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
|
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
|
||||||
this.oracleConfig = {
|
this.oracleConfig = {
|
||||||
|
|
|
@ -419,6 +419,7 @@ export class MangoClient {
|
||||||
params.interestCurveScaling,
|
params.interestCurveScaling,
|
||||||
params.interestTargetUtilization,
|
params.interestTargetUtilization,
|
||||||
params.groupInsuranceFund,
|
params.groupInsuranceFund,
|
||||||
|
params.depositLimit,
|
||||||
)
|
)
|
||||||
.accounts({
|
.accounts({
|
||||||
group: group.publicKey,
|
group: group.publicKey,
|
||||||
|
@ -501,6 +502,8 @@ export class MangoClient {
|
||||||
params.maintWeightShiftAssetTarget,
|
params.maintWeightShiftAssetTarget,
|
||||||
params.maintWeightShiftLiabTarget,
|
params.maintWeightShiftLiabTarget,
|
||||||
params.maintWeightShiftAbort ?? false,
|
params.maintWeightShiftAbort ?? false,
|
||||||
|
false, // setFallbackOracle, unused
|
||||||
|
params.depositLimit,
|
||||||
)
|
)
|
||||||
.accounts({
|
.accounts({
|
||||||
group: group.publicKey,
|
group: group.publicKey,
|
||||||
|
@ -1568,9 +1571,10 @@ export class MangoClient {
|
||||||
quoteBank: Bank,
|
quoteBank: Bank,
|
||||||
marketIndex: number,
|
marketIndex: number,
|
||||||
name: string,
|
name: string,
|
||||||
|
oraclePriceBand: number,
|
||||||
): Promise<MangoSignatureStatus> {
|
): Promise<MangoSignatureStatus> {
|
||||||
const ix = await this.program.methods
|
const ix = await this.program.methods
|
||||||
.serum3RegisterMarket(marketIndex, name)
|
.serum3RegisterMarket(marketIndex, name, oraclePriceBand)
|
||||||
.accounts({
|
.accounts({
|
||||||
group: group.publicKey,
|
group: group.publicKey,
|
||||||
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
|
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||||
|
@ -1590,11 +1594,12 @@ export class MangoClient {
|
||||||
reduceOnly: boolean | null,
|
reduceOnly: boolean | null,
|
||||||
forceClose: boolean | null,
|
forceClose: boolean | null,
|
||||||
name: string | null,
|
name: string | null,
|
||||||
|
oraclePriceBand: number | null,
|
||||||
): Promise<MangoSignatureStatus> {
|
): Promise<MangoSignatureStatus> {
|
||||||
const serum3Market =
|
const serum3Market =
|
||||||
group.serum3MarketsMapByMarketIndex.get(serum3MarketIndex);
|
group.serum3MarketsMapByMarketIndex.get(serum3MarketIndex);
|
||||||
const ix = await this.program.methods
|
const ix = await this.program.methods
|
||||||
.serum3EditMarket(reduceOnly, forceClose, name)
|
.serum3EditMarket(reduceOnly, forceClose, name, oraclePriceBand)
|
||||||
.accounts({
|
.accounts({
|
||||||
group: group.publicKey,
|
group: group.publicKey,
|
||||||
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
|
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||||
|
|
|
@ -27,6 +27,7 @@ export interface TokenRegisterParams {
|
||||||
flashLoanSwapFeeRate: number;
|
flashLoanSwapFeeRate: number;
|
||||||
interestCurveScaling: number;
|
interestCurveScaling: number;
|
||||||
interestTargetUtilization: number;
|
interestTargetUtilization: number;
|
||||||
|
depositLimit: BN;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DefaultTokenRegisterParams: TokenRegisterParams = {
|
export const DefaultTokenRegisterParams: TokenRegisterParams = {
|
||||||
|
@ -64,6 +65,7 @@ export const DefaultTokenRegisterParams: TokenRegisterParams = {
|
||||||
flashLoanSwapFeeRate: 0.0005,
|
flashLoanSwapFeeRate: 0.0005,
|
||||||
interestCurveScaling: 4.0,
|
interestCurveScaling: 4.0,
|
||||||
interestTargetUtilization: 0.5,
|
interestTargetUtilization: 0.5,
|
||||||
|
depositLimit: new BN(0),
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface TokenEditParams {
|
export interface TokenEditParams {
|
||||||
|
@ -101,6 +103,7 @@ export interface TokenEditParams {
|
||||||
maintWeightShiftAssetTarget: number | null;
|
maintWeightShiftAssetTarget: number | null;
|
||||||
maintWeightShiftLiabTarget: number | null;
|
maintWeightShiftLiabTarget: number | null;
|
||||||
maintWeightShiftAbort: boolean | null;
|
maintWeightShiftAbort: boolean | null;
|
||||||
|
depositLimit: BN | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NullTokenEditParams: TokenEditParams = {
|
export const NullTokenEditParams: TokenEditParams = {
|
||||||
|
@ -138,6 +141,7 @@ export const NullTokenEditParams: TokenEditParams = {
|
||||||
maintWeightShiftAssetTarget: null,
|
maintWeightShiftAssetTarget: null,
|
||||||
maintWeightShiftLiabTarget: null,
|
maintWeightShiftLiabTarget: null,
|
||||||
maintWeightShiftAbort: null,
|
maintWeightShiftAbort: null,
|
||||||
|
depositLimit: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface PerpEditParams {
|
export interface PerpEditParams {
|
||||||
|
|
|
@ -614,6 +614,10 @@ export type MangoV4 = {
|
||||||
{
|
{
|
||||||
"name": "groupInsuranceFund",
|
"name": "groupInsuranceFund",
|
||||||
"type": "bool"
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "depositLimit",
|
||||||
|
"type": "u64"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -988,6 +992,12 @@ export type MangoV4 = {
|
||||||
{
|
{
|
||||||
"name": "maintWeightShiftAbort",
|
"name": "maintWeightShiftAbort",
|
||||||
"type": "bool"
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "depositLimitOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "u64"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2357,6 +2367,10 @@ export type MangoV4 = {
|
||||||
{
|
{
|
||||||
"name": "name",
|
"name": "name",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "oraclePriceBand",
|
||||||
|
"type": "f32"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2400,6 +2414,12 @@ export type MangoV4 = {
|
||||||
"type": {
|
"type": {
|
||||||
"option": "string"
|
"option": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "oraclePriceBandOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "f32"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2742,6 +2762,9 @@ export type MangoV4 = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "serum3PlaceOrderV2",
|
"name": "serum3PlaceOrderV2",
|
||||||
|
"docs": [
|
||||||
|
"requires the receiver_bank in the health account list to be writable"
|
||||||
|
],
|
||||||
"accounts": [
|
"accounts": [
|
||||||
{
|
{
|
||||||
"name": "group",
|
"name": "group",
|
||||||
|
@ -7266,11 +7289,9 @@ export type MangoV4 = {
|
||||||
"name": "potentialSerumTokens",
|
"name": "potentialSerumTokens",
|
||||||
"docs": [
|
"docs": [
|
||||||
"Largest amount of tokens that might be added the the bank based on",
|
"Largest amount of tokens that might be added the the bank based on",
|
||||||
"serum open order execution.",
|
"serum open order execution."
|
||||||
"",
|
|
||||||
"Can be negative with multiple banks, then it'd need to be balanced in the keeper."
|
|
||||||
],
|
],
|
||||||
"type": "i64"
|
"type": "u64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "maintWeightShiftStart",
|
"name": "maintWeightShiftStart",
|
||||||
|
@ -7298,12 +7319,19 @@ export type MangoV4 = {
|
||||||
"defined": "I80F48"
|
"defined": "I80F48"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "depositLimit",
|
||||||
|
"docs": [
|
||||||
|
"zero means none, in token native"
|
||||||
|
],
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "reserved",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
2008
|
2000
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8402,10 +8430,20 @@ export type MangoV4 = {
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"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",
|
"name": "registrationTime",
|
||||||
"type": "u64"
|
"type": "u64"
|
||||||
|
@ -13467,6 +13505,16 @@ export type MangoV4 = {
|
||||||
"code": 6060,
|
"code": 6060,
|
||||||
"name": "HealthAccountBankNotWritable",
|
"name": "HealthAccountBankNotWritable",
|
||||||
"msg": "a bank in the health account list should be writable but is not"
|
"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",
|
"name": "groupInsuranceFund",
|
||||||
"type": "bool"
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "depositLimit",
|
||||||
|
"type": "u64"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -14461,6 +14513,12 @@ export const IDL: MangoV4 = {
|
||||||
{
|
{
|
||||||
"name": "maintWeightShiftAbort",
|
"name": "maintWeightShiftAbort",
|
||||||
"type": "bool"
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "depositLimitOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "u64"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -15830,6 +15888,10 @@ export const IDL: MangoV4 = {
|
||||||
{
|
{
|
||||||
"name": "name",
|
"name": "name",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "oraclePriceBand",
|
||||||
|
"type": "f32"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -15873,6 +15935,12 @@ export const IDL: MangoV4 = {
|
||||||
"type": {
|
"type": {
|
||||||
"option": "string"
|
"option": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "oraclePriceBandOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "f32"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -16215,6 +16283,9 @@ export const IDL: MangoV4 = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "serum3PlaceOrderV2",
|
"name": "serum3PlaceOrderV2",
|
||||||
|
"docs": [
|
||||||
|
"requires the receiver_bank in the health account list to be writable"
|
||||||
|
],
|
||||||
"accounts": [
|
"accounts": [
|
||||||
{
|
{
|
||||||
"name": "group",
|
"name": "group",
|
||||||
|
@ -20739,11 +20810,9 @@ export const IDL: MangoV4 = {
|
||||||
"name": "potentialSerumTokens",
|
"name": "potentialSerumTokens",
|
||||||
"docs": [
|
"docs": [
|
||||||
"Largest amount of tokens that might be added the the bank based on",
|
"Largest amount of tokens that might be added the the bank based on",
|
||||||
"serum open order execution.",
|
"serum open order execution."
|
||||||
"",
|
|
||||||
"Can be negative with multiple banks, then it'd need to be balanced in the keeper."
|
|
||||||
],
|
],
|
||||||
"type": "i64"
|
"type": "u64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "maintWeightShiftStart",
|
"name": "maintWeightShiftStart",
|
||||||
|
@ -20771,12 +20840,19 @@ export const IDL: MangoV4 = {
|
||||||
"defined": "I80F48"
|
"defined": "I80F48"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "depositLimit",
|
||||||
|
"docs": [
|
||||||
|
"zero means none, in token native"
|
||||||
|
],
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "reserved",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
2008
|
2000
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21875,10 +21951,20 @@ export const IDL: MangoV4 = {
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"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",
|
"name": "registrationTime",
|
||||||
"type": "u64"
|
"type": "u64"
|
||||||
|
@ -26940,6 +27026,16 @@ export const IDL: MangoV4 = {
|
||||||
"code": 6060,
|
"code": 6060,
|
||||||
"name": "HealthAccountBankNotWritable",
|
"name": "HealthAccountBankNotWritable",
|
||||||
"msg": "a bank in the health account list should be writable but is not"
|
"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