Merge branch 'deploy-v0.21.0' into deploy
This commit is contained in:
commit
e75d279b15
57
CHANGELOG.md
57
CHANGELOG.md
|
@ -4,7 +4,60 @@ Update this for each program release and mainnet deployment.
|
||||||
|
|
||||||
## not on mainnet
|
## not on mainnet
|
||||||
|
|
||||||
### v0.20.0, 2023-10-
|
### v0.21.0, 2023-12-
|
||||||
|
|
||||||
|
- Introduce deposit limits (#806)
|
||||||
|
|
||||||
|
The DAO can now configure hard deposit limits per token. They can be used in
|
||||||
|
conjunction with the previous soft limits to restrict how much of a token can
|
||||||
|
be on the platform providing collateral weight.
|
||||||
|
|
||||||
|
- Improve OpenBook order tracking and price bands (#805)
|
||||||
|
|
||||||
|
In order for hard deposit limits to work, OpenBook orders need to be tracked
|
||||||
|
and potentially restricted. The DAO can now configure a band around the oracle
|
||||||
|
price and new bids and asks that don't fall within this band will be rejected.
|
||||||
|
|
||||||
|
- SerumPlaceOrderV2 breaking change (#805)
|
||||||
|
|
||||||
|
A new instruction for placing orders on OpenBook markets is introduced. The
|
||||||
|
old instruction should be disabled shortly after release.
|
||||||
|
|
||||||
|
- Changing token maint weights over time (#780)
|
||||||
|
|
||||||
|
The DAO can now trigger a gradual change in token maint weights. This allows
|
||||||
|
it to make maint weights less favorable without potentially causing many
|
||||||
|
liquidations at the same time.
|
||||||
|
|
||||||
|
- Changed perp settlement incentives (#771)
|
||||||
|
|
||||||
|
The incentives were too high when the user account was close to liquidation.
|
||||||
|
The DAO had previously reduced the percentage amount as a mitigation.
|
||||||
|
|
||||||
|
With this change:
|
||||||
|
- low-health settlement incentives are capped at 2x the flat fee, removing
|
||||||
|
unlimited percentual incentive fees entirely
|
||||||
|
- incentives are only paid if at least 1% of position value is settled,
|
||||||
|
avoiding the incentive to settle accounts with large positions very frequently
|
||||||
|
|
||||||
|
- More configurable token interest rate curve (#755)
|
||||||
|
|
||||||
|
The scaling factor and target utilization are now stored separately, giving the
|
||||||
|
DAO more flexibility for configuration.
|
||||||
|
|
||||||
|
- Delegates can now deposit even when a new token position needs to be created (#775)
|
||||||
|
- TokenRegister: Add argument for insurance (#782)
|
||||||
|
- Close zero token positions when user asks to withdraw everything (#793)
|
||||||
|
- Fix default parameters for fast listing tokens (#804)
|
||||||
|
- Disable TokenAddBank instruction, which was unused (#803)
|
||||||
|
- Significantly reduce program heap use (#787, #785)
|
||||||
|
- Reduce compute use of OpenBook health computations (#750)
|
||||||
|
|
||||||
|
## mainnet
|
||||||
|
|
||||||
|
### v0.20.0, 2023-11-8
|
||||||
|
|
||||||
|
Deployment: Nov 8, 2023 at 10:44:24 Central European Standard Time, https://explorer.solana.com/tx/4LM5NJAa71tjjKT4a7MXVVsautU1DNvszbXp2ufeps9gMrksRh9pURRiacoyCEgW9gdBYJb1W3TL6o7dzDcUVmVH
|
||||||
|
|
||||||
- Token conditional swaps: Add two auction mechanisms (#717)
|
- Token conditional swaps: Add two auction mechanisms (#717)
|
||||||
|
|
||||||
|
@ -46,8 +99,6 @@ Update this for each program release and mainnet deployment.
|
||||||
- Fix computing maximum allowed amount when swapping zero asset-weight tokens (#699)
|
- Fix computing maximum allowed amount when swapping zero asset-weight tokens (#699)
|
||||||
- Fix too-strict validation of max rate on token edit (#734)
|
- Fix too-strict validation of max rate on token edit (#734)
|
||||||
|
|
||||||
## mainnet
|
|
||||||
|
|
||||||
### v0.19.1, 2023-9-16
|
### v0.19.1, 2023-9-16
|
||||||
|
|
||||||
Deployment: Sep 16, 2023 at 11:20:20 Central European Summer Time, https://explorer.solana.com/tx/K9BJ1uDBH6Xe8erhS6C8Rmz6k6V1cKJ8z6wNmf4DV2aF5Woin4H5xXKj1ypTNDSTccNvcsAUTHStoai3k2hYY5E
|
Deployment: Sep 16, 2023 at 11:20:20 Central European Summer Time, https://explorer.solana.com/tx/K9BJ1uDBH6Xe8erhS6C8Rmz6k6V1cKJ8z6wNmf4DV2aF5Woin4H5xXKj1ypTNDSTccNvcsAUTHStoai3k2hYY5E
|
||||||
|
|
|
@ -3301,7 +3301,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mango-v4"
|
name = "mango-v4"
|
||||||
version = "0.20.0"
|
version = "0.21.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anchor-lang",
|
"anchor-lang",
|
||||||
"anchor-spl",
|
"anchor-spl",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -5,6 +5,8 @@ use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
|
||||||
use mango_v4::health::{FixedOrderAccountRetriever, HealthCache};
|
use mango_v4::health::{FixedOrderAccountRetriever, HealthCache};
|
||||||
use mango_v4::state::MangoAccountValue;
|
use mango_v4::state::MangoAccountValue;
|
||||||
|
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
context: &MangoGroupContext,
|
context: &MangoGroupContext,
|
||||||
account_fetcher: &impl AccountFetcher,
|
account_fetcher: &impl AccountFetcher,
|
||||||
|
@ -33,7 +35,9 @@ pub async fn new(
|
||||||
begin_serum3: active_token_len * 2 + active_perp_len * 2,
|
begin_serum3: active_token_len * 2 + active_perp_len * 2,
|
||||||
staleness_slot: None,
|
staleness_slot: None,
|
||||||
};
|
};
|
||||||
mango_v4::health::new_health_cache(&account.borrow(), &retriever).context("make health cache")
|
let now_ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
||||||
|
mango_v4::health::new_health_cache(&account.borrow(), &retriever, now_ts)
|
||||||
|
.context("make health cache")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_sync(
|
pub fn new_sync(
|
||||||
|
@ -64,5 +68,7 @@ pub fn new_sync(
|
||||||
begin_serum3: active_token_len * 2 + active_perp_len * 2,
|
begin_serum3: active_token_len * 2 + active_perp_len * 2,
|
||||||
staleness_slot: None,
|
staleness_slot: None,
|
||||||
};
|
};
|
||||||
mango_v4::health::new_health_cache(&account.borrow(), &retriever).context("make health cache")
|
let now_ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
||||||
|
mango_v4::health::new_health_cache(&account.borrow(), &retriever, now_ts)
|
||||||
|
.context("make health cache")
|
||||||
}
|
}
|
||||||
|
|
400
mango_v4.json
400
mango_v4.json
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "0.20.0",
|
"version": "0.21.0",
|
||||||
"name": "mango_v4",
|
"name": "mango_v4",
|
||||||
"instructions": [
|
"instructions": [
|
||||||
{
|
{
|
||||||
|
@ -602,6 +602,22 @@
|
||||||
{
|
{
|
||||||
"name": "flashLoanSwapFeeRate",
|
"name": "flashLoanSwapFeeRate",
|
||||||
"type": "f32"
|
"type": "f32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interestCurveScaling",
|
||||||
|
"type": "f32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interestTargetUtilization",
|
||||||
|
"type": "f32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "groupInsuranceFund",
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "depositLimit",
|
||||||
|
"type": "u64"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -936,6 +952,56 @@
|
||||||
"type": {
|
"type": {
|
||||||
"option": "f32"
|
"option": "f32"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interestCurveScalingOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "f32"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interestTargetUtilizationOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "f32"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maintWeightShiftStartOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "u64"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maintWeightShiftEndOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "u64"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maintWeightShiftAssetTargetOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "f32"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maintWeightShiftLiabTargetOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "f32"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maintWeightShiftAbort",
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "setFallbackOracle",
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "depositLimitOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "u64"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1817,8 +1883,7 @@
|
||||||
"isMut": true,
|
"isMut": true,
|
||||||
"isSigner": false,
|
"isSigner": false,
|
||||||
"relations": [
|
"relations": [
|
||||||
"group",
|
"group"
|
||||||
"owner"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2306,6 +2371,10 @@
|
||||||
{
|
{
|
||||||
"name": "name",
|
"name": "name",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "oraclePriceBand",
|
||||||
|
"type": "f32"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2349,6 +2418,12 @@
|
||||||
"type": {
|
"type": {
|
||||||
"option": "string"
|
"option": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "oraclePriceBandOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "f32"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2689,6 +2764,164 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "serum3PlaceOrderV2",
|
||||||
|
"docs": [
|
||||||
|
"requires the receiver_bank in the health account list to be writable"
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "group",
|
||||||
|
"isMut": false,
|
||||||
|
"isSigner": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "account",
|
||||||
|
"isMut": true,
|
||||||
|
"isSigner": false,
|
||||||
|
"relations": [
|
||||||
|
"group"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "owner",
|
||||||
|
"isMut": false,
|
||||||
|
"isSigner": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "openOrders",
|
||||||
|
"isMut": true,
|
||||||
|
"isSigner": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "serumMarket",
|
||||||
|
"isMut": false,
|
||||||
|
"isSigner": false,
|
||||||
|
"relations": [
|
||||||
|
"group",
|
||||||
|
"serum_program",
|
||||||
|
"serum_market_external"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "serumProgram",
|
||||||
|
"isMut": false,
|
||||||
|
"isSigner": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "serumMarketExternal",
|
||||||
|
"isMut": true,
|
||||||
|
"isSigner": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "marketBids",
|
||||||
|
"isMut": true,
|
||||||
|
"isSigner": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "marketAsks",
|
||||||
|
"isMut": true,
|
||||||
|
"isSigner": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "marketEventQueue",
|
||||||
|
"isMut": true,
|
||||||
|
"isSigner": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "marketRequestQueue",
|
||||||
|
"isMut": true,
|
||||||
|
"isSigner": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "marketBaseVault",
|
||||||
|
"isMut": true,
|
||||||
|
"isSigner": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "marketQuoteVault",
|
||||||
|
"isMut": true,
|
||||||
|
"isSigner": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "marketVaultSigner",
|
||||||
|
"isMut": false,
|
||||||
|
"isSigner": false,
|
||||||
|
"docs": [
|
||||||
|
"needed for the automatic settle_funds call"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "payerBank",
|
||||||
|
"isMut": true,
|
||||||
|
"isSigner": false,
|
||||||
|
"docs": [
|
||||||
|
"The bank that pays for the order, if necessary"
|
||||||
|
],
|
||||||
|
"relations": [
|
||||||
|
"group"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "payerVault",
|
||||||
|
"isMut": true,
|
||||||
|
"isSigner": false,
|
||||||
|
"docs": [
|
||||||
|
"The bank vault that pays for the order, if necessary"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "payerOracle",
|
||||||
|
"isMut": false,
|
||||||
|
"isSigner": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tokenProgram",
|
||||||
|
"isMut": false,
|
||||||
|
"isSigner": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "side",
|
||||||
|
"type": {
|
||||||
|
"defined": "Serum3Side"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "limitPrice",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maxBaseQty",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maxNativeQuoteQtyIncludingFees",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "selfTradeBehavior",
|
||||||
|
"type": {
|
||||||
|
"defined": "Serum3SelfTradeBehavior"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "orderType",
|
||||||
|
"type": {
|
||||||
|
"defined": "Serum3OrderType"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clientOrderId",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "limit",
|
||||||
|
"type": "u16"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "serum3CancelOrder",
|
"name": "serum3CancelOrder",
|
||||||
"accounts": [
|
"accounts": [
|
||||||
|
@ -7057,15 +7290,56 @@
|
||||||
"type": "f64"
|
"type": "f64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "depositsInSerum",
|
"name": "potentialSerumTokens",
|
||||||
"type": "i64"
|
"docs": [
|
||||||
|
"Largest amount of tokens that might be added the the bank based on",
|
||||||
|
"serum open order execution."
|
||||||
|
],
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maintWeightShiftStart",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maintWeightShiftEnd",
|
||||||
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maintWeightShiftDurationInv",
|
||||||
|
"type": {
|
||||||
|
"defined": "I80F48"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maintWeightShiftAssetTarget",
|
||||||
|
"type": {
|
||||||
|
"defined": "I80F48"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maintWeightShiftLiabTarget",
|
||||||
|
"type": {
|
||||||
|
"defined": "I80F48"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fallbackOracle",
|
||||||
|
"type": "publicKey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "depositLimit",
|
||||||
|
"docs": [
|
||||||
|
"zero means none, in token native"
|
||||||
|
],
|
||||||
|
"type": "u64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "reserved",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
2072
|
1968
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7990,7 +8264,7 @@
|
||||||
"name": "settleFeeFlat",
|
"name": "settleFeeFlat",
|
||||||
"docs": [
|
"docs": [
|
||||||
"In native units of settlement token, given to each settle call above the",
|
"In native units of settlement token, given to each settle call above the",
|
||||||
"settle_fee_amount_threshold."
|
"settle_fee_amount_threshold if settling at least 1% of perp base pos value."
|
||||||
],
|
],
|
||||||
"type": "f32"
|
"type": "f32"
|
||||||
},
|
},
|
||||||
|
@ -8004,7 +8278,8 @@
|
||||||
{
|
{
|
||||||
"name": "settleFeeFractionLowHealth",
|
"name": "settleFeeFractionLowHealth",
|
||||||
"docs": [
|
"docs": [
|
||||||
"Fraction of pnl to pay out as fee if +pnl account has low health."
|
"Fraction of pnl to pay out as fee if +pnl account has low health.",
|
||||||
|
"(limited to 2x settle_fee_flat)"
|
||||||
],
|
],
|
||||||
"type": "f32"
|
"type": "f32"
|
||||||
},
|
},
|
||||||
|
@ -8163,10 +8438,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"
|
||||||
|
@ -8602,26 +8887,41 @@
|
||||||
"type": "f64"
|
"type": "f64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "baseDepositsReserved",
|
"name": "potentialBaseTokens",
|
||||||
"docs": [
|
"docs": [
|
||||||
"Tracks the amount of deposits that flowed into the serum open orders account.",
|
"An overestimate of the amount of tokens that might flow out of the open orders account.",
|
||||||
"",
|
"",
|
||||||
"The bank still considers these amounts user deposits (see deposits_in_serum)",
|
"The bank still considers these amounts user deposits (see Bank::potential_serum_tokens)",
|
||||||
"and they need to be deducted from there when they flow back into the bank",
|
"and that value needs to be updated in conjunction with these numbers.",
|
||||||
"as real tokens."
|
"",
|
||||||
|
"This estimation is based on the amount of tokens in the open orders account",
|
||||||
|
"(see update_bank_potential_tokens() in serum3_place_order and settle)"
|
||||||
],
|
],
|
||||||
"type": "u64"
|
"type": "u64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "quoteDepositsReserved",
|
"name": "potentialQuoteTokens",
|
||||||
"type": "u64"
|
"type": "u64"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "lowestPlacedBidInv",
|
||||||
|
"docs": [
|
||||||
|
"Track lowest bid/highest ask, same way as for highest bid/lowest ask.",
|
||||||
|
"",
|
||||||
|
"0 is a special \"unset\" state."
|
||||||
|
],
|
||||||
|
"type": "f64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "highestPlacedAsk",
|
||||||
|
"type": "f64"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "reserved",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
32
|
16
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10284,6 +10584,9 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "TokenConditionalSwapCreateLinearAuction"
|
"name": "TokenConditionalSwapCreateLinearAuction"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Serum3PlaceOrderV2"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -11393,6 +11696,56 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "UpdateRateLogV2",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "mangoGroup",
|
||||||
|
"type": "publicKey",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tokenIndex",
|
||||||
|
"type": "u16",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rate0",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "util0",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rate1",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "util1",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maxRate",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "curveScaling",
|
||||||
|
"type": "f64",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "targetUtilization",
|
||||||
|
"type": "f32",
|
||||||
|
"index": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "TokenLiqWithTokenLog",
|
"name": "TokenLiqWithTokenLog",
|
||||||
"fields": [
|
"fields": [
|
||||||
|
@ -13155,6 +13508,21 @@
|
||||||
"code": 6059,
|
"code": 6059,
|
||||||
"name": "TokenConditionalSwapTypeNotStartable",
|
"name": "TokenConditionalSwapTypeNotStartable",
|
||||||
"msg": "token conditional swap type cannot be started"
|
"msg": "token conditional swap type cannot be started"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6060,
|
||||||
|
"name": "HealthAccountBankNotWritable",
|
||||||
|
"msg": "a bank in the health account list should be writable but is not"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6061,
|
||||||
|
"name": "Serum3PriceBandExceeded",
|
||||||
|
"msg": "the market does not allow limit orders too far from the current oracle value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6062,
|
||||||
|
"name": "BankDepositLimit",
|
||||||
|
"msg": "deposit crosses the token's deposit limit"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "mango-v4"
|
name = "mango-v4"
|
||||||
version = "0.20.0"
|
version = "0.21.0"
|
||||||
description = "Created with Anchor"
|
description = "Created with Anchor"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|
|
@ -31,11 +31,10 @@ pub enum Serum3Side {
|
||||||
Ask = 1,
|
Ask = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used for Serum3PlaceOrder v1 and v2
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
pub struct Serum3PlaceOrder<'info> {
|
pub struct Serum3PlaceOrder<'info> {
|
||||||
#[account(
|
// ix gate check is done at #4
|
||||||
constraint = group.load()?.is_ix_enabled(IxGate::Serum3PlaceOrder) @ MangoError::IxIsDisabled,
|
|
||||||
)]
|
|
||||||
pub group: AccountLoader<'info, Group>,
|
pub group: AccountLoader<'info, Group>,
|
||||||
|
|
||||||
#[account(
|
#[account(
|
||||||
|
|
|
@ -10,7 +10,12 @@ pub struct TokenAddBank<'info> {
|
||||||
#[account(
|
#[account(
|
||||||
has_one = admin,
|
has_one = admin,
|
||||||
constraint = group.load()?.is_ix_enabled(IxGate::TokenAddBank) @ MangoError::IxIsDisabled,
|
constraint = group.load()?.is_ix_enabled(IxGate::TokenAddBank) @ MangoError::IxIsDisabled,
|
||||||
constraint = group.load()?.multiple_banks_supported()
|
constraint = group.load()?.multiple_banks_supported(),
|
||||||
|
// Concerns are:
|
||||||
|
// - general reaudit
|
||||||
|
// - client support
|
||||||
|
// - potential_serum_tokens
|
||||||
|
constraint = group.load()?.is_testing(),
|
||||||
)]
|
)]
|
||||||
pub group: AccountLoader<'info, Group>,
|
pub group: AccountLoader<'info, Group>,
|
||||||
pub admin: Signer<'info>,
|
pub admin: Signer<'info>,
|
||||||
|
|
|
@ -125,6 +125,12 @@ pub enum MangoError {
|
||||||
TokenConditionalSwapTooSmallForStartIncentive,
|
TokenConditionalSwapTooSmallForStartIncentive,
|
||||||
#[msg("token conditional swap type cannot be started")]
|
#[msg("token conditional swap type cannot be started")]
|
||||||
TokenConditionalSwapTypeNotStartable,
|
TokenConditionalSwapTypeNotStartable,
|
||||||
|
#[msg("a bank in the health account list should be writable but is not")]
|
||||||
|
HealthAccountBankNotWritable,
|
||||||
|
#[msg("the market does not allow limit orders too far from the current oracle value")]
|
||||||
|
Serum3PriceBandExceeded,
|
||||||
|
#[msg("deposit crosses the token's deposit limit")]
|
||||||
|
BankDepositLimit,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MangoError {
|
impl MangoError {
|
||||||
|
|
|
@ -94,9 +94,10 @@ pub fn compute_health_from_fixed_accounts(
|
||||||
account: &MangoAccountRef,
|
account: &MangoAccountRef,
|
||||||
health_type: HealthType,
|
health_type: HealthType,
|
||||||
ais: &[AccountInfo],
|
ais: &[AccountInfo],
|
||||||
|
now_ts: u64,
|
||||||
) -> Result<I80F48> {
|
) -> Result<I80F48> {
|
||||||
let retriever = new_fixed_order_account_retriever(ais, account)?;
|
let retriever = new_fixed_order_account_retriever(ais, account)?;
|
||||||
Ok(new_health_cache(account, &retriever)?.health(health_type))
|
Ok(new_health_cache(account, &retriever, now_ts)?.health(health_type))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute health with an arbitrary AccountRetriever
|
/// Compute health with an arbitrary AccountRetriever
|
||||||
|
@ -104,8 +105,9 @@ pub fn compute_health(
|
||||||
account: &MangoAccountRef,
|
account: &MangoAccountRef,
|
||||||
health_type: HealthType,
|
health_type: HealthType,
|
||||||
retriever: &impl AccountRetriever,
|
retriever: &impl AccountRetriever,
|
||||||
|
now_ts: u64,
|
||||||
) -> Result<I80F48> {
|
) -> Result<I80F48> {
|
||||||
Ok(new_health_cache(account, retriever)?.health(health_type))
|
Ok(new_health_cache(account, retriever, now_ts)?.health(health_type))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How much of a token can be taken away before health decreases to zero?
|
/// How much of a token can be taken away before health decreases to zero?
|
||||||
|
@ -1221,8 +1223,9 @@ pub(crate) fn find_token_info_index(infos: &[TokenInfo], token_index: TokenIndex
|
||||||
pub fn new_health_cache(
|
pub fn new_health_cache(
|
||||||
account: &MangoAccountRef,
|
account: &MangoAccountRef,
|
||||||
retriever: &impl AccountRetriever,
|
retriever: &impl AccountRetriever,
|
||||||
|
now_ts: u64,
|
||||||
) -> Result<HealthCache> {
|
) -> Result<HealthCache> {
|
||||||
new_health_cache_impl(account, retriever, false)
|
new_health_cache_impl(account, retriever, now_ts, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a special HealthCache for an account and its health accounts
|
/// Generate a special HealthCache for an account and its health accounts
|
||||||
|
@ -1233,20 +1236,22 @@ pub fn new_health_cache(
|
||||||
pub fn new_health_cache_skipping_bad_oracles(
|
pub fn new_health_cache_skipping_bad_oracles(
|
||||||
account: &MangoAccountRef,
|
account: &MangoAccountRef,
|
||||||
retriever: &impl AccountRetriever,
|
retriever: &impl AccountRetriever,
|
||||||
|
now_ts: u64,
|
||||||
) -> Result<HealthCache> {
|
) -> Result<HealthCache> {
|
||||||
new_health_cache_impl(account, retriever, true)
|
new_health_cache_impl(account, retriever, now_ts, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_health_cache_impl(
|
fn new_health_cache_impl(
|
||||||
account: &MangoAccountRef,
|
account: &MangoAccountRef,
|
||||||
retriever: &impl AccountRetriever,
|
retriever: &impl AccountRetriever,
|
||||||
|
now_ts: u64,
|
||||||
// If an oracle is stale or inconfident and the health contribution would
|
// If an oracle is stale or inconfident and the health contribution would
|
||||||
// not be negative, skip it. This decreases health, but maybe overall it's
|
// not be negative, skip it. This decreases health, but maybe overall it's
|
||||||
// still positive?
|
// still positive?
|
||||||
skip_bad_oracles: bool,
|
skip_bad_oracles: bool,
|
||||||
) -> Result<HealthCache> {
|
) -> Result<HealthCache> {
|
||||||
// token contribution from token accounts
|
// token contribution from token accounts
|
||||||
let mut token_infos = vec![];
|
let mut token_infos = Vec::with_capacity(account.active_token_positions().count());
|
||||||
|
|
||||||
for (i, position) in account.active_token_positions().enumerate() {
|
for (i, position) in account.active_token_positions().enumerate() {
|
||||||
let bank_oracle_result =
|
let bank_oracle_result =
|
||||||
|
@ -1268,12 +1273,15 @@ fn new_health_cache_impl(
|
||||||
// Use the liab price for computing weight scaling, because it's pessimistic and
|
// Use the liab price for computing weight scaling, because it's pessimistic and
|
||||||
// causes the most unfavorable scaling.
|
// causes the most unfavorable scaling.
|
||||||
let liab_price = prices.liab(HealthType::Init);
|
let liab_price = prices.liab(HealthType::Init);
|
||||||
|
|
||||||
|
let (maint_asset_weight, maint_liab_weight) = bank.maint_weights(now_ts);
|
||||||
|
|
||||||
token_infos.push(TokenInfo {
|
token_infos.push(TokenInfo {
|
||||||
token_index: bank.token_index,
|
token_index: bank.token_index,
|
||||||
maint_asset_weight: bank.maint_asset_weight,
|
maint_asset_weight,
|
||||||
init_asset_weight: bank.init_asset_weight,
|
init_asset_weight: bank.init_asset_weight,
|
||||||
init_scaled_asset_weight: bank.scaled_init_asset_weight(liab_price),
|
init_scaled_asset_weight: bank.scaled_init_asset_weight(liab_price),
|
||||||
maint_liab_weight: bank.maint_liab_weight,
|
maint_liab_weight,
|
||||||
init_liab_weight: bank.init_liab_weight,
|
init_liab_weight: bank.init_liab_weight,
|
||||||
init_scaled_liab_weight: bank.scaled_init_liab_weight(liab_price),
|
init_scaled_liab_weight: bank.scaled_init_liab_weight(liab_price),
|
||||||
prices,
|
prices,
|
||||||
|
@ -1282,7 +1290,7 @@ fn new_health_cache_impl(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill the TokenInfo balance with free funds in serum3 oo accounts and build Serum3Infos.
|
// Fill the TokenInfo balance with free funds in serum3 oo accounts and build Serum3Infos.
|
||||||
let mut serum3_infos = vec![];
|
let mut serum3_infos = Vec::with_capacity(account.active_serum3_orders().count());
|
||||||
for (i, serum_account) in account.active_serum3_orders().enumerate() {
|
for (i, serum_account) in account.active_serum3_orders().enumerate() {
|
||||||
let oo = retriever.serum_oo(i, &serum_account.open_orders)?;
|
let oo = retriever.serum_oo(i, &serum_account.open_orders)?;
|
||||||
|
|
||||||
|
@ -1443,7 +1451,7 @@ mod tests {
|
||||||
// for bank2/oracle2
|
// for bank2/oracle2
|
||||||
let health2 = (-10.0 + 3.0) * 5.0 * 1.5;
|
let health2 = (-10.0 + 3.0) * 5.0 * 1.5;
|
||||||
assert!(health_eq(
|
assert!(health_eq(
|
||||||
compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(),
|
compute_health(&account.borrow(), HealthType::Init, &retriever, 0).unwrap(),
|
||||||
health1 + health2
|
health1 + health2
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -1454,7 +1462,7 @@ mod tests {
|
||||||
borrows: u64,
|
borrows: u64,
|
||||||
deposit_weight_scale_start_quote: u64,
|
deposit_weight_scale_start_quote: u64,
|
||||||
borrow_weight_scale_start_quote: u64,
|
borrow_weight_scale_start_quote: u64,
|
||||||
deposits_in_serum: i64,
|
potential_serum_tokens: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -1510,7 +1518,7 @@ mod tests {
|
||||||
let bank = bank.data();
|
let bank = bank.data();
|
||||||
bank.indexed_deposits = I80F48::from(settings.deposits) / bank.deposit_index;
|
bank.indexed_deposits = I80F48::from(settings.deposits) / bank.deposit_index;
|
||||||
bank.indexed_borrows = I80F48::from(settings.borrows) / bank.borrow_index;
|
bank.indexed_borrows = I80F48::from(settings.borrows) / bank.borrow_index;
|
||||||
bank.deposits_in_serum = settings.deposits_in_serum;
|
bank.potential_serum_tokens = settings.potential_serum_tokens;
|
||||||
if settings.deposit_weight_scale_start_quote > 0 {
|
if settings.deposit_weight_scale_start_quote > 0 {
|
||||||
bank.deposit_weight_scale_start_quote =
|
bank.deposit_weight_scale_start_quote =
|
||||||
settings.deposit_weight_scale_start_quote as f64;
|
settings.deposit_weight_scale_start_quote as f64;
|
||||||
|
@ -1568,7 +1576,7 @@ mod tests {
|
||||||
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
||||||
|
|
||||||
assert!(health_eq(
|
assert!(health_eq(
|
||||||
compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(),
|
compute_health(&account.borrow(), HealthType::Init, &retriever, 0).unwrap(),
|
||||||
testcase.expected_health
|
testcase.expected_health
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -1826,7 +1834,7 @@ mod tests {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
TestHealth1Case {
|
TestHealth1Case {
|
||||||
// 17, deposits_in_serum counts for deposit weight scaling
|
// 17, potential_serum_tokens counts for deposit weight scaling
|
||||||
token1: 100,
|
token1: 100,
|
||||||
token2: 100,
|
token2: 100,
|
||||||
token3: 100,
|
token3: 100,
|
||||||
|
@ -1839,13 +1847,13 @@ mod tests {
|
||||||
BankSettings {
|
BankSettings {
|
||||||
deposits: 100,
|
deposits: 100,
|
||||||
deposit_weight_scale_start_quote: 100 * 5,
|
deposit_weight_scale_start_quote: 100 * 5,
|
||||||
deposits_in_serum: 100,
|
potential_serum_tokens: 100,
|
||||||
..BankSettings::default()
|
..BankSettings::default()
|
||||||
},
|
},
|
||||||
BankSettings {
|
BankSettings {
|
||||||
deposits: 600,
|
deposits: 600,
|
||||||
deposit_weight_scale_start_quote: 500 * 10,
|
deposit_weight_scale_start_quote: 500 * 10,
|
||||||
deposits_in_serum: 100,
|
potential_serum_tokens: 100,
|
||||||
..BankSettings::default()
|
..BankSettings::default()
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -1264,7 +1311,7 @@ mod tests {
|
||||||
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
||||||
|
|
||||||
assert!(health_eq(
|
assert!(health_eq(
|
||||||
compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(),
|
compute_health(&account.borrow(), HealthType::Init, &retriever, 0).unwrap(),
|
||||||
// token
|
// token
|
||||||
0.8 * (100.0
|
0.8 * (100.0
|
||||||
// perp base
|
// perp base
|
||||||
|
@ -1353,27 +1400,27 @@ mod tests {
|
||||||
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
||||||
|
|
||||||
assert!(health_eq(
|
assert!(health_eq(
|
||||||
compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(),
|
compute_health(&account.borrow(), HealthType::Init, &retriever, 0).unwrap(),
|
||||||
0.8 * 0.5 * 100.0
|
0.8 * 0.5 * 100.0
|
||||||
));
|
));
|
||||||
assert!(health_eq(
|
assert!(health_eq(
|
||||||
compute_health(&account.borrow(), HealthType::Maint, &retriever).unwrap(),
|
compute_health(&account.borrow(), HealthType::Maint, &retriever, 0).unwrap(),
|
||||||
0.9 * 1.0 * 100.0
|
0.9 * 1.0 * 100.0
|
||||||
));
|
));
|
||||||
assert!(health_eq(
|
assert!(health_eq(
|
||||||
compute_health(&account2.borrow(), HealthType::Init, &retriever).unwrap(),
|
compute_health(&account2.borrow(), HealthType::Init, &retriever, 0).unwrap(),
|
||||||
-1.2 * 1.0 * 100.0
|
-1.2 * 1.0 * 100.0
|
||||||
));
|
));
|
||||||
assert!(health_eq(
|
assert!(health_eq(
|
||||||
compute_health(&account2.borrow(), HealthType::Maint, &retriever).unwrap(),
|
compute_health(&account2.borrow(), HealthType::Maint, &retriever, 0).unwrap(),
|
||||||
-1.1 * 1.0 * 100.0
|
-1.1 * 1.0 * 100.0
|
||||||
));
|
));
|
||||||
assert!(health_eq(
|
assert!(health_eq(
|
||||||
compute_health(&account3.borrow(), HealthType::Init, &retriever).unwrap(),
|
compute_health(&account3.borrow(), HealthType::Init, &retriever, 0).unwrap(),
|
||||||
1.2 * (0.8 * 0.5 * 10.0 * 10.0 - 100.0)
|
1.2 * (0.8 * 0.5 * 10.0 * 10.0 - 100.0)
|
||||||
));
|
));
|
||||||
assert!(health_eq(
|
assert!(health_eq(
|
||||||
compute_health(&account3.borrow(), HealthType::Maint, &retriever).unwrap(),
|
compute_health(&account3.borrow(), HealthType::Maint, &retriever, 0).unwrap(),
|
||||||
1.1 * (0.9 * 1.0 * 10.0 * 10.0 - 100.0)
|
1.1 * (0.9 * 1.0 * 10.0 * 10.0 - 100.0)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ use crate::state::*;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
|
|
||||||
use crate::logs::{AccountBuybackFeesWithMngoLog, TokenBalanceLog};
|
use crate::logs::{emit_stack, AccountBuybackFeesWithMngoLog, TokenBalanceLog};
|
||||||
|
|
||||||
pub fn account_buyback_fees_with_mngo(
|
pub fn account_buyback_fees_with_mngo(
|
||||||
ctx: Context<AccountBuybackFeesWithMngo>,
|
ctx: Context<AccountBuybackFeesWithMngo>,
|
||||||
|
@ -105,7 +105,7 @@ pub fn account_buyback_fees_with_mngo(
|
||||||
);
|
);
|
||||||
let in_use =
|
let in_use =
|
||||||
mngo_bank.withdraw_without_fee(account_mngo_token_position, max_buyback_mngo, now_ts)?;
|
mngo_bank.withdraw_without_fee(account_mngo_token_position, max_buyback_mngo, now_ts)?;
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.account.key(),
|
mango_account: ctx.accounts.account.key(),
|
||||||
token_index: mngo_bank.token_index,
|
token_index: mngo_bank.token_index,
|
||||||
|
@ -137,7 +137,7 @@ pub fn account_buyback_fees_with_mngo(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let in_use = fees_bank.deposit(account_fees_token_position, max_buyback_fees, now_ts)?;
|
let in_use = fees_bank.deposit(account_fees_token_position, max_buyback_fees, now_ts)?;
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.account.key(),
|
mango_account: ctx.accounts.account.key(),
|
||||||
token_index: fees_bank.token_index,
|
token_index: fees_bank.token_index,
|
||||||
|
@ -162,7 +162,7 @@ pub fn account_buyback_fees_with_mngo(
|
||||||
max_buyback_fees,
|
max_buyback_fees,
|
||||||
);
|
);
|
||||||
|
|
||||||
emit!(AccountBuybackFeesWithMngoLog {
|
emit_stack(AccountBuybackFeesWithMngoLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.account.key(),
|
mango_account: ctx.accounts.account.key(),
|
||||||
buyback_fees: max_buyback_fees.to_bits(),
|
buyback_fees: max_buyback_fees.to_bits(),
|
||||||
|
|
|
@ -13,12 +13,14 @@ pub fn compute_account_data(ctx: Context<ComputeAccountData>) -> Result<()> {
|
||||||
|
|
||||||
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &group_pk)?;
|
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &group_pk)?;
|
||||||
|
|
||||||
let health_cache = new_health_cache(&account.borrow(), &account_retriever)?;
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
|
let health_cache = new_health_cache(&account.borrow(), &account_retriever, now_ts)?;
|
||||||
let init_health = health_cache.health(HealthType::Init);
|
let init_health = health_cache.health(HealthType::Init);
|
||||||
let maint_health = health_cache.health(HealthType::Maint);
|
let maint_health = health_cache.health(HealthType::Maint);
|
||||||
|
|
||||||
let equity = compute_equity(&account.borrow(), &account_retriever)?;
|
let equity = compute_equity(&account.borrow(), &account_retriever)?;
|
||||||
|
|
||||||
|
// Potentially too big for the stack!
|
||||||
emit!(MangoAccountData {
|
emit!(MangoAccountData {
|
||||||
init_health,
|
init_health,
|
||||||
maint_health,
|
maint_health,
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::accounts_zerocopy::*;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::group_seeds;
|
use crate::group_seeds;
|
||||||
use crate::health::{new_fixed_order_account_retriever, new_health_cache, AccountRetriever};
|
use crate::health::{new_fixed_order_account_retriever, new_health_cache, AccountRetriever};
|
||||||
use crate::logs::{FlashLoanLogV3, FlashLoanTokenDetailV3, TokenBalanceLog};
|
use crate::logs::{emit_stack, FlashLoanLogV3, FlashLoanTokenDetailV3, TokenBalanceLog};
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
|
@ -388,7 +388,8 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
||||||
|
|
||||||
// Check health before balance adjustments
|
// Check health before balance adjustments
|
||||||
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
|
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
|
||||||
let health_cache = new_health_cache(&account.borrow(), &retriever)?;
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
|
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)?;
|
||||||
let pre_init_health = account.check_health_pre(&health_cache)?;
|
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||||
|
|
||||||
// Prices for logging and net borrow checks
|
// Prices for logging and net borrow checks
|
||||||
|
@ -466,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;
|
||||||
|
|
||||||
|
@ -481,7 +486,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
||||||
approved_amount: approved_amount_u64,
|
approved_amount: approved_amount_u64,
|
||||||
});
|
});
|
||||||
|
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: group.key(),
|
mango_group: group.key(),
|
||||||
mango_account: ctx.accounts.account.key(),
|
mango_account: ctx.accounts.account.key(),
|
||||||
token_index: bank.token_index as u16,
|
token_index: bank.token_index as u16,
|
||||||
|
@ -491,16 +496,16 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
emit!(FlashLoanLogV3 {
|
emit_stack(FlashLoanLogV3 {
|
||||||
mango_group: group.key(),
|
mango_group: group.key(),
|
||||||
mango_account: ctx.accounts.account.key(),
|
mango_account: ctx.accounts.account.key(),
|
||||||
flash_loan_type,
|
flash_loan_type,
|
||||||
token_loan_details
|
token_loan_details,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check health after account position changes
|
// Check health after account position changes
|
||||||
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
|
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
|
||||||
let health_cache = new_health_cache(&account.borrow(), &retriever)?;
|
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)?;
|
||||||
account.check_health_post(&health_cache, pre_init_health)?;
|
account.check_health_post(&health_cache, pre_init_health)?;
|
||||||
|
|
||||||
// Deactivate inactive token accounts after health check
|
// Deactivate inactive token accounts after health check
|
||||||
|
|
|
@ -23,6 +23,7 @@ pub fn health_region_begin<'key, 'accounts, 'remaining, 'info>(
|
||||||
crate::instruction::Serum3CancelAllOrders::discriminator(),
|
crate::instruction::Serum3CancelAllOrders::discriminator(),
|
||||||
crate::instruction::Serum3CancelOrder::discriminator(),
|
crate::instruction::Serum3CancelOrder::discriminator(),
|
||||||
crate::instruction::Serum3PlaceOrder::discriminator(),
|
crate::instruction::Serum3PlaceOrder::discriminator(),
|
||||||
|
crate::instruction::Serum3PlaceOrderV2::discriminator(),
|
||||||
crate::instruction::Serum3SettleFunds::discriminator(),
|
crate::instruction::Serum3SettleFunds::discriminator(),
|
||||||
crate::instruction::Serum3SettleFundsV2::discriminator(),
|
crate::instruction::Serum3SettleFundsV2::discriminator(),
|
||||||
];
|
];
|
||||||
|
@ -87,7 +88,8 @@ pub fn health_region_begin<'key, 'accounts, 'remaining, 'info>(
|
||||||
.context("create account retriever")?;
|
.context("create account retriever")?;
|
||||||
|
|
||||||
// Compute pre-health and store it on the account
|
// Compute pre-health and store it on the account
|
||||||
let health_cache = new_health_cache(&account.borrow(), &account_retriever)?;
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
|
let health_cache = new_health_cache(&account.borrow(), &account_retriever, now_ts)?;
|
||||||
let pre_init_health = account.check_health_pre(&health_cache)?;
|
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||||
account.fixed.health_region_begin_init_health = pre_init_health.ceil().to_num();
|
account.fixed.health_region_begin_init_health = pre_init_health.ceil().to_num();
|
||||||
|
|
||||||
|
@ -107,7 +109,8 @@ pub fn health_region_end<'key, 'accounts, 'remaining, 'info>(
|
||||||
let group = account.fixed.group;
|
let group = account.fixed.group;
|
||||||
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &group)
|
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &group)
|
||||||
.context("create account retriever")?;
|
.context("create account retriever")?;
|
||||||
let health_cache = new_health_cache(&account.borrow(), &account_retriever)?;
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
|
let health_cache = new_health_cache(&account.borrow(), &account_retriever, now_ts)?;
|
||||||
|
|
||||||
let pre_init_health = I80F48::from(account.fixed.health_region_begin_init_health);
|
let pre_init_health = I80F48::from(account.fixed.health_region_begin_init_health);
|
||||||
account.check_health_post(&health_cache, pre_init_health)?;
|
account.check_health_post(&health_cache, pre_init_health)?;
|
||||||
|
|
|
@ -94,6 +94,7 @@ pub fn ix_gate_set(ctx: Context<IxGateSet>, ix_gate: u128) -> Result<()> {
|
||||||
ix_gate,
|
ix_gate,
|
||||||
IxGate::TokenConditionalSwapCreateLinearAuction,
|
IxGate::TokenConditionalSwapCreateLinearAuction,
|
||||||
);
|
);
|
||||||
|
log_if_changed(&group, ix_gate, IxGate::Serum3PlaceOrderV2);
|
||||||
|
|
||||||
group.ix_gate = ix_gate;
|
group.ix_gate = ix_gate;
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::error::MangoError;
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::logs::{emit_perp_balances, FillLogV3};
|
use crate::logs::{emit_perp_balances, emit_stack, FillLogV3};
|
||||||
|
|
||||||
/// Load a mango account by key from the list of account infos.
|
/// Load a mango account by key from the list of account infos.
|
||||||
///
|
///
|
||||||
|
@ -131,7 +131,7 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
|
||||||
let taker_closed_pnl = taker_after_pnl - taker_before_pnl;
|
let taker_closed_pnl = taker_after_pnl - taker_before_pnl;
|
||||||
(maker_closed_pnl, taker_closed_pnl)
|
(maker_closed_pnl, taker_closed_pnl)
|
||||||
};
|
};
|
||||||
emit!(FillLogV3 {
|
emit_stack(FillLogV3 {
|
||||||
mango_group: group_key,
|
mango_group: group_key,
|
||||||
market_index: perp_market_index,
|
market_index: perp_market_index,
|
||||||
taker_side: fill.taker_side as u8,
|
taker_side: fill.taker_side as u8,
|
||||||
|
@ -149,7 +149,7 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
|
||||||
price: fill.price,
|
price: fill.price,
|
||||||
quantity: fill.quantity,
|
quantity: fill.quantity,
|
||||||
maker_closed_pnl: maker_closed_pnl.to_num(),
|
maker_closed_pnl: maker_closed_pnl.to_num(),
|
||||||
taker_closed_pnl: taker_closed_pnl.to_num()
|
taker_closed_pnl: taker_closed_pnl.to_num(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
EventType::Out => {
|
EventType::Out => {
|
||||||
|
|
|
@ -7,7 +7,7 @@ use crate::state::*;
|
||||||
use crate::util::fill_from_str;
|
use crate::util::fill_from_str;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::logs::PerpMarketMetaDataLog;
|
use crate::logs::{emit_stack, PerpMarketMetaDataLog};
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn perp_create_market(
|
pub fn perp_create_market(
|
||||||
|
@ -111,7 +111,7 @@ pub fn perp_create_market(
|
||||||
};
|
};
|
||||||
orderbook.init();
|
orderbook.init();
|
||||||
|
|
||||||
emit!(PerpMarketMetaDataLog {
|
emit_stack(PerpMarketMetaDataLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
perp_market: ctx.accounts.perp_market.key(),
|
perp_market: ctx.accounts.perp_market.key(),
|
||||||
perp_market_index,
|
perp_market_index,
|
||||||
|
|
|
@ -4,7 +4,7 @@ use anchor_lang::prelude::*;
|
||||||
use fixed::types::I80F48;
|
use fixed::types::I80F48;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::logs::PerpMarketMetaDataLog;
|
use crate::logs::{emit_stack, PerpMarketMetaDataLog};
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn perp_edit_market(
|
pub fn perp_edit_market(
|
||||||
|
@ -358,7 +358,7 @@ pub fn perp_edit_market(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
emit!(PerpMarketMetaDataLog {
|
emit_stack(PerpMarketMetaDataLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
perp_market: ctx.accounts.perp_market.key(),
|
perp_market: ctx.accounts.perp_market.key(),
|
||||||
perp_market_index: perp_market.perp_market_index,
|
perp_market_index: perp_market.perp_market_index,
|
||||||
|
|
|
@ -4,7 +4,7 @@ use crate::accounts_ix::*;
|
||||||
|
|
||||||
use crate::accounts_zerocopy::AccountInfoRef;
|
use crate::accounts_zerocopy::AccountInfoRef;
|
||||||
use crate::error::MangoError;
|
use crate::error::MangoError;
|
||||||
use crate::logs::{emit_perp_balances, PerpForceClosePositionLog};
|
use crate::logs::{emit_perp_balances, emit_stack, PerpForceClosePositionLog};
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
use fixed::types::I80F48;
|
use fixed::types::I80F48;
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ pub fn perp_force_close_position(ctx: Context<PerpForceClosePosition>) -> Result
|
||||||
&perp_market,
|
&perp_market,
|
||||||
);
|
);
|
||||||
|
|
||||||
emit!(PerpForceClosePositionLog {
|
emit_stack(PerpForceClosePositionLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
perp_market_index: perp_market.perp_market_index,
|
perp_market_index: perp_market.perp_market_index,
|
||||||
account_a: ctx.accounts.account_a.key(),
|
account_a: ctx.accounts.account_a.key(),
|
||||||
|
|
|
@ -8,7 +8,7 @@ use crate::health::*;
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::logs::{emit_perp_balances, PerpLiqBaseOrPositivePnlLog, TokenBalanceLog};
|
use crate::logs::{emit_perp_balances, emit_stack, PerpLiqBaseOrPositivePnlLog, TokenBalanceLog};
|
||||||
|
|
||||||
/// This instruction deals with increasing health by:
|
/// This instruction deals with increasing health by:
|
||||||
/// - reducing the liqee's base position
|
/// - reducing the liqee's base position
|
||||||
|
@ -29,6 +29,7 @@ pub fn perp_liq_base_or_positive_pnl(
|
||||||
max_base_transfer = max_base_transfer.max(i64::MIN + 1);
|
max_base_transfer = max_base_transfer.max(i64::MIN + 1);
|
||||||
|
|
||||||
let group_pk = &ctx.accounts.group.key();
|
let group_pk = &ctx.accounts.group.key();
|
||||||
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
|
|
||||||
require_keys_neq!(ctx.accounts.liqor.key(), ctx.accounts.liqee.key());
|
require_keys_neq!(ctx.accounts.liqor.key(), ctx.accounts.liqee.key());
|
||||||
let mut liqor = ctx.accounts.liqor.load_full_mut()?;
|
let mut liqor = ctx.accounts.liqor.load_full_mut()?;
|
||||||
|
@ -51,7 +52,7 @@ pub fn perp_liq_base_or_positive_pnl(
|
||||||
let mut liqee_health_cache = {
|
let mut liqee_health_cache = {
|
||||||
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)
|
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)
|
||||||
.context("create account retriever")?;
|
.context("create account retriever")?;
|
||||||
new_health_cache(&liqee.borrow(), &account_retriever)
|
new_health_cache(&liqee.borrow(), &account_retriever, now_ts)
|
||||||
.context("create liqee health cache")?
|
.context("create liqee health cache")?
|
||||||
};
|
};
|
||||||
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
||||||
|
@ -87,7 +88,6 @@ pub fn perp_liq_base_or_positive_pnl(
|
||||||
// Settle funding, update limit
|
// Settle funding, update limit
|
||||||
liqee_perp_position.settle_funding(&perp_market);
|
liqee_perp_position.settle_funding(&perp_market);
|
||||||
liqor_perp_position.settle_funding(&perp_market);
|
liqor_perp_position.settle_funding(&perp_market);
|
||||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
|
||||||
liqee_perp_position.update_settle_limit(&perp_market, now_ts);
|
liqee_perp_position.update_settle_limit(&perp_market, now_ts);
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -131,7 +131,7 @@ pub fn perp_liq_base_or_positive_pnl(
|
||||||
let liqee_token_position = liqee.token_position(settle_token_index)?;
|
let liqee_token_position = liqee.token_position(settle_token_index)?;
|
||||||
let liqor_token_position = liqor.token_position(settle_token_index)?;
|
let liqor_token_position = liqor.token_position(settle_token_index)?;
|
||||||
|
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.liqee.key(),
|
mango_account: ctx.accounts.liqee.key(),
|
||||||
token_index: settle_token_index,
|
token_index: settle_token_index,
|
||||||
|
@ -140,7 +140,7 @@ pub fn perp_liq_base_or_positive_pnl(
|
||||||
borrow_index: settle_bank.borrow_index.to_bits(),
|
borrow_index: settle_bank.borrow_index.to_bits(),
|
||||||
});
|
});
|
||||||
|
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.liqor.key(),
|
mango_account: ctx.accounts.liqor.key(),
|
||||||
token_index: settle_token_index,
|
token_index: settle_token_index,
|
||||||
|
@ -151,7 +151,7 @@ pub fn perp_liq_base_or_positive_pnl(
|
||||||
}
|
}
|
||||||
|
|
||||||
if base_transfer != 0 || pnl_transfer != 0 {
|
if base_transfer != 0 || pnl_transfer != 0 {
|
||||||
emit!(PerpLiqBaseOrPositivePnlLog {
|
emit_stack(PerpLiqBaseOrPositivePnlLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
perp_market_index: perp_market.perp_market_index,
|
perp_market_index: perp_market.perp_market_index,
|
||||||
liqor: ctx.accounts.liqor.key(),
|
liqor: ctx.accounts.liqor.key(),
|
||||||
|
@ -183,7 +183,12 @@ pub fn perp_liq_base_or_positive_pnl(
|
||||||
if !liqor.fixed.is_in_health_region() {
|
if !liqor.fixed.is_in_health_region() {
|
||||||
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)
|
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)
|
||||||
.context("create account retriever end")?;
|
.context("create account retriever end")?;
|
||||||
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
|
let liqor_health = compute_health(
|
||||||
|
&liqor.borrow(),
|
||||||
|
HealthType::Init,
|
||||||
|
&account_retriever,
|
||||||
|
now_ts,
|
||||||
|
)
|
||||||
.context("compute liqor health")?;
|
.context("compute liqor health")?;
|
||||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||||
}
|
}
|
||||||
|
@ -675,7 +680,7 @@ mod tests {
|
||||||
let retriever =
|
let retriever =
|
||||||
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
|
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
|
||||||
|
|
||||||
health::new_health_cache(&setup.liqee.borrow(), &retriever).unwrap()
|
health::new_health_cache(&setup.liqee.borrow(), &retriever, 0).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(&self, max_base: i64, max_pnl: u64) -> Result<Self> {
|
fn run(&self, max_base: i64, max_pnl: u64) -> Result<Self> {
|
||||||
|
|
|
@ -11,10 +11,11 @@ pub fn perp_liq_force_cancel_orders(
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
let mut account = ctx.accounts.account.load_full_mut()?;
|
||||||
|
|
||||||
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
let mut health_cache = {
|
let mut health_cache = {
|
||||||
let retriever =
|
let retriever =
|
||||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||||
new_health_cache(&account.borrow(), &retriever).context("create health cache")?
|
new_health_cache(&account.borrow(), &retriever, now_ts).context("create health cache")?
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||||
|
|
|
@ -10,7 +10,8 @@ use crate::accounts_zerocopy::AccountInfoRef;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::health::*;
|
use crate::health::*;
|
||||||
use crate::logs::{
|
use crate::logs::{
|
||||||
emit_perp_balances, PerpLiqBankruptcyLog, PerpLiqNegativePnlOrBankruptcyLog, TokenBalanceLog,
|
emit_perp_balances, emit_stack, PerpLiqBankruptcyLog, PerpLiqNegativePnlOrBankruptcyLog,
|
||||||
|
TokenBalanceLog,
|
||||||
};
|
};
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
|
||||||
|
@ -71,7 +72,7 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
|
||||||
|
|
||||||
let retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &mango_group)
|
let retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &mango_group)
|
||||||
.context("create account retriever")?;
|
.context("create account retriever")?;
|
||||||
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &retriever)?;
|
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &retriever, now_ts)?;
|
||||||
drop(retriever);
|
drop(retriever);
|
||||||
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
||||||
|
|
||||||
|
@ -136,7 +137,7 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
|
||||||
if settlement > 0 {
|
if settlement > 0 {
|
||||||
let settle_bank = ctx.accounts.settle_bank.load()?;
|
let settle_bank = ctx.accounts.settle_bank.load()?;
|
||||||
let liqor_token_position = liqor.token_position(settle_token_index)?;
|
let liqor_token_position = liqor.token_position(settle_token_index)?;
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group,
|
mango_group,
|
||||||
mango_account: ctx.accounts.liqor.key(),
|
mango_account: ctx.accounts.liqor.key(),
|
||||||
token_index: settle_token_index,
|
token_index: settle_token_index,
|
||||||
|
@ -146,7 +147,7 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
|
||||||
});
|
});
|
||||||
|
|
||||||
let liqee_token_position = liqee.token_position(settle_token_index)?;
|
let liqee_token_position = liqee.token_position(settle_token_index)?;
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group,
|
mango_group,
|
||||||
mango_account: ctx.accounts.liqee.key(),
|
mango_account: ctx.accounts.liqee.key(),
|
||||||
token_index: settle_token_index,
|
token_index: settle_token_index,
|
||||||
|
@ -159,7 +160,7 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
|
||||||
if insurance_transfer > 0 {
|
if insurance_transfer > 0 {
|
||||||
let insurance_bank = ctx.accounts.insurance_bank.load()?;
|
let insurance_bank = ctx.accounts.insurance_bank.load()?;
|
||||||
let liqor_token_position = liqor.token_position(insurance_bank.token_index)?;
|
let liqor_token_position = liqor.token_position(insurance_bank.token_index)?;
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group,
|
mango_group,
|
||||||
mango_account: ctx.accounts.liqor.key(),
|
mango_account: ctx.accounts.liqor.key(),
|
||||||
token_index: insurance_bank.token_index,
|
token_index: insurance_bank.token_index,
|
||||||
|
@ -197,7 +198,12 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
|
||||||
if !liqor.fixed.is_in_health_region() {
|
if !liqor.fixed.is_in_health_region() {
|
||||||
let account_retriever =
|
let account_retriever =
|
||||||
ScanningAccountRetriever::new(ctx.remaining_accounts, &mango_group)?;
|
ScanningAccountRetriever::new(ctx.remaining_accounts, &mango_group)?;
|
||||||
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
|
let liqor_health = compute_health(
|
||||||
|
&liqor.borrow(),
|
||||||
|
HealthType::Init,
|
||||||
|
&account_retriever,
|
||||||
|
now_ts,
|
||||||
|
)
|
||||||
.context("compute liqor health")?;
|
.context("compute liqor health")?;
|
||||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||||
}
|
}
|
||||||
|
@ -276,7 +282,7 @@ pub(crate) fn liquidation_action(
|
||||||
settle_bank.withdraw_without_fee(liqee_token_position, settlement, now_ts)?;
|
settle_bank.withdraw_without_fee(liqee_token_position, settlement, now_ts)?;
|
||||||
liqee_health_cache.adjust_token_balance(&settle_bank, -settlement)?;
|
liqee_health_cache.adjust_token_balance(&settle_bank, -settlement)?;
|
||||||
|
|
||||||
emit!(PerpLiqNegativePnlOrBankruptcyLog {
|
emit_stack(PerpLiqNegativePnlOrBankruptcyLog {
|
||||||
mango_group: group_key,
|
mango_group: group_key,
|
||||||
liqee: liqee_key,
|
liqee: liqee_key,
|
||||||
liqor: liqor_key,
|
liqor: liqor_key,
|
||||||
|
@ -397,7 +403,7 @@ pub(crate) fn liquidation_action(
|
||||||
msg!("socialized loss: {}", socialized_loss);
|
msg!("socialized loss: {}", socialized_loss);
|
||||||
}
|
}
|
||||||
|
|
||||||
emit!(PerpLiqBankruptcyLog {
|
emit_stack(PerpLiqBankruptcyLog {
|
||||||
mango_group: group_key,
|
mango_group: group_key,
|
||||||
liqee: liqee_key,
|
liqee: liqee_key,
|
||||||
liqor: liqor_key,
|
liqor: liqor_key,
|
||||||
|
@ -509,7 +515,7 @@ mod tests {
|
||||||
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
|
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
|
||||||
|
|
||||||
liqee_health_cache =
|
liqee_health_cache =
|
||||||
health::new_health_cache(&setup.liqee.borrow(), &retriever).unwrap();
|
health::new_health_cache(&setup.liqee.borrow(), &retriever, 0).unwrap();
|
||||||
liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -67,8 +67,8 @@ pub fn perp_place_order(
|
||||||
let pre_health_opt = if !account.fixed.is_in_health_region() {
|
let pre_health_opt = if !account.fixed.is_in_health_region() {
|
||||||
let retriever =
|
let retriever =
|
||||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||||
let health_cache =
|
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)
|
||||||
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?;
|
.context("pre-withdraw init health")?;
|
||||||
let pre_init_health = account.check_health_pre(&health_cache)?;
|
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||||
Some((health_cache, pre_init_health))
|
Some((health_cache, pre_init_health))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -8,7 +8,7 @@ use crate::health::{compute_health, new_fixed_order_account_retriever, HealthTyp
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::logs::{emit_perp_balances, PerpSettleFeesLog, TokenBalanceLog};
|
use crate::logs::{emit_perp_balances, emit_stack, PerpSettleFeesLog, TokenBalanceLog};
|
||||||
|
|
||||||
pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) -> Result<()> {
|
pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) -> Result<()> {
|
||||||
// max_settle_amount must greater than zero
|
// max_settle_amount must greater than zero
|
||||||
|
@ -100,7 +100,7 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
|
||||||
// Update the settled balance on the market itself
|
// Update the settled balance on the market itself
|
||||||
perp_market.fees_settled += settlement;
|
perp_market.fees_settled += settlement;
|
||||||
|
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.account.key(),
|
mango_account: ctx.accounts.account.key(),
|
||||||
token_index: perp_market.settle_token_index,
|
token_index: perp_market.settle_token_index,
|
||||||
|
@ -109,7 +109,7 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
|
||||||
borrow_index: settle_bank.borrow_index.to_bits(),
|
borrow_index: settle_bank.borrow_index.to_bits(),
|
||||||
});
|
});
|
||||||
|
|
||||||
emit!(PerpSettleFeesLog {
|
emit_stack(PerpSettleFeesLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.account.key(),
|
mango_account: ctx.accounts.account.key(),
|
||||||
perp_market_index: perp_market.perp_market_index,
|
perp_market_index: perp_market.perp_market_index,
|
||||||
|
@ -122,7 +122,8 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
|
||||||
|
|
||||||
// Verify that the result of settling did not violate the health of the account that lost money
|
// Verify that the result of settling did not violate the health of the account that lost money
|
||||||
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||||
let health = compute_health(&account.borrow(), HealthType::Init, &retriever)?;
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
|
let health = compute_health(&account.borrow(), HealthType::Init, &retriever, now_ts)?;
|
||||||
require!(health >= 0, MangoError::HealthMustBePositive);
|
require!(health >= 0, MangoError::HealthMustBePositive);
|
||||||
|
|
||||||
msg!("settled fees = {}", settlement);
|
msg!("settled fees = {}", settlement);
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::accounts_ix::*;
|
||||||
use crate::accounts_zerocopy::*;
|
use crate::accounts_zerocopy::*;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::health::{new_health_cache, HealthType, ScanningAccountRetriever};
|
use crate::health::{new_health_cache, HealthType, ScanningAccountRetriever};
|
||||||
use crate::logs::{emit_perp_balances, PerpSettlePnlLog, TokenBalanceLog};
|
use crate::logs::{emit_perp_balances, emit_stack, PerpSettlePnlLog, TokenBalanceLog};
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
|
||||||
pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
||||||
|
@ -36,6 +36,8 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
||||||
account_b.token_position(settle_token_index)?;
|
account_b.token_position(settle_token_index)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
|
|
||||||
let a_liq_end_health;
|
let a_liq_end_health;
|
||||||
let a_maint_health;
|
let a_maint_health;
|
||||||
let b_max_settle;
|
let b_max_settle;
|
||||||
|
@ -43,9 +45,9 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
||||||
let retriever =
|
let retriever =
|
||||||
ScanningAccountRetriever::new(ctx.remaining_accounts, &ctx.accounts.group.key())
|
ScanningAccountRetriever::new(ctx.remaining_accounts, &ctx.accounts.group.key())
|
||||||
.context("create account retriever")?;
|
.context("create account retriever")?;
|
||||||
b_max_settle = new_health_cache(&account_b.borrow(), &retriever)?
|
b_max_settle = new_health_cache(&account_b.borrow(), &retriever, now_ts)?
|
||||||
.perp_max_settle(settle_token_index)?;
|
.perp_max_settle(settle_token_index)?;
|
||||||
let a_cache = new_health_cache(&account_a.borrow(), &retriever)?;
|
let a_cache = new_health_cache(&account_a.borrow(), &retriever, now_ts)?;
|
||||||
a_liq_end_health = a_cache.health(HealthType::LiquidationEnd);
|
a_liq_end_health = a_cache.health(HealthType::LiquidationEnd);
|
||||||
a_maint_health = a_cache.health(HealthType::Maint);
|
a_maint_health = a_cache.health(HealthType::Maint);
|
||||||
};
|
};
|
||||||
|
@ -93,7 +95,6 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Apply pnl settle limits
|
// Apply pnl settle limits
|
||||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
|
||||||
a_perp_position.update_settle_limit(&perp_market, now_ts);
|
a_perp_position.update_settle_limit(&perp_market, now_ts);
|
||||||
let a_settleable_pnl = a_perp_position.apply_pnl_settle_limit(&perp_market, a_pnl);
|
let a_settleable_pnl = a_perp_position.apply_pnl_settle_limit(&perp_market, a_pnl);
|
||||||
b_perp_position.update_settle_limit(&perp_market, now_ts);
|
b_perp_position.update_settle_limit(&perp_market, now_ts);
|
||||||
|
@ -188,7 +189,7 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
||||||
// settled back and forth repeatedly.
|
// settled back and forth repeatedly.
|
||||||
settle_bank.withdraw_without_fee(b_token_position, settlement, now_ts)?;
|
settle_bank.withdraw_without_fee(b_token_position, settlement, now_ts)?;
|
||||||
|
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.account_a.key(),
|
mango_account: ctx.accounts.account_a.key(),
|
||||||
token_index: settle_token_index,
|
token_index: settle_token_index,
|
||||||
|
@ -197,7 +198,7 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
||||||
borrow_index: settle_bank.borrow_index.to_bits(),
|
borrow_index: settle_bank.borrow_index.to_bits(),
|
||||||
});
|
});
|
||||||
|
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.account_b.key(),
|
mango_account: ctx.accounts.account_b.key(),
|
||||||
token_index: settle_token_index,
|
token_index: settle_token_index,
|
||||||
|
@ -223,7 +224,7 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
||||||
settler.ensure_token_position(settle_token_index)?;
|
settler.ensure_token_position(settle_token_index)?;
|
||||||
let settler_token_position_active = settle_bank.deposit(settler_token_position, fee, now_ts)?;
|
let settler_token_position_active = settle_bank.deposit(settler_token_position, fee, now_ts)?;
|
||||||
|
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.settler.key(),
|
mango_account: ctx.accounts.settler.key(),
|
||||||
token_index: settler_token_position.token_index,
|
token_index: settler_token_position.token_index,
|
||||||
|
@ -237,7 +238,7 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
||||||
.deactivate_token_position_and_log(settler_token_raw_index, ctx.accounts.settler.key());
|
.deactivate_token_position_and_log(settler_token_raw_index, ctx.accounts.settler.key());
|
||||||
}
|
}
|
||||||
|
|
||||||
emit!(PerpSettlePnlLog {
|
emit_stack(PerpSettlePnlLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account_a: ctx.accounts.account_a.key(),
|
mango_account_a: ctx.accounts.account_a.key(),
|
||||||
mango_account_b: ctx.accounts.account_b.key(),
|
mango_account_b: ctx.accounts.account_b.key(),
|
||||||
|
|
|
@ -2,7 +2,7 @@ use anchor_lang::prelude::*;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::logs::Serum3OpenOrdersBalanceLogV2;
|
use crate::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2};
|
||||||
use crate::serum3_cpi::{load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim};
|
use crate::serum3_cpi::{load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim};
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ pub fn serum3_cancel_all_orders(ctx: Context<Serum3CancelAllOrders>, limit: u8)
|
||||||
let oo_ai = &ctx.accounts.open_orders.as_ref();
|
let oo_ai = &ctx.accounts.open_orders.as_ref();
|
||||||
let open_orders = load_open_orders_ref(oo_ai)?;
|
let open_orders = load_open_orders_ref(oo_ai)?;
|
||||||
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
|
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
|
||||||
emit!(Serum3OpenOrdersBalanceLogV2 {
|
emit_stack(Serum3OpenOrdersBalanceLogV2 {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.account.key(),
|
mango_account: ctx.accounts.account.key(),
|
||||||
market_index: serum_market.market_index,
|
market_index: serum_market.market_index,
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::error::*;
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::logs::Serum3OpenOrdersBalanceLogV2;
|
use crate::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2};
|
||||||
use crate::serum3_cpi::{load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim};
|
use crate::serum3_cpi::{load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim};
|
||||||
|
|
||||||
pub fn serum3_cancel_order(
|
pub fn serum3_cancel_order(
|
||||||
|
@ -49,7 +49,7 @@ pub fn serum3_cancel_order(
|
||||||
let oo_ai = &ctx.accounts.open_orders.as_ref();
|
let oo_ai = &ctx.accounts.open_orders.as_ref();
|
||||||
let open_orders = load_open_orders_ref(oo_ai)?;
|
let open_orders = load_open_orders_ref(oo_ai)?;
|
||||||
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
|
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
|
||||||
emit!(Serum3OpenOrdersBalanceLogV2 {
|
emit_stack(Serum3OpenOrdersBalanceLogV2 {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.account.key(),
|
mango_account: ctx.accounts.account.key(),
|
||||||
market_index: serum_market.market_index,
|
market_index: serum_market.market_index,
|
||||||
|
|
|
@ -7,6 +7,7 @@ pub fn serum3_edit_market(
|
||||||
reduce_only_opt: Option<bool>,
|
reduce_only_opt: Option<bool>,
|
||||||
force_close_opt: Option<bool>,
|
force_close_opt: Option<bool>,
|
||||||
name_opt: Option<String>,
|
name_opt: Option<String>,
|
||||||
|
oracle_price_band_opt: Option<f32>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut serum3_market = ctx.accounts.market.load_mut()?;
|
let mut serum3_market = ctx.accounts.market.load_mut()?;
|
||||||
|
|
||||||
|
@ -46,6 +47,16 @@ pub fn serum3_edit_market(
|
||||||
require_group_admin = true;
|
require_group_admin = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(oracle_price_band) = oracle_price_band_opt {
|
||||||
|
msg!(
|
||||||
|
"Oracle price band: old - {:?}, new - {:?}",
|
||||||
|
serum3_market.oracle_price_band,
|
||||||
|
oracle_price_band
|
||||||
|
);
|
||||||
|
serum3_market.oracle_price_band = oracle_price_band;
|
||||||
|
require_group_admin = true;
|
||||||
|
};
|
||||||
|
|
||||||
if require_group_admin {
|
if require_group_admin {
|
||||||
require!(
|
require!(
|
||||||
group.admin == ctx.accounts.admin.key(),
|
group.admin == ctx.accounts.admin.key(),
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::error::*;
|
||||||
use crate::health::*;
|
use crate::health::*;
|
||||||
use crate::instructions::apply_settle_changes;
|
use crate::instructions::apply_settle_changes;
|
||||||
use crate::instructions::charge_loan_origination_fees;
|
use crate::instructions::charge_loan_origination_fees;
|
||||||
use crate::logs::Serum3OpenOrdersBalanceLogV2;
|
use crate::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2};
|
||||||
use crate::serum3_cpi::{load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim};
|
use crate::serum3_cpi::{load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim};
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
|
||||||
|
@ -57,8 +57,9 @@ pub fn serum3_liq_force_cancel_orders(
|
||||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
let mut account = ctx.accounts.account.load_full_mut()?;
|
||||||
let retriever =
|
let retriever =
|
||||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||||
let health_cache =
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
new_health_cache(&account.borrow(), &retriever).context("create health cache")?;
|
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)
|
||||||
|
.context("create health cache")?;
|
||||||
|
|
||||||
let liquidatable = account.check_liquidatable(&health_cache)?;
|
let liquidatable = account.check_liquidatable(&health_cache)?;
|
||||||
let can_force_cancel = !account.fixed.is_operational()
|
let can_force_cancel = !account.fixed.is_operational()
|
||||||
|
@ -116,7 +117,7 @@ pub fn serum3_liq_force_cancel_orders(
|
||||||
let open_orders = load_open_orders_ref(oo_ai)?;
|
let open_orders = load_open_orders_ref(oo_ai)?;
|
||||||
after_oo = OpenOrdersSlim::from_oo(&open_orders);
|
after_oo = OpenOrdersSlim::from_oo(&open_orders);
|
||||||
|
|
||||||
emit!(Serum3OpenOrdersBalanceLogV2 {
|
emit_stack(Serum3OpenOrdersBalanceLogV2 {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.account.key(),
|
mango_account: ctx.accounts.account.key(),
|
||||||
market_index: serum_market.market_index,
|
market_index: serum_market.market_index,
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use crate::accounts_zerocopy::AccountInfoRef;
|
use crate::accounts_zerocopy::*;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::health::*;
|
use crate::health::*;
|
||||||
use crate::i80f48::ClampToInt;
|
use crate::i80f48::ClampToInt;
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::logs::{Serum3OpenOrdersBalanceLogV2, TokenBalanceLog};
|
use crate::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2, TokenBalanceLog};
|
||||||
use crate::serum3_cpi::{
|
use crate::serum3_cpi::{
|
||||||
load_market_state, load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim,
|
load_market_state, load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim,
|
||||||
};
|
};
|
||||||
|
@ -25,6 +25,7 @@ pub fn serum3_place_order(
|
||||||
order_type: Serum3OrderType,
|
order_type: Serum3OrderType,
|
||||||
client_order_id: u64,
|
client_order_id: u64,
|
||||||
limit: u16,
|
limit: u16,
|
||||||
|
require_v2: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Also required by serum3's place order
|
// Also required by serum3's place order
|
||||||
require_gt!(limit_price_lots, 0);
|
require_gt!(limit_price_lots, 0);
|
||||||
|
@ -76,8 +77,9 @@ pub fn serum3_place_order(
|
||||||
//
|
//
|
||||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
let mut account = ctx.accounts.account.load_full_mut()?;
|
||||||
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||||
let mut health_cache =
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?;
|
let mut health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)
|
||||||
|
.context("pre-withdraw init health")?;
|
||||||
let pre_health_opt = if !account.fixed.is_in_health_region() {
|
let pre_health_opt = if !account.fixed.is_in_health_region() {
|
||||||
let pre_init_health = account.check_health_pre(&health_cache)?;
|
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||||
Some(pre_init_health)
|
Some(pre_init_health)
|
||||||
|
@ -86,18 +88,52 @@ pub fn serum3_place_order(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if the bank for the token whose balance is increased is in reduce-only mode
|
// Check if the bank for the token whose balance is increased is in reduce-only mode
|
||||||
let receiver_bank_reduce_only = {
|
let receiver_bank_ai;
|
||||||
|
let receiver_bank_oracle;
|
||||||
|
let receiver_bank_reduce_only;
|
||||||
|
{
|
||||||
// The token position already exists, but we need the active_index.
|
// The token position already exists, but we need the active_index.
|
||||||
let (_, _, active_index) = account.ensure_token_position(receiver_token_index)?;
|
let (_, _, active_index) = account.ensure_token_position(receiver_token_index)?;
|
||||||
let group_key = ctx.accounts.group.key();
|
let group_key = ctx.accounts.group.key();
|
||||||
let receiver_bank = retriever
|
let (receiver_bank, oracle) =
|
||||||
.bank_and_oracle(&group_key, active_index, receiver_token_index)?
|
retriever.bank_and_oracle(&group_key, active_index, receiver_token_index)?;
|
||||||
.0;
|
receiver_bank_oracle = oracle;
|
||||||
receiver_bank.are_deposits_reduce_only()
|
receiver_bank_reduce_only = receiver_bank.are_deposits_reduce_only();
|
||||||
};
|
|
||||||
|
// The fixed_order account retriever can't give us mut references, so use the above
|
||||||
|
// call to .bank_and_oracle() as validation and then copy out the matching AccountInfo.
|
||||||
|
receiver_bank_ai = ctx.remaining_accounts[active_index].clone();
|
||||||
|
// Double-check that we got the right account
|
||||||
|
let receiver_bank2 = receiver_bank_ai.load::<Bank>()?;
|
||||||
|
assert_eq!(receiver_bank2.group, group_key);
|
||||||
|
assert_eq!(receiver_bank2.token_index, receiver_token_index);
|
||||||
|
}
|
||||||
|
|
||||||
drop(retriever);
|
drop(retriever);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Instruction version checking #4
|
||||||
|
//
|
||||||
|
let is_v2_instruction;
|
||||||
|
{
|
||||||
|
let group = ctx.accounts.group.load()?;
|
||||||
|
let v1_available = group.is_ix_enabled(IxGate::Serum3PlaceOrder);
|
||||||
|
let v2_available = group.is_ix_enabled(IxGate::Serum3PlaceOrderV2);
|
||||||
|
is_v2_instruction =
|
||||||
|
require_v2 || !v1_available || (receiver_bank_ai.is_writable && v2_available);
|
||||||
|
if is_v2_instruction {
|
||||||
|
require!(v2_available, MangoError::IxIsDisabled);
|
||||||
|
require_msg_typed!(
|
||||||
|
receiver_bank_ai.is_writable,
|
||||||
|
MangoError::HealthAccountBankNotWritable,
|
||||||
|
"the receiver bank (token index {}) in the health account list must be writable",
|
||||||
|
receiver_token_index
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
require!(v1_available, MangoError::IxIsDisabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Before-order tracking
|
// Before-order tracking
|
||||||
//
|
//
|
||||||
|
@ -184,38 +220,50 @@ pub fn serum3_place_order(
|
||||||
if !before_had_bids {
|
if !before_had_bids {
|
||||||
// The 0 state means uninitialized/no value
|
// The 0 state means uninitialized/no value
|
||||||
serum.highest_placed_bid_inv = 0.0;
|
serum.highest_placed_bid_inv = 0.0;
|
||||||
|
serum.lowest_placed_bid_inv = 0.0;
|
||||||
}
|
}
|
||||||
if !before_had_asks {
|
if !before_had_asks {
|
||||||
serum.lowest_placed_ask = 0.0;
|
serum.lowest_placed_ask = 0.0;
|
||||||
|
serum.highest_placed_ask = 0.0;
|
||||||
}
|
}
|
||||||
|
// in the normal quote per base units
|
||||||
|
let limit_price = limit_price_lots as f64 * quote_lot_size as f64 / base_lot_size as f64;
|
||||||
|
|
||||||
let new_order_on_book = after_oo_free_slots != before_oo_free_slots;
|
let new_order_on_book = after_oo_free_slots != before_oo_free_slots;
|
||||||
if new_order_on_book {
|
if new_order_on_book {
|
||||||
match side {
|
match side {
|
||||||
Serum3Side::Ask => {
|
Serum3Side::Ask => {
|
||||||
// in the normal quote per base units
|
|
||||||
let limit_price =
|
|
||||||
limit_price_lots as f64 * quote_lot_size as f64 / base_lot_size as f64;
|
|
||||||
serum.lowest_placed_ask = if serum.lowest_placed_ask == 0.0 {
|
serum.lowest_placed_ask = if serum.lowest_placed_ask == 0.0 {
|
||||||
limit_price
|
limit_price
|
||||||
} else {
|
} else {
|
||||||
serum.lowest_placed_ask.min(limit_price)
|
serum.lowest_placed_ask.min(limit_price)
|
||||||
};
|
};
|
||||||
|
serum.highest_placed_ask = if serum.highest_placed_ask == 0.0 {
|
||||||
|
limit_price
|
||||||
|
} else {
|
||||||
|
serum.highest_placed_ask.max(limit_price)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Serum3Side::Bid => {
|
Serum3Side::Bid => {
|
||||||
// in base per quote units, to avoid a division in health
|
// in base per quote units, to avoid a division in health
|
||||||
let limit_price_inv =
|
let limit_price_inv = 1.0 / limit_price;
|
||||||
base_lot_size as f64 / (limit_price_lots as f64 * quote_lot_size as f64);
|
|
||||||
serum.highest_placed_bid_inv = if serum.highest_placed_bid_inv == 0.0 {
|
serum.highest_placed_bid_inv = if serum.highest_placed_bid_inv == 0.0 {
|
||||||
limit_price_inv
|
limit_price_inv
|
||||||
} else {
|
} else {
|
||||||
// the highest bid has the lowest _inv value
|
// the highest bid has the lowest _inv value
|
||||||
serum.highest_placed_bid_inv.min(limit_price_inv)
|
serum.highest_placed_bid_inv.min(limit_price_inv)
|
||||||
};
|
};
|
||||||
|
serum.lowest_placed_bid_inv = if serum.lowest_placed_bid_inv == 0.0 {
|
||||||
|
limit_price_inv
|
||||||
|
} else {
|
||||||
|
// lowest bid has max _inv value
|
||||||
|
serum.lowest_placed_bid_inv.max(limit_price_inv)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emit!(Serum3OpenOrdersBalanceLogV2 {
|
emit_stack(Serum3OpenOrdersBalanceLogV2 {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.account.key(),
|
mango_account: ctx.accounts.account.key(),
|
||||||
market_index: serum_market.market_index,
|
market_index: serum_market.market_index,
|
||||||
|
@ -236,14 +284,27 @@ 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()?;
|
||||||
|
|
||||||
// Enforce min vault to deposits ratio
|
// Update the potential token tracking in banks
|
||||||
let withdrawn_from_vault = I80F48::from(before_vault - after_vault);
|
// (for init weight scaling, deposit limit checks)
|
||||||
let position_native = account
|
if is_v2_instruction {
|
||||||
|
let mut receiver_bank = receiver_bank_ai.load_mut::<Bank>()?;
|
||||||
|
let (base_bank, quote_bank) = match side {
|
||||||
|
Serum3Side::Bid => (&mut receiver_bank, &mut payer_bank),
|
||||||
|
Serum3Side::Ask => (&mut payer_bank, &mut receiver_bank),
|
||||||
|
};
|
||||||
|
update_bank_potential_tokens(serum, base_bank, quote_bank, &after_oo);
|
||||||
|
} else {
|
||||||
|
update_bank_potential_tokens_payer_only(serum, &mut payer_bank, &after_oo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track position before withdraw happens
|
||||||
|
let before_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(),
|
||||||
|
@ -255,20 +316,80 @@ pub fn serum3_place_order(
|
||||||
)?
|
)?
|
||||||
};
|
};
|
||||||
|
|
||||||
if withdrawn_from_vault > position_native {
|
// Deposit limit check: Placing an order can increase deposit limit use on both
|
||||||
|
// the payer and receiver bank. Imagine placing a bid for 500 base @ 0.5: it would
|
||||||
|
// use up 1000 quote and 500 base because either could be deposit on cancel/fill.
|
||||||
|
// This is why this must happen after update_bank_potential_tokens() and any withdraws.
|
||||||
|
{
|
||||||
|
let receiver_bank = receiver_bank_ai.load::<Bank>()?;
|
||||||
|
receiver_bank
|
||||||
|
.check_deposit_and_oo_limit()
|
||||||
|
.with_context(|| std::format!("on {}", receiver_bank.name()))?;
|
||||||
|
payer_bank
|
||||||
|
.check_deposit_and_oo_limit()
|
||||||
|
.with_context(|| std::format!("on {}", payer_bank.name()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payer bank safety checks like reduce-only, net borrows, vault-to-deposits ratio
|
||||||
|
let payer_bank_oracle =
|
||||||
|
payer_bank.oracle_price(&AccountInfoRef::borrow(&ctx.accounts.payer_oracle)?, None)?;
|
||||||
|
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,
|
||||||
"the payer tokens cannot be borrowed"
|
"the payer tokens cannot be borrowed"
|
||||||
);
|
);
|
||||||
let oracle_price =
|
|
||||||
payer_bank.oracle_price(&AccountInfoRef::borrow(&ctx.accounts.payer_oracle)?, None)?;
|
|
||||||
payer_bank.enforce_min_vault_to_deposits_ratio((*ctx.accounts.payer_vault).as_ref())?;
|
payer_bank.enforce_min_vault_to_deposits_ratio((*ctx.accounts.payer_vault).as_ref())?;
|
||||||
payer_bank.check_net_borrows(oracle_price)?;
|
payer_bank.check_net_borrows(payer_bank_oracle)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
vault_difference.adjust_health_cache_token_balance(&mut health_cache, &payer_bank)?;
|
// Limit order price bands: If the order ends up on the book, ensure
|
||||||
|
// - a bid isn't too far below oracle
|
||||||
|
// - an ask isn't too far above oracle
|
||||||
|
// because placing orders that are guaranteed to never be hit can be bothersome:
|
||||||
|
// For example placing a very large bid near zero would make the potential_base_tokens
|
||||||
|
// value go through the roof, reducing available init margin for other users.
|
||||||
|
let band_threshold = serum_market.oracle_price_band();
|
||||||
|
if new_order_on_book && band_threshold != f32::MAX {
|
||||||
|
let (base_oracle, quote_oracle) = match side {
|
||||||
|
Serum3Side::Bid => (&receiver_bank_oracle, &payer_bank_oracle),
|
||||||
|
Serum3Side::Ask => (&payer_bank_oracle, &receiver_bank_oracle),
|
||||||
|
};
|
||||||
|
let base_oracle_f64 = base_oracle.to_num::<f64>();
|
||||||
|
let quote_oracle_f64 = quote_oracle.to_num::<f64>();
|
||||||
|
// this has the same units as base_oracle: USD per BASE; limit_price is in QUOTE per BASE
|
||||||
|
let limit_price_in_dollar = limit_price * quote_oracle_f64;
|
||||||
|
let band_factor = 1.0 + band_threshold as f64;
|
||||||
|
match side {
|
||||||
|
Serum3Side::Bid => {
|
||||||
|
require_msg_typed!(
|
||||||
|
limit_price_in_dollar * band_factor >= base_oracle_f64,
|
||||||
|
MangoError::Serum3PriceBandExceeded,
|
||||||
|
"bid price {} must be larger than {} ({}% of oracle)",
|
||||||
|
limit_price,
|
||||||
|
base_oracle_f64 / (quote_oracle_f64 * band_factor),
|
||||||
|
(100.0 / band_factor) as u64,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Serum3Side::Ask => {
|
||||||
|
require_msg_typed!(
|
||||||
|
limit_price_in_dollar <= base_oracle_f64 * band_factor,
|
||||||
|
MangoError::Serum3PriceBandExceeded,
|
||||||
|
"ask price {} must be smaller than {} ({}% of oracle)",
|
||||||
|
limit_price,
|
||||||
|
base_oracle_f64 * band_factor / quote_oracle_f64,
|
||||||
|
(100.0 * band_factor) as u64,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health cache updates for the changed account state
|
||||||
|
let receiver_bank = receiver_bank_ai.load::<Bank>()?;
|
||||||
|
// update scaled weights for receiver bank
|
||||||
|
health_cache.adjust_token_balance(&receiver_bank, I80F48::ZERO)?;
|
||||||
|
vault_difference.adjust_health_cache_token_balance(&mut health_cache, &payer_bank)?;
|
||||||
let serum_account = account.serum3_orders(serum_market.market_index)?;
|
let serum_account = account.serum3_orders(serum_market.market_index)?;
|
||||||
oo_difference.recompute_health_cache_serum3_state(
|
oo_difference.recompute_health_cache_serum3_state(
|
||||||
&mut health_cache,
|
&mut health_cache,
|
||||||
|
@ -378,40 +499,29 @@ fn apply_vault_difference(
|
||||||
.min(I80F48::ZERO)
|
.min(I80F48::ZERO)
|
||||||
.abs()
|
.abs()
|
||||||
.to_num::<u64>();
|
.to_num::<u64>();
|
||||||
// amount of tokens transferred to serum3 reserved that were taken from deposits
|
|
||||||
let used_deposits = native_before
|
|
||||||
.min(-needed_change)
|
|
||||||
.max(I80F48::ZERO)
|
|
||||||
.to_num::<u64>();
|
|
||||||
|
|
||||||
let indexed_position = position.indexed_position;
|
let indexed_position = position.indexed_position;
|
||||||
let market = account.serum3_orders_mut(serum_market_index).unwrap();
|
let market = account.serum3_orders_mut(serum_market_index).unwrap();
|
||||||
let borrows_without_fee;
|
let borrows_without_fee;
|
||||||
let deposits_reserved;
|
|
||||||
if bank.token_index == market.base_token_index {
|
if bank.token_index == market.base_token_index {
|
||||||
borrows_without_fee = &mut market.base_borrows_without_fee;
|
borrows_without_fee = &mut market.base_borrows_without_fee;
|
||||||
deposits_reserved = &mut market.base_deposits_reserved;
|
|
||||||
} else if bank.token_index == market.quote_token_index {
|
} else if bank.token_index == market.quote_token_index {
|
||||||
borrows_without_fee = &mut market.quote_borrows_without_fee;
|
borrows_without_fee = &mut market.quote_borrows_without_fee;
|
||||||
deposits_reserved = &mut market.quote_deposits_reserved;
|
|
||||||
} else {
|
} else {
|
||||||
return Err(error_msg!(
|
return Err(error_msg!(
|
||||||
"assert failed: apply_vault_difference called with bad token index"
|
"assert failed: apply_vault_difference called with bad token index"
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only for place: Add to potential borrow amount and reserved deposits
|
// Only for place: Add to potential borrow amount
|
||||||
*borrows_without_fee += new_borrows;
|
*borrows_without_fee += new_borrows;
|
||||||
*deposits_reserved += used_deposits;
|
|
||||||
let used_deposits_signed: i64 = used_deposits.try_into().unwrap();
|
|
||||||
bank.deposits_in_serum += used_deposits_signed;
|
|
||||||
|
|
||||||
// Only for settle/liq_force_cancel: Reduce the potential borrow amounts
|
// Only for settle/liq_force_cancel: Reduce the potential borrow amounts
|
||||||
if needed_change > 0 {
|
if needed_change > 0 {
|
||||||
*borrows_without_fee = (*borrows_without_fee).saturating_sub(needed_change.to_num::<u64>());
|
*borrows_without_fee = (*borrows_without_fee).saturating_sub(needed_change.to_num::<u64>());
|
||||||
}
|
}
|
||||||
|
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: bank.group,
|
mango_group: bank.group,
|
||||||
mango_account: account_pk,
|
mango_account: account_pk,
|
||||||
token_index: bank.token_index,
|
token_index: bank.token_index,
|
||||||
|
@ -498,25 +608,10 @@ pub fn apply_settle_changes(
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Tokens were moved from open orders into banks again: also update the tracking
|
// Tokens were moved from open orders into banks again: also update the tracking
|
||||||
// for deposits_in_serum on the banks.
|
// for potential_serum_tokens on the banks.
|
||||||
{
|
{
|
||||||
let serum_orders = account.serum3_orders_mut(serum_market.market_index)?;
|
let serum_orders = account.serum3_orders_mut(serum_market.market_index)?;
|
||||||
|
update_bank_potential_tokens(serum_orders, base_bank, quote_bank, after_oo);
|
||||||
let after_base_reserved = after_oo.native_base_reserved();
|
|
||||||
if after_base_reserved < serum_orders.base_deposits_reserved {
|
|
||||||
let diff = serum_orders.base_deposits_reserved - after_base_reserved;
|
|
||||||
serum_orders.base_deposits_reserved = after_base_reserved;
|
|
||||||
let diff_signed: i64 = diff.try_into().unwrap();
|
|
||||||
base_bank.deposits_in_serum -= diff_signed;
|
|
||||||
}
|
|
||||||
|
|
||||||
let after_quote_reserved = after_oo.native_quote_reserved();
|
|
||||||
if after_quote_reserved < serum_orders.quote_deposits_reserved {
|
|
||||||
let diff = serum_orders.quote_deposits_reserved - after_quote_reserved;
|
|
||||||
serum_orders.quote_deposits_reserved = after_quote_reserved;
|
|
||||||
let diff_signed: i64 = diff.try_into().unwrap();
|
|
||||||
quote_bank.deposits_in_serum -= diff_signed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(health_cache) = health_cache {
|
if let Some(health_cache) = health_cache {
|
||||||
|
@ -534,6 +629,58 @@ pub fn apply_settle_changes(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_bank_potential_tokens_payer_only(
|
||||||
|
serum_orders: &mut Serum3Orders,
|
||||||
|
payer_bank: &mut Bank,
|
||||||
|
oo: &OpenOrdersSlim,
|
||||||
|
) {
|
||||||
|
// Do the tracking for the avaliable bank
|
||||||
|
if serum_orders.base_token_index == payer_bank.token_index {
|
||||||
|
let new_base = oo.native_base_total()
|
||||||
|
+ (oo.native_quote_reserved() as f64 * serum_orders.lowest_placed_bid_inv) as u64;
|
||||||
|
let old_base = serum_orders.potential_base_tokens;
|
||||||
|
|
||||||
|
payer_bank.update_potential_serum_tokens(old_base, new_base);
|
||||||
|
serum_orders.potential_base_tokens = new_base;
|
||||||
|
} else {
|
||||||
|
assert_eq!(serum_orders.quote_token_index, payer_bank.token_index);
|
||||||
|
|
||||||
|
let new_quote = oo.native_quote_total()
|
||||||
|
+ (oo.native_base_reserved() as f64 * serum_orders.highest_placed_ask) as u64;
|
||||||
|
let old_quote = serum_orders.potential_quote_tokens;
|
||||||
|
|
||||||
|
payer_bank.update_potential_serum_tokens(old_quote, new_quote);
|
||||||
|
serum_orders.potential_quote_tokens = new_quote;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_bank_potential_tokens(
|
||||||
|
serum_orders: &mut Serum3Orders,
|
||||||
|
base_bank: &mut Bank,
|
||||||
|
quote_bank: &mut Bank,
|
||||||
|
oo: &OpenOrdersSlim,
|
||||||
|
) {
|
||||||
|
assert_eq!(serum_orders.base_token_index, base_bank.token_index);
|
||||||
|
assert_eq!(serum_orders.quote_token_index, quote_bank.token_index);
|
||||||
|
|
||||||
|
// Potential tokens are all tokens on the side, plus reserved on the other side
|
||||||
|
// converted at favorable price. This creates an overestimation of the potential
|
||||||
|
// base and quote tokens flowing out of this open orders account.
|
||||||
|
let new_base = oo.native_base_total()
|
||||||
|
+ (oo.native_quote_reserved() as f64 * serum_orders.lowest_placed_bid_inv) as u64;
|
||||||
|
let new_quote = oo.native_quote_total()
|
||||||
|
+ (oo.native_base_reserved() as f64 * serum_orders.highest_placed_ask) as u64;
|
||||||
|
|
||||||
|
let old_base = serum_orders.potential_base_tokens;
|
||||||
|
let old_quote = serum_orders.potential_quote_tokens;
|
||||||
|
|
||||||
|
base_bank.update_potential_serum_tokens(old_base, new_base);
|
||||||
|
quote_bank.update_potential_serum_tokens(old_quote, new_quote);
|
||||||
|
|
||||||
|
serum_orders.potential_base_tokens = new_base;
|
||||||
|
serum_orders.potential_quote_tokens = new_quote;
|
||||||
|
}
|
||||||
|
|
||||||
fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Result<()> {
|
fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Result<()> {
|
||||||
use crate::serum3_cpi;
|
use crate::serum3_cpi;
|
||||||
|
|
||||||
|
|
|
@ -6,12 +6,13 @@ use crate::state::*;
|
||||||
use crate::util::fill_from_str;
|
use crate::util::fill_from_str;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::logs::Serum3RegisterMarketLog;
|
use crate::logs::{emit_stack, Serum3RegisterMarketLog};
|
||||||
|
|
||||||
pub fn serum3_register_market(
|
pub fn serum3_register_market(
|
||||||
ctx: Context<Serum3RegisterMarket>,
|
ctx: Context<Serum3RegisterMarket>,
|
||||||
market_index: Serum3MarketIndex,
|
market_index: Serum3MarketIndex,
|
||||||
name: String,
|
name: String,
|
||||||
|
oracle_price_band: f32,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// TODO: must guard against accidentally using the same market_index twice!
|
// TODO: must guard against accidentally using the same market_index twice!
|
||||||
|
|
||||||
|
@ -44,6 +45,7 @@ pub fn serum3_register_market(
|
||||||
market_index,
|
market_index,
|
||||||
bump: *ctx.bumps.get("serum_market").ok_or(MangoError::SomeError)?,
|
bump: *ctx.bumps.get("serum_market").ok_or(MangoError::SomeError)?,
|
||||||
padding2: Default::default(),
|
padding2: Default::default(),
|
||||||
|
oracle_price_band,
|
||||||
registration_time: Clock::get()?.unix_timestamp.try_into().unwrap(),
|
registration_time: Clock::get()?.unix_timestamp.try_into().unwrap(),
|
||||||
reserved: [0; 128],
|
reserved: [0; 128],
|
||||||
};
|
};
|
||||||
|
@ -55,7 +57,7 @@ pub fn serum3_register_market(
|
||||||
reserved: [0; 38],
|
reserved: [0; 38],
|
||||||
};
|
};
|
||||||
|
|
||||||
emit!(Serum3RegisterMarketLog {
|
emit_stack(Serum3RegisterMarketLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
serum_market: ctx.accounts.serum_market.key(),
|
serum_market: ctx.accounts.serum_market.key(),
|
||||||
market_index,
|
market_index,
|
||||||
|
|
|
@ -7,8 +7,9 @@ use crate::state::*;
|
||||||
|
|
||||||
use super::apply_settle_changes;
|
use super::apply_settle_changes;
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::logs::Serum3OpenOrdersBalanceLogV2;
|
use crate::logs::{
|
||||||
use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanLog};
|
emit_stack, LoanOriginationFeeInstruction, Serum3OpenOrdersBalanceLogV2, WithdrawLoanLog,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::accounts_zerocopy::AccountInfoRef;
|
use crate::accounts_zerocopy::AccountInfoRef;
|
||||||
|
|
||||||
|
@ -132,7 +133,7 @@ pub fn serum3_settle_funds<'info>(
|
||||||
v2.map(|d| d.quote_oracle.as_ref()),
|
v2.map(|d| d.quote_oracle.as_ref()),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
emit!(Serum3OpenOrdersBalanceLogV2 {
|
emit_stack(Serum3OpenOrdersBalanceLogV2 {
|
||||||
mango_group: accounts.group.key(),
|
mango_group: accounts.group.key(),
|
||||||
mango_account: accounts.account.key(),
|
mango_account: accounts.account.key(),
|
||||||
market_index: serum_market.market_index,
|
market_index: serum_market.market_index,
|
||||||
|
@ -188,14 +189,14 @@ pub fn charge_loan_origination_fees(
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
emit!(WithdrawLoanLog {
|
emit_stack(WithdrawLoanLog {
|
||||||
mango_group: *group_pubkey,
|
mango_group: *group_pubkey,
|
||||||
mango_account: *account_pubkey,
|
mango_account: *account_pubkey,
|
||||||
token_index: base_bank.token_index,
|
token_index: base_bank.token_index,
|
||||||
loan_amount: withdraw_result.loan_amount.to_bits(),
|
loan_amount: withdraw_result.loan_amount.to_bits(),
|
||||||
loan_origination_fee: withdraw_result.loan_origination_fee.to_bits(),
|
loan_origination_fee: withdraw_result.loan_origination_fee.to_bits(),
|
||||||
instruction: LoanOriginationFeeInstruction::Serum3SettleFunds,
|
instruction: LoanOriginationFeeInstruction::Serum3SettleFunds,
|
||||||
price: base_oracle_price.map(|p| p.to_bits())
|
price: base_oracle_price.map(|p| p.to_bits()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,14 +225,14 @@ pub fn charge_loan_origination_fees(
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
emit!(WithdrawLoanLog {
|
emit_stack(WithdrawLoanLog {
|
||||||
mango_group: *group_pubkey,
|
mango_group: *group_pubkey,
|
||||||
mango_account: *account_pubkey,
|
mango_account: *account_pubkey,
|
||||||
token_index: quote_bank.token_index,
|
token_index: quote_bank.token_index,
|
||||||
loan_amount: withdraw_result.loan_amount.to_bits(),
|
loan_amount: withdraw_result.loan_amount.to_bits(),
|
||||||
loan_origination_fee: withdraw_result.loan_origination_fee.to_bits(),
|
loan_origination_fee: withdraw_result.loan_origination_fee.to_bits(),
|
||||||
instruction: LoanOriginationFeeInstruction::Serum3SettleFunds,
|
instruction: LoanOriginationFeeInstruction::Serum3SettleFunds,
|
||||||
price: quote_oracle_price.map(|p| p.to_bits())
|
price: quote_oracle_price.map(|p| p.to_bits()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ use anchor_lang::prelude::*;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::error::MangoError;
|
use crate::error::MangoError;
|
||||||
use crate::logs::TokenConditionalSwapCancelLog;
|
use crate::logs::{emit_stack, TokenConditionalSwapCancelLog};
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
@ -31,7 +31,7 @@ pub fn token_conditional_swap_cancel(
|
||||||
);
|
);
|
||||||
*tcs = TokenConditionalSwap::default();
|
*tcs = TokenConditionalSwap::default();
|
||||||
|
|
||||||
emit!(TokenConditionalSwapCancelLog {
|
emit_stack(TokenConditionalSwapCancelLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.account.key(),
|
mango_account: ctx.accounts.account.key(),
|
||||||
id: token_conditional_swap_id,
|
id: token_conditional_swap_id,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::logs::TokenConditionalSwapCreateLogV3;
|
use crate::logs::{emit_stack, TokenConditionalSwapCreateLogV3};
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
@ -56,7 +56,7 @@ pub fn token_conditional_swap_create(
|
||||||
require_gte!(tcs.price_lower_limit, 0.0);
|
require_gte!(tcs.price_lower_limit, 0.0);
|
||||||
require_gte!(tcs.price_upper_limit, 0.0);
|
require_gte!(tcs.price_upper_limit, 0.0);
|
||||||
|
|
||||||
emit!(TokenConditionalSwapCreateLogV3 {
|
emit_stack(TokenConditionalSwapCreateLogV3 {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.account.key(),
|
mango_account: ctx.accounts.account.key(),
|
||||||
id,
|
id,
|
||||||
|
|
|
@ -5,8 +5,7 @@ use crate::accounts_ix::*;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::health::*;
|
use crate::health::*;
|
||||||
use crate::i80f48::ClampToInt;
|
use crate::i80f48::ClampToInt;
|
||||||
use crate::logs::TokenBalanceLog;
|
use crate::logs::{emit_stack, TokenBalanceLog, TokenConditionalSwapStartLog};
|
||||||
use crate::logs::TokenConditionalSwapStartLog;
|
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
@ -44,7 +43,7 @@ pub fn token_conditional_swap_start(
|
||||||
MangoError::TokenConditionalSwapTypeNotStartable
|
MangoError::TokenConditionalSwapTypeNotStartable
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut health_cache = new_health_cache(&liqee.borrow(), &account_retriever)
|
let mut health_cache = new_health_cache(&liqee.borrow(), &account_retriever, now_ts)
|
||||||
.context("create liqee health cache")?;
|
.context("create liqee health cache")?;
|
||||||
let pre_init_health = liqee.check_health_pre(&health_cache)?;
|
let pre_init_health = liqee.check_health_pre(&health_cache)?;
|
||||||
|
|
||||||
|
@ -65,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();
|
||||||
|
|
||||||
|
@ -74,28 +78,27 @@ 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
|
||||||
.adjust_token_balance(sell_bank, liqee_sell_post_balance - liqee_sell_pre_balance)?;
|
.adjust_token_balance(sell_bank, liqee_sell_post_balance - liqee_sell_pre_balance)?;
|
||||||
|
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: *group_pk,
|
mango_group: *group_pk,
|
||||||
mango_account: liqee_key,
|
mango_account: liqee_key,
|
||||||
token_index: sell_token_index,
|
token_index: sell_token_index,
|
||||||
|
@ -103,7 +106,7 @@ pub fn token_conditional_swap_start(
|
||||||
deposit_index: sell_bank.deposit_index.to_bits(),
|
deposit_index: sell_bank.deposit_index.to_bits(),
|
||||||
borrow_index: sell_bank.borrow_index.to_bits(),
|
borrow_index: sell_bank.borrow_index.to_bits(),
|
||||||
});
|
});
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: *group_pk,
|
mango_group: *group_pk,
|
||||||
mango_account: liqor_key,
|
mango_account: liqor_key,
|
||||||
token_index: sell_token_index,
|
token_index: sell_token_index,
|
||||||
|
@ -111,7 +114,7 @@ pub fn token_conditional_swap_start(
|
||||||
deposit_index: sell_bank.deposit_index.to_bits(),
|
deposit_index: sell_bank.deposit_index.to_bits(),
|
||||||
borrow_index: sell_bank.borrow_index.to_bits(),
|
borrow_index: sell_bank.borrow_index.to_bits(),
|
||||||
});
|
});
|
||||||
emit!(TokenConditionalSwapStartLog {
|
emit_stack(TokenConditionalSwapStartLog {
|
||||||
mango_group: *group_pk,
|
mango_group: *group_pk,
|
||||||
mango_account: liqee_key,
|
mango_account: liqee_key,
|
||||||
caller: liqor_key,
|
caller: liqor_key,
|
||||||
|
|
|
@ -5,10 +5,9 @@ use crate::accounts_ix::*;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::health::*;
|
use crate::health::*;
|
||||||
use crate::i80f48::ClampToInt;
|
use crate::i80f48::ClampToInt;
|
||||||
use crate::logs::TokenConditionalSwapCancelLog;
|
|
||||||
use crate::logs::{
|
use crate::logs::{
|
||||||
LoanOriginationFeeInstruction, TokenBalanceLog, TokenConditionalSwapTriggerLogV3,
|
emit_stack, LoanOriginationFeeInstruction, TokenBalanceLog, TokenConditionalSwapCancelLog,
|
||||||
WithdrawLoanLog,
|
TokenConditionalSwapTriggerLogV3, WithdrawLoanLog,
|
||||||
};
|
};
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
|
||||||
|
@ -73,7 +72,7 @@ pub fn token_conditional_swap_trigger(
|
||||||
liqee.token_decrement_dust_deactivate(sell_bank, now_ts, liqee_key)?;
|
liqee.token_decrement_dust_deactivate(sell_bank, now_ts, liqee_key)?;
|
||||||
|
|
||||||
msg!("TokenConditionalSwap is expired, removing");
|
msg!("TokenConditionalSwap is expired, removing");
|
||||||
emit!(TokenConditionalSwapCancelLog {
|
emit_stack(TokenConditionalSwapCancelLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.liqee.key(),
|
mango_account: ctx.accounts.liqee.key(),
|
||||||
id: token_conditional_swap_id,
|
id: token_conditional_swap_id,
|
||||||
|
@ -87,7 +86,7 @@ pub fn token_conditional_swap_trigger(
|
||||||
// changes when the tcs was created.
|
// changes when the tcs was created.
|
||||||
liqee.ensure_token_position(buy_token_index)?;
|
liqee.ensure_token_position(buy_token_index)?;
|
||||||
liqee.ensure_token_position(sell_token_index)?;
|
liqee.ensure_token_position(sell_token_index)?;
|
||||||
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever)
|
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever, now_ts)
|
||||||
.context("create liqee health cache")?;
|
.context("create liqee health cache")?;
|
||||||
|
|
||||||
let (buy_bank, buy_token_price, sell_bank_and_oracle_opt) =
|
let (buy_bank, buy_token_price, sell_bank_and_oracle_opt) =
|
||||||
|
@ -118,7 +117,12 @@ pub fn token_conditional_swap_trigger(
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check liqor health, liqee health is checked inside (has to be, since tcs closure depends on it)
|
// Check liqor health, liqee health is checked inside (has to be, since tcs closure depends on it)
|
||||||
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
|
let liqor_health = compute_health(
|
||||||
|
&liqor.borrow(),
|
||||||
|
HealthType::Init,
|
||||||
|
&account_retriever,
|
||||||
|
now_ts,
|
||||||
|
)
|
||||||
.context("compute liqor health")?;
|
.context("compute liqor health")?;
|
||||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||||
|
|
||||||
|
@ -290,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);
|
||||||
|
@ -303,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;
|
||||||
|
@ -332,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 {
|
||||||
|
@ -342,7 +342,7 @@ fn action(
|
||||||
// Log info
|
// Log info
|
||||||
|
|
||||||
// liqee buy token
|
// liqee buy token
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: liqee.fixed.group,
|
mango_group: liqee.fixed.group,
|
||||||
mango_account: liqee_key,
|
mango_account: liqee_key,
|
||||||
token_index: tcs.buy_token_index,
|
token_index: tcs.buy_token_index,
|
||||||
|
@ -351,7 +351,7 @@ fn action(
|
||||||
borrow_index: buy_bank.borrow_index.to_bits(),
|
borrow_index: buy_bank.borrow_index.to_bits(),
|
||||||
});
|
});
|
||||||
// liqee sell token
|
// liqee sell token
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
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,
|
||||||
|
@ -360,7 +360,7 @@ fn action(
|
||||||
borrow_index: sell_bank.borrow_index.to_bits(),
|
borrow_index: sell_bank.borrow_index.to_bits(),
|
||||||
});
|
});
|
||||||
// liqor buy token
|
// liqor buy token
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
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,
|
||||||
|
@ -369,7 +369,7 @@ fn action(
|
||||||
borrow_index: buy_bank.borrow_index.to_bits(),
|
borrow_index: buy_bank.borrow_index.to_bits(),
|
||||||
});
|
});
|
||||||
// liqor sell token
|
// liqor sell token
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: liqee.fixed.group,
|
mango_group: liqee.fixed.group,
|
||||||
mango_account: liqor_key,
|
mango_account: liqor_key,
|
||||||
token_index: tcs.sell_token_index,
|
token_index: tcs.sell_token_index,
|
||||||
|
@ -378,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!(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!(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()),
|
||||||
});
|
});
|
||||||
|
@ -483,7 +483,7 @@ fn action(
|
||||||
liqee.token_decrement_dust_deactivate(sell_bank, now_ts, liqee_key)?;
|
liqee.token_decrement_dust_deactivate(sell_bank, now_ts, liqee_key)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit!(TokenConditionalSwapTriggerLogV3 {
|
emit_stack(TokenConditionalSwapTriggerLogV3 {
|
||||||
mango_group: liqee.fixed.group,
|
mango_group: liqee.fixed.group,
|
||||||
liqee: liqee_key,
|
liqee: liqee_key,
|
||||||
liqor: liqor_key,
|
liqor: liqor_key,
|
||||||
|
@ -797,7 +797,7 @@ mod tests {
|
||||||
let retriever =
|
let retriever =
|
||||||
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
|
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
|
||||||
let mut liqee_health_cache =
|
let mut liqee_health_cache =
|
||||||
crate::health::new_health_cache(&setup.liqee.borrow(), &retriever).unwrap();
|
crate::health::new_health_cache(&setup.liqee.borrow(), &retriever, 0).unwrap();
|
||||||
|
|
||||||
action(
|
action(
|
||||||
&mut self.liqor.borrow_mut(),
|
&mut self.liqor.borrow_mut(),
|
||||||
|
|
|
@ -10,7 +10,7 @@ use crate::health::*;
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::logs::{DepositLog, TokenBalanceLog};
|
use crate::logs::*;
|
||||||
|
|
||||||
struct DepositCommon<'a, 'info> {
|
struct DepositCommon<'a, 'info> {
|
||||||
pub group: &'a AccountLoader<'info, Group>,
|
pub group: &'a AccountLoader<'info, Group>,
|
||||||
|
@ -90,17 +90,22 @@ 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>();
|
||||||
account.fixed.net_deposits += amount_usd;
|
account.fixed.net_deposits += amount_usd;
|
||||||
|
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: self.group.key(),
|
mango_group: self.group.key(),
|
||||||
mango_account: self.account.key(),
|
mango_account: self.account.key(),
|
||||||
token_index,
|
token_index,
|
||||||
|
@ -114,11 +119,12 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
|
||||||
// Health computation
|
// Health computation
|
||||||
//
|
//
|
||||||
let retriever = new_fixed_order_account_retriever(remaining_accounts, &account.borrow())?;
|
let retriever = new_fixed_order_account_retriever(remaining_accounts, &account.borrow())?;
|
||||||
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
|
|
||||||
// We only compute health to check if the account leaves the being_liquidated state.
|
// We only compute health to check if the account leaves the being_liquidated state.
|
||||||
// So it's ok to possibly skip token positions for bad oracles and compute a health
|
// So it's ok to possibly skip token positions for bad oracles and compute a health
|
||||||
// value that is too low.
|
// value that is too low.
|
||||||
let cache = new_health_cache_skipping_bad_oracles(&account.borrow(), &retriever)?;
|
let cache = new_health_cache_skipping_bad_oracles(&account.borrow(), &retriever, now_ts)?;
|
||||||
|
|
||||||
// Since depositing can only increase health, we can skip the usual pre-health computation.
|
// Since depositing can only increase health, we can skip the usual pre-health computation.
|
||||||
// Also, TokenDeposit is one of the rare instructions that is allowed even during being_liquidated.
|
// Also, TokenDeposit is one of the rare instructions that is allowed even during being_liquidated.
|
||||||
|
@ -162,7 +168,20 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
|
||||||
account.deactivate_token_position_and_log(raw_token_index, self.account.key());
|
account.deactivate_token_position_and_log(raw_token_index, self.account.key());
|
||||||
}
|
}
|
||||||
|
|
||||||
emit!(DepositLog {
|
unsafe {
|
||||||
|
const POS_PTR: *mut usize = 0x300000000 as usize as *mut usize;
|
||||||
|
msg!("heap {}", *POS_PTR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit_stack(DepositLog {
|
||||||
|
// mango_group: self.group.key(),
|
||||||
|
// mango_account: self.account.key(),
|
||||||
|
// signer: self.token_authority.key(),
|
||||||
|
// token_index,
|
||||||
|
// quantity: amount_i80f48.to_num::<u64>(),
|
||||||
|
// price: unsafe_oracle_price.to_bits(),
|
||||||
|
// });
|
||||||
|
emit_stack(DepositLog {
|
||||||
mango_group: self.group.key(),
|
mango_group: self.group.key(),
|
||||||
mango_account: self.account.key(),
|
mango_account: self.account.key(),
|
||||||
signer: self.token_authority.key(),
|
signer: self.token_authority.key(),
|
||||||
|
@ -171,6 +190,11 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
|
||||||
price: unsafe_oracle_price.to_bits(),
|
price: unsafe_oracle_price.to_bits(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
const POS_PTR: *mut usize = 0x300000000 as usize as *mut usize;
|
||||||
|
msg!("heap {}", *POS_PTR);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ use crate::error::MangoError;
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::logs::TokenMetaDataLog;
|
use crate::logs::{emit_stack, TokenMetaDataLog};
|
||||||
use crate::util::fill_from_str;
|
use crate::util::fill_from_str;
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
|
@ -42,6 +42,15 @@ pub fn token_edit(
|
||||||
token_conditional_swap_taker_fee_rate_opt: Option<f32>,
|
token_conditional_swap_taker_fee_rate_opt: Option<f32>,
|
||||||
token_conditional_swap_maker_fee_rate_opt: Option<f32>,
|
token_conditional_swap_maker_fee_rate_opt: Option<f32>,
|
||||||
flash_loan_swap_fee_rate_opt: Option<f32>,
|
flash_loan_swap_fee_rate_opt: Option<f32>,
|
||||||
|
interest_curve_scaling_opt: Option<f32>,
|
||||||
|
interest_target_utilization_opt: Option<f32>,
|
||||||
|
maint_weight_shift_start_opt: Option<u64>,
|
||||||
|
maint_weight_shift_end_opt: Option<u64>,
|
||||||
|
maint_weight_shift_asset_target_opt: Option<f32>,
|
||||||
|
maint_weight_shift_liab_target_opt: Option<f32>,
|
||||||
|
maint_weight_shift_abort: bool,
|
||||||
|
set_fallback_oracle: bool, // unused, introduced in v0.22
|
||||||
|
deposit_limit_opt: Option<u64>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let group = ctx.accounts.group.load()?;
|
let group = ctx.accounts.group.load()?;
|
||||||
|
|
||||||
|
@ -339,6 +348,106 @@ pub fn token_edit(
|
||||||
bank.flash_loan_swap_fee_rate = fee_rate;
|
bank.flash_loan_swap_fee_rate = fee_rate;
|
||||||
require_group_admin = true;
|
require_group_admin = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(interest_curve_scaling) = interest_curve_scaling_opt {
|
||||||
|
msg!(
|
||||||
|
"Interest curve scaling old {:?}, new {:?}",
|
||||||
|
bank.interest_curve_scaling,
|
||||||
|
interest_curve_scaling
|
||||||
|
);
|
||||||
|
require_gte!(interest_curve_scaling, 1.0);
|
||||||
|
bank.interest_curve_scaling = interest_curve_scaling.into();
|
||||||
|
require_group_admin = true;
|
||||||
|
}
|
||||||
|
if let Some(interest_target_utilization) = interest_target_utilization_opt {
|
||||||
|
msg!(
|
||||||
|
"Interest target utilization old {:?}, new {:?}",
|
||||||
|
bank.interest_target_utilization,
|
||||||
|
interest_target_utilization
|
||||||
|
);
|
||||||
|
require_gte!(interest_target_utilization, 0.0);
|
||||||
|
bank.interest_target_utilization = interest_target_utilization;
|
||||||
|
require_group_admin = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if maint_weight_shift_abort {
|
||||||
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
|
let (maint_asset_weight, maint_liab_weight) = bank.maint_weights(now_ts);
|
||||||
|
bank.maint_asset_weight = maint_asset_weight;
|
||||||
|
bank.maint_liab_weight = maint_liab_weight;
|
||||||
|
bank.maint_weight_shift_start = 0;
|
||||||
|
bank.maint_weight_shift_end = 0;
|
||||||
|
bank.maint_weight_shift_duration_inv = I80F48::ZERO;
|
||||||
|
bank.maint_weight_shift_asset_target = I80F48::ZERO;
|
||||||
|
bank.maint_weight_shift_liab_target = I80F48::ZERO;
|
||||||
|
msg!(
|
||||||
|
"Maint weight shift aborted, current maint weights asset {} liab {}",
|
||||||
|
maint_asset_weight,
|
||||||
|
maint_liab_weight,
|
||||||
|
);
|
||||||
|
// Allow execution by group admin or security admin
|
||||||
|
}
|
||||||
|
if let Some(maint_weight_shift_start) = maint_weight_shift_start_opt {
|
||||||
|
msg!(
|
||||||
|
"Maint weight shift start old {:?}, new {:?}",
|
||||||
|
bank.maint_weight_shift_start,
|
||||||
|
maint_weight_shift_start
|
||||||
|
);
|
||||||
|
bank.maint_weight_shift_start = maint_weight_shift_start;
|
||||||
|
require_group_admin = true;
|
||||||
|
}
|
||||||
|
if let Some(maint_weight_shift_end) = maint_weight_shift_end_opt {
|
||||||
|
msg!(
|
||||||
|
"Maint weight shift end old {:?}, new {:?}",
|
||||||
|
bank.maint_weight_shift_end,
|
||||||
|
maint_weight_shift_end
|
||||||
|
);
|
||||||
|
bank.maint_weight_shift_end = maint_weight_shift_end;
|
||||||
|
require_group_admin = true;
|
||||||
|
}
|
||||||
|
if let Some(maint_weight_shift_asset_target) = maint_weight_shift_asset_target_opt {
|
||||||
|
msg!(
|
||||||
|
"Maint weight shift asset target old {:?}, new {:?}",
|
||||||
|
bank.maint_weight_shift_asset_target,
|
||||||
|
maint_weight_shift_asset_target
|
||||||
|
);
|
||||||
|
bank.maint_weight_shift_asset_target =
|
||||||
|
I80F48::from_num(maint_weight_shift_asset_target);
|
||||||
|
require_group_admin = true;
|
||||||
|
}
|
||||||
|
if let Some(maint_weight_shift_liab_target) = maint_weight_shift_liab_target_opt {
|
||||||
|
msg!(
|
||||||
|
"Maint weight shift liab target old {:?}, new {:?}",
|
||||||
|
bank.maint_weight_shift_liab_target,
|
||||||
|
maint_weight_shift_liab_target
|
||||||
|
);
|
||||||
|
bank.maint_weight_shift_liab_target = I80F48::from_num(maint_weight_shift_liab_target);
|
||||||
|
require_group_admin = true;
|
||||||
|
}
|
||||||
|
if maint_weight_shift_start_opt.is_some() || maint_weight_shift_end_opt.is_some() {
|
||||||
|
let was_enabled = bank.maint_weight_shift_duration_inv.is_positive();
|
||||||
|
if bank.maint_weight_shift_end <= bank.maint_weight_shift_start {
|
||||||
|
bank.maint_weight_shift_duration_inv = I80F48::ZERO;
|
||||||
|
} else {
|
||||||
|
bank.maint_weight_shift_duration_inv = I80F48::ONE
|
||||||
|
/ I80F48::from(bank.maint_weight_shift_end - bank.maint_weight_shift_start);
|
||||||
|
}
|
||||||
|
msg!(
|
||||||
|
"Maint weight shift enabled old {}, new {}",
|
||||||
|
was_enabled,
|
||||||
|
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
|
||||||
|
@ -359,7 +468,7 @@ pub fn token_edit(
|
||||||
let bank = ctx.remaining_accounts.first().unwrap().load_mut::<Bank>()?;
|
let bank = ctx.remaining_accounts.first().unwrap().load_mut::<Bank>()?;
|
||||||
bank.verify()?;
|
bank.verify()?;
|
||||||
|
|
||||||
emit!(TokenMetaDataLog {
|
emit_stack(TokenMetaDataLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mint: mint_info.mint.key(),
|
mint: mint_info.mint.key(),
|
||||||
token_index: bank.token_index,
|
token_index: bank.token_index,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::health::*;
|
use crate::health::*;
|
||||||
use crate::logs::{TokenBalanceLog, TokenForceCloseBorrowsWithTokenLog};
|
use crate::logs::{emit_stack, TokenBalanceLog, TokenForceCloseBorrowsWithTokenLog};
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
use fixed::types::I80F48;
|
use fixed::types::I80F48;
|
||||||
|
@ -131,7 +131,7 @@ pub fn token_force_close_borrows_with_token(
|
||||||
);
|
);
|
||||||
|
|
||||||
// liqee asset
|
// liqee asset
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: liqee.fixed.group,
|
mango_group: liqee.fixed.group,
|
||||||
mango_account: liqee_key,
|
mango_account: liqee_key,
|
||||||
token_index: asset_token_index,
|
token_index: asset_token_index,
|
||||||
|
@ -140,7 +140,7 @@ pub fn token_force_close_borrows_with_token(
|
||||||
borrow_index: asset_bank.borrow_index.to_bits(),
|
borrow_index: asset_bank.borrow_index.to_bits(),
|
||||||
});
|
});
|
||||||
// liqee liab
|
// liqee liab
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: liqee.fixed.group,
|
mango_group: liqee.fixed.group,
|
||||||
mango_account: liqee_key,
|
mango_account: liqee_key,
|
||||||
token_index: liab_token_index,
|
token_index: liab_token_index,
|
||||||
|
@ -149,7 +149,7 @@ pub fn token_force_close_borrows_with_token(
|
||||||
borrow_index: liab_bank.borrow_index.to_bits(),
|
borrow_index: liab_bank.borrow_index.to_bits(),
|
||||||
});
|
});
|
||||||
// liqor asset
|
// liqor asset
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: liqee.fixed.group,
|
mango_group: liqee.fixed.group,
|
||||||
mango_account: liqor_key,
|
mango_account: liqor_key,
|
||||||
token_index: asset_token_index,
|
token_index: asset_token_index,
|
||||||
|
@ -158,7 +158,7 @@ pub fn token_force_close_borrows_with_token(
|
||||||
borrow_index: asset_bank.borrow_index.to_bits(),
|
borrow_index: asset_bank.borrow_index.to_bits(),
|
||||||
});
|
});
|
||||||
// liqor liab
|
// liqor liab
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: liqee.fixed.group,
|
mango_group: liqee.fixed.group,
|
||||||
mango_account: liqor_key,
|
mango_account: liqor_key,
|
||||||
token_index: liab_token_index,
|
token_index: liab_token_index,
|
||||||
|
@ -167,7 +167,7 @@ pub fn token_force_close_borrows_with_token(
|
||||||
borrow_index: liab_bank.borrow_index.to_bits(),
|
borrow_index: liab_bank.borrow_index.to_bits(),
|
||||||
});
|
});
|
||||||
|
|
||||||
emit!(TokenForceCloseBorrowsWithTokenLog {
|
emit_stack(TokenForceCloseBorrowsWithTokenLog {
|
||||||
mango_group: liqee.fixed.group,
|
mango_group: liqee.fixed.group,
|
||||||
liqee: liqee_key,
|
liqee: liqee_key,
|
||||||
liqor: liqor_key,
|
liqor: liqor_key,
|
||||||
|
@ -186,7 +186,8 @@ pub fn token_force_close_borrows_with_token(
|
||||||
MangoError::SomeError
|
MangoError::SomeError
|
||||||
);
|
);
|
||||||
|
|
||||||
let liqee_health_cache = new_health_cache(&liqee.borrow(), &mut account_retriever)
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
|
let liqee_health_cache = new_health_cache(&liqee.borrow(), &mut account_retriever, now_ts)
|
||||||
.context("create liqee health cache")?;
|
.context("create liqee health cache")?;
|
||||||
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
||||||
liqee
|
liqee
|
||||||
|
@ -211,12 +212,17 @@ pub fn token_force_close_borrows_with_token(
|
||||||
// Check liqor's health
|
// Check liqor's health
|
||||||
// This should always improve liqor health, since we decrease the zero-asset-weight
|
// This should always improve liqor health, since we decrease the zero-asset-weight
|
||||||
// liab token and gain some asset token, this check is just for denfensive measure
|
// liab token and gain some asset token, this check is just for denfensive measure
|
||||||
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &mut account_retriever)
|
let liqor_health = compute_health(
|
||||||
|
&liqor.borrow(),
|
||||||
|
HealthType::Init,
|
||||||
|
&mut account_retriever,
|
||||||
|
now_ts,
|
||||||
|
)
|
||||||
.context("compute liqor health")?;
|
.context("compute liqor health")?;
|
||||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||||
|
|
||||||
// TODO log
|
// TODO log
|
||||||
// emit!(TokenForceCloseBorrowWithToken
|
// emit_stack(TokenForceCloseBorrowWithToken
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,8 @@ use crate::state::*;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::logs::{
|
use crate::logs::{
|
||||||
LoanOriginationFeeInstruction, TokenBalanceLog, TokenLiqBankruptcyLog, WithdrawLoanLog,
|
emit_stack, LoanOriginationFeeInstruction, TokenBalanceLog, TokenLiqBankruptcyLog,
|
||||||
|
WithdrawLoanLog,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn token_liq_bankruptcy(
|
pub fn token_liq_bankruptcy(
|
||||||
|
@ -42,9 +43,10 @@ pub fn token_liq_bankruptcy(
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut account_retriever = ScanningAccountRetriever::new(health_ais, group_pk)?;
|
let mut account_retriever = ScanningAccountRetriever::new(health_ais, group_pk)?;
|
||||||
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
|
|
||||||
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
|
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
|
||||||
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever)
|
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever, now_ts)
|
||||||
.context("create liqee health cache")?;
|
.context("create liqee health cache")?;
|
||||||
liqee_health_cache.require_after_phase2_liquidation()?;
|
liqee_health_cache.require_after_phase2_liquidation()?;
|
||||||
liqee.fixed.set_being_liquidated(true);
|
liqee.fixed.set_being_liquidated(true);
|
||||||
|
@ -105,8 +107,6 @@ pub fn token_liq_bankruptcy(
|
||||||
// liquidators to exploit the insurance fund for 1 native token each call.
|
// liquidators to exploit the insurance fund for 1 native token each call.
|
||||||
let liab_transfer = insurance_transfer_i80f48 / liab_to_quote_with_fee;
|
let liab_transfer = insurance_transfer_i80f48 / liab_to_quote_with_fee;
|
||||||
|
|
||||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
|
||||||
|
|
||||||
let mut liqee_liab_active = true;
|
let mut liqee_liab_active = true;
|
||||||
if insurance_transfer > 0 {
|
if insurance_transfer > 0 {
|
||||||
// liqee gets liab assets (enable dusting to prevent a case where the position is brought
|
// liqee gets liab assets (enable dusting to prevent a case where the position is brought
|
||||||
|
@ -138,7 +138,7 @@ pub fn token_liq_bankruptcy(
|
||||||
quote_bank.deposit(liqor_quote, insurance_transfer_i80f48, now_ts)?;
|
quote_bank.deposit(liqor_quote, insurance_transfer_i80f48, now_ts)?;
|
||||||
|
|
||||||
// liqor quote
|
// liqor quote
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.liqor.key(),
|
mango_account: ctx.accounts.liqor.key(),
|
||||||
token_index: INSURANCE_TOKEN_INDEX,
|
token_index: INSURANCE_TOKEN_INDEX,
|
||||||
|
@ -154,7 +154,7 @@ pub fn token_liq_bankruptcy(
|
||||||
liab_bank.withdraw_with_fee(liqor_liab, liab_transfer, now_ts)?;
|
liab_bank.withdraw_with_fee(liqor_liab, liab_transfer, now_ts)?;
|
||||||
|
|
||||||
// liqor liab
|
// liqor liab
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.liqor.key(),
|
mango_account: ctx.accounts.liqor.key(),
|
||||||
token_index: liab_token_index,
|
token_index: liab_token_index,
|
||||||
|
@ -165,8 +165,12 @@ pub fn token_liq_bankruptcy(
|
||||||
|
|
||||||
// Check liqor's health
|
// Check liqor's health
|
||||||
if !liqor.fixed.is_in_health_region() {
|
if !liqor.fixed.is_in_health_region() {
|
||||||
let liqor_health =
|
let liqor_health = compute_health(
|
||||||
compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)?;
|
&liqor.borrow(),
|
||||||
|
HealthType::Init,
|
||||||
|
&account_retriever,
|
||||||
|
now_ts,
|
||||||
|
)?;
|
||||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,14 +178,14 @@ pub fn token_liq_bankruptcy(
|
||||||
.loan_origination_fee
|
.loan_origination_fee
|
||||||
.is_positive()
|
.is_positive()
|
||||||
{
|
{
|
||||||
emit!(WithdrawLoanLog {
|
emit_stack(WithdrawLoanLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.liqor.key(),
|
mango_account: ctx.accounts.liqor.key(),
|
||||||
token_index: liab_token_index,
|
token_index: liab_token_index,
|
||||||
loan_amount: liqor_liab_withdraw_result.loan_amount.to_bits(),
|
loan_amount: liqor_liab_withdraw_result.loan_amount.to_bits(),
|
||||||
loan_origination_fee: liqor_liab_withdraw_result.loan_origination_fee.to_bits(),
|
loan_origination_fee: liqor_liab_withdraw_result.loan_origination_fee.to_bits(),
|
||||||
instruction: LoanOriginationFeeInstruction::LiqTokenBankruptcy,
|
instruction: LoanOriginationFeeInstruction::LiqTokenBankruptcy,
|
||||||
price: Some(liab_oracle_price.to_bits())
|
price: Some(liab_oracle_price.to_bits()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,7 +257,7 @@ pub fn token_liq_bankruptcy(
|
||||||
}
|
}
|
||||||
|
|
||||||
// liqee liab
|
// liqee liab
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.liqee.key(),
|
mango_account: ctx.accounts.liqee.key(),
|
||||||
token_index: liab_token_index,
|
token_index: liab_token_index,
|
||||||
|
@ -276,7 +280,7 @@ pub fn token_liq_bankruptcy(
|
||||||
liqee.deactivate_token_position_and_log(liqee_raw_token_index, ctx.accounts.liqee.key());
|
liqee.deactivate_token_position_and_log(liqee_raw_token_index, ctx.accounts.liqee.key());
|
||||||
}
|
}
|
||||||
|
|
||||||
emit!(TokenLiqBankruptcyLog {
|
emit_stack(TokenLiqBankruptcyLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
liqee: ctx.accounts.liqee.key(),
|
liqee: ctx.accounts.liqee.key(),
|
||||||
liqor: ctx.accounts.liqor.key(),
|
liqor: ctx.accounts.liqor.key(),
|
||||||
|
@ -287,7 +291,7 @@ pub fn token_liq_bankruptcy(
|
||||||
insurance_transfer: insurance_transfer_i80f48.to_bits(),
|
insurance_transfer: insurance_transfer_i80f48.to_bits(),
|
||||||
socialized_loss: socialized_loss.to_bits(),
|
socialized_loss: socialized_loss.to_bits(),
|
||||||
starting_liab_deposit_index: starting_deposit_index.to_bits(),
|
starting_liab_deposit_index: starting_deposit_index.to_bits(),
|
||||||
ending_liab_deposit_index: liab_deposit_index.to_bits()
|
ending_liab_deposit_index: liab_deposit_index.to_bits(),
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -5,7 +5,8 @@ use crate::accounts_ix::*;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::health::*;
|
use crate::health::*;
|
||||||
use crate::logs::{
|
use crate::logs::{
|
||||||
LoanOriginationFeeInstruction, TokenBalanceLog, TokenLiqWithTokenLog, WithdrawLoanLog,
|
emit_stack, LoanOriginationFeeInstruction, TokenBalanceLog, TokenLiqWithTokenLog,
|
||||||
|
WithdrawLoanLog,
|
||||||
};
|
};
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ pub fn token_liq_with_token(
|
||||||
require!(asset_token_index != liab_token_index, MangoError::SomeError);
|
require!(asset_token_index != liab_token_index, MangoError::SomeError);
|
||||||
let mut account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)
|
let mut account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)
|
||||||
.context("create account retriever")?;
|
.context("create account retriever")?;
|
||||||
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
|
|
||||||
require_keys_neq!(ctx.accounts.liqor.key(), ctx.accounts.liqee.key());
|
require_keys_neq!(ctx.accounts.liqor.key(), ctx.accounts.liqee.key());
|
||||||
let mut liqor = ctx.accounts.liqor.load_full_mut()?;
|
let mut liqor = ctx.accounts.liqor.load_full_mut()?;
|
||||||
|
@ -39,7 +41,7 @@ pub fn token_liq_with_token(
|
||||||
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
|
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
|
||||||
|
|
||||||
// Initial liqee health check
|
// Initial liqee health check
|
||||||
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever)
|
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever, now_ts)
|
||||||
.context("create liqee health cache")?;
|
.context("create liqee health cache")?;
|
||||||
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
||||||
liqee_health_cache.require_after_phase1_liquidation()?;
|
liqee_health_cache.require_after_phase1_liquidation()?;
|
||||||
|
@ -52,7 +54,6 @@ pub fn token_liq_with_token(
|
||||||
// Transfer some liab_token from liqor to liqee and
|
// Transfer some liab_token from liqor to liqee and
|
||||||
// transfer some asset_token from liqee to liqor.
|
// transfer some asset_token from liqee to liqor.
|
||||||
//
|
//
|
||||||
let now_ts = Clock::get()?.unix_timestamp.try_into().unwrap();
|
|
||||||
liquidation_action(
|
liquidation_action(
|
||||||
&mut account_retriever,
|
&mut account_retriever,
|
||||||
liab_token_index,
|
liab_token_index,
|
||||||
|
@ -69,7 +70,12 @@ pub fn token_liq_with_token(
|
||||||
|
|
||||||
// Check liqor's health
|
// Check liqor's health
|
||||||
if !liqor.fixed.is_in_health_region() {
|
if !liqor.fixed.is_in_health_region() {
|
||||||
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
|
let liqor_health = compute_health(
|
||||||
|
&liqor.borrow(),
|
||||||
|
HealthType::Init,
|
||||||
|
&account_retriever,
|
||||||
|
now_ts,
|
||||||
|
)
|
||||||
.context("compute liqor health")?;
|
.context("compute liqor health")?;
|
||||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||||
}
|
}
|
||||||
|
@ -252,7 +258,7 @@ pub(crate) fn liquidation_action(
|
||||||
);
|
);
|
||||||
|
|
||||||
// liqee asset
|
// liqee asset
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: liqee.fixed.group,
|
mango_group: liqee.fixed.group,
|
||||||
mango_account: liqee_key,
|
mango_account: liqee_key,
|
||||||
token_index: asset_token_index,
|
token_index: asset_token_index,
|
||||||
|
@ -261,7 +267,7 @@ pub(crate) fn liquidation_action(
|
||||||
borrow_index: asset_bank.borrow_index.to_bits(),
|
borrow_index: asset_bank.borrow_index.to_bits(),
|
||||||
});
|
});
|
||||||
// liqee liab
|
// liqee liab
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: liqee.fixed.group,
|
mango_group: liqee.fixed.group,
|
||||||
mango_account: liqee_key,
|
mango_account: liqee_key,
|
||||||
token_index: liab_token_index,
|
token_index: liab_token_index,
|
||||||
|
@ -270,7 +276,7 @@ pub(crate) fn liquidation_action(
|
||||||
borrow_index: liab_bank.borrow_index.to_bits(),
|
borrow_index: liab_bank.borrow_index.to_bits(),
|
||||||
});
|
});
|
||||||
// liqor asset
|
// liqor asset
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: liqee.fixed.group,
|
mango_group: liqee.fixed.group,
|
||||||
mango_account: liqor_key,
|
mango_account: liqor_key,
|
||||||
token_index: asset_token_index,
|
token_index: asset_token_index,
|
||||||
|
@ -279,7 +285,7 @@ pub(crate) fn liquidation_action(
|
||||||
borrow_index: asset_bank.borrow_index.to_bits(),
|
borrow_index: asset_bank.borrow_index.to_bits(),
|
||||||
});
|
});
|
||||||
// liqor liab
|
// liqor liab
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: liqee.fixed.group,
|
mango_group: liqee.fixed.group,
|
||||||
mango_account: liqor_key,
|
mango_account: liqor_key,
|
||||||
token_index: liab_token_index,
|
token_index: liab_token_index,
|
||||||
|
@ -292,14 +298,14 @@ pub(crate) fn liquidation_action(
|
||||||
.loan_origination_fee
|
.loan_origination_fee
|
||||||
.is_positive()
|
.is_positive()
|
||||||
{
|
{
|
||||||
emit!(WithdrawLoanLog {
|
emit_stack(WithdrawLoanLog {
|
||||||
mango_group: liqee.fixed.group,
|
mango_group: liqee.fixed.group,
|
||||||
mango_account: liqor_key,
|
mango_account: liqor_key,
|
||||||
token_index: liab_token_index,
|
token_index: liab_token_index,
|
||||||
loan_amount: liqor_liab_withdraw_result.loan_amount.to_bits(),
|
loan_amount: liqor_liab_withdraw_result.loan_amount.to_bits(),
|
||||||
loan_origination_fee: liqor_liab_withdraw_result.loan_origination_fee.to_bits(),
|
loan_origination_fee: liqor_liab_withdraw_result.loan_origination_fee.to_bits(),
|
||||||
instruction: LoanOriginationFeeInstruction::LiqTokenWithToken,
|
instruction: LoanOriginationFeeInstruction::LiqTokenWithToken,
|
||||||
price: Some(liab_oracle_price.to_bits())
|
price: Some(liab_oracle_price.to_bits()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,7 +329,7 @@ pub(crate) fn liquidation_action(
|
||||||
.fixed
|
.fixed
|
||||||
.maybe_recover_from_being_liquidated(liqee_liq_end_health);
|
.maybe_recover_from_being_liquidated(liqee_liq_end_health);
|
||||||
|
|
||||||
emit!(TokenLiqWithTokenLog {
|
emit_stack(TokenLiqWithTokenLog {
|
||||||
mango_group: liqee.fixed.group,
|
mango_group: liqee.fixed.group,
|
||||||
liqee: liqee_key,
|
liqee: liqee_key,
|
||||||
liqor: liqor_key,
|
liqor: liqor_key,
|
||||||
|
@ -334,7 +340,7 @@ pub(crate) fn liquidation_action(
|
||||||
asset_price: asset_oracle_price.to_bits(),
|
asset_price: asset_oracle_price.to_bits(),
|
||||||
liab_price: liab_oracle_price.to_bits(),
|
liab_price: liab_oracle_price.to_bits(),
|
||||||
bankruptcy: !liqee_health_cache.has_phase2_liquidatable()
|
bankruptcy: !liqee_health_cache.has_phase2_liquidatable()
|
||||||
& liqee_liq_end_health.is_negative()
|
& liqee_liq_end_health.is_negative(),
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -446,7 +452,7 @@ mod tests {
|
||||||
let retriever =
|
let retriever =
|
||||||
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
|
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
|
||||||
|
|
||||||
health::new_health_cache(&setup.liqee.borrow(), &retriever).unwrap()
|
health::new_health_cache(&setup.liqee.borrow(), &retriever, 0).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(&self, max_liab_transfer: I80F48) -> Result<Self> {
|
fn run(&self, max_liab_transfer: I80F48) -> Result<Self> {
|
||||||
|
@ -468,7 +474,7 @@ mod tests {
|
||||||
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
|
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
|
||||||
|
|
||||||
let mut liqee_health_cache =
|
let mut liqee_health_cache =
|
||||||
health::new_health_cache(&setup.liqee.borrow(), &retriever).unwrap();
|
health::new_health_cache(&setup.liqee.borrow(), &retriever, 0).unwrap();
|
||||||
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
||||||
|
|
||||||
liquidation_action(
|
liquidation_action(
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::error::*;
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
use crate::util::fill_from_str;
|
use crate::util::fill_from_str;
|
||||||
|
|
||||||
use crate::logs::TokenMetaDataLog;
|
use crate::logs::{emit_stack, TokenMetaDataLog};
|
||||||
|
|
||||||
pub const INDEX_START: I80F48 = I80F48::from_bits(1_000_000 * I80F48::ONE.to_bits());
|
pub const INDEX_START: I80F48 = I80F48::from_bits(1_000_000 * I80F48::ONE.to_bits());
|
||||||
|
|
||||||
|
@ -38,6 +38,10 @@ pub fn token_register(
|
||||||
token_conditional_swap_taker_fee_rate: f32,
|
token_conditional_swap_taker_fee_rate: f32,
|
||||||
token_conditional_swap_maker_fee_rate: f32,
|
token_conditional_swap_maker_fee_rate: f32,
|
||||||
flash_loan_swap_fee_rate: f32,
|
flash_loan_swap_fee_rate: f32,
|
||||||
|
interest_curve_scaling: f32,
|
||||||
|
interest_target_utilization: f32,
|
||||||
|
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 {
|
||||||
|
@ -107,11 +111,18 @@ pub fn token_register(
|
||||||
fees_withdrawn: 0,
|
fees_withdrawn: 0,
|
||||||
token_conditional_swap_taker_fee_rate,
|
token_conditional_swap_taker_fee_rate,
|
||||||
token_conditional_swap_maker_fee_rate,
|
token_conditional_swap_maker_fee_rate,
|
||||||
flash_loan_swap_fee_rate,
|
flash_loan_swap_fee_rate: flash_loan_swap_fee_rate,
|
||||||
interest_target_utilization: 0.0, // unused in v0.20.0
|
interest_target_utilization,
|
||||||
interest_curve_scaling: 0.0, // unused in v0.20.0
|
interest_curve_scaling: interest_curve_scaling.into(),
|
||||||
deposits_in_serum: 0,
|
potential_serum_tokens: 0,
|
||||||
reserved: [0; 2072],
|
maint_weight_shift_start: 0,
|
||||||
|
maint_weight_shift_end: 0,
|
||||||
|
maint_weight_shift_duration_inv: I80F48::ZERO,
|
||||||
|
maint_weight_shift_asset_target: I80F48::ZERO,
|
||||||
|
maint_weight_shift_liab_target: I80F48::ZERO,
|
||||||
|
fallback_oracle: Pubkey::default(), // unused, introduced in v0.22
|
||||||
|
deposit_limit,
|
||||||
|
reserved: [0; 1968],
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(oracle_price) =
|
if let Ok(oracle_price) =
|
||||||
|
@ -129,7 +140,7 @@ pub fn token_register(
|
||||||
*mint_info = MintInfo {
|
*mint_info = MintInfo {
|
||||||
group: ctx.accounts.group.key(),
|
group: ctx.accounts.group.key(),
|
||||||
token_index,
|
token_index,
|
||||||
group_insurance_fund: 1,
|
group_insurance_fund: if group_insurance_fund { 1 } else { 0 },
|
||||||
padding1: Default::default(),
|
padding1: Default::default(),
|
||||||
mint: ctx.accounts.mint.key(),
|
mint: ctx.accounts.mint.key(),
|
||||||
banks: Default::default(),
|
banks: Default::default(),
|
||||||
|
@ -142,7 +153,7 @@ pub fn token_register(
|
||||||
mint_info.banks[0] = ctx.accounts.bank.key();
|
mint_info.banks[0] = ctx.accounts.bank.key();
|
||||||
mint_info.vaults[0] = ctx.accounts.vault.key();
|
mint_info.vaults[0] = ctx.accounts.vault.key();
|
||||||
|
|
||||||
emit!(TokenMetaDataLog {
|
emit_stack(TokenMetaDataLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mint: ctx.accounts.mint.key(),
|
mint: ctx.accounts.mint.key(),
|
||||||
token_index,
|
token_index,
|
||||||
|
|
|
@ -7,7 +7,7 @@ use crate::instructions::INDEX_START;
|
||||||
use crate::state::*;
|
use crate::state::*;
|
||||||
use crate::util::fill_from_str;
|
use crate::util::fill_from_str;
|
||||||
|
|
||||||
use crate::logs::TokenMetaDataLog;
|
use crate::logs::{emit_stack, TokenMetaDataLog};
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
|
|
||||||
|
@ -45,8 +45,8 @@ pub fn token_register_trustless(
|
||||||
vault: ctx.accounts.vault.key(),
|
vault: ctx.accounts.vault.key(),
|
||||||
oracle: ctx.accounts.oracle.key(),
|
oracle: ctx.accounts.oracle.key(),
|
||||||
oracle_config: OracleConfig {
|
oracle_config: OracleConfig {
|
||||||
conf_filter: I80F48::from_num(0.10),
|
conf_filter: I80F48::from_num(1000.0), // effectively disabled
|
||||||
max_staleness_slots: 10000,
|
max_staleness_slots: -1,
|
||||||
reserved: [0; 72],
|
reserved: [0; 72],
|
||||||
},
|
},
|
||||||
stable_price_model: StablePriceModel::default(),
|
stable_price_model: StablePriceModel::default(),
|
||||||
|
@ -67,11 +67,11 @@ pub fn token_register_trustless(
|
||||||
collected_fees_native: I80F48::ZERO,
|
collected_fees_native: I80F48::ZERO,
|
||||||
loan_origination_fee_rate: I80F48::from_num(0.0020),
|
loan_origination_fee_rate: I80F48::from_num(0.0020),
|
||||||
loan_fee_rate: I80F48::from_num(0.005),
|
loan_fee_rate: I80F48::from_num(0.005),
|
||||||
maint_asset_weight: I80F48::from_num(0.75), // 4x leverage
|
maint_asset_weight: I80F48::from_num(0),
|
||||||
init_asset_weight: I80F48::from_num(0.5), // 2x leverage
|
init_asset_weight: I80F48::from_num(0),
|
||||||
maint_liab_weight: I80F48::from_num(1.25), // 4x leverage
|
maint_liab_weight: I80F48::from_num(1.4), // 2.5x
|
||||||
init_liab_weight: I80F48::from_num(1.5), // 2x leverage
|
init_liab_weight: I80F48::from_num(1.8), // 1.25x
|
||||||
liquidation_fee: I80F48::from_num(0.125),
|
liquidation_fee: I80F48::from_num(0.2),
|
||||||
dust: I80F48::ZERO,
|
dust: I80F48::ZERO,
|
||||||
flash_loan_token_account_initial: u64::MAX,
|
flash_loan_token_account_initial: u64::MAX,
|
||||||
flash_loan_approved_amount: 0,
|
flash_loan_approved_amount: 0,
|
||||||
|
@ -87,17 +87,24 @@ pub fn token_register_trustless(
|
||||||
net_borrows_in_window: 0,
|
net_borrows_in_window: 0,
|
||||||
borrow_weight_scale_start_quote: 5_000_000_000.0, // $5k
|
borrow_weight_scale_start_quote: 5_000_000_000.0, // $5k
|
||||||
deposit_weight_scale_start_quote: 5_000_000_000.0, // $5k
|
deposit_weight_scale_start_quote: 5_000_000_000.0, // $5k
|
||||||
reduce_only: 0, // allow both deposits and borrows
|
reduce_only: 2, // deposit-only
|
||||||
force_close: 0,
|
force_close: 0,
|
||||||
padding: Default::default(),
|
padding: Default::default(),
|
||||||
fees_withdrawn: 0,
|
fees_withdrawn: 0,
|
||||||
token_conditional_swap_taker_fee_rate: 0.0005,
|
token_conditional_swap_taker_fee_rate: 0.0,
|
||||||
token_conditional_swap_maker_fee_rate: 0.0005,
|
token_conditional_swap_maker_fee_rate: 0.0,
|
||||||
flash_loan_swap_fee_rate: 0.0,
|
flash_loan_swap_fee_rate: 0.0,
|
||||||
interest_target_utilization: 0.0, // unused in v0.20.0
|
interest_target_utilization: 0.5,
|
||||||
interest_curve_scaling: 0.0, // unused in v0.20.0
|
interest_curve_scaling: 4.0,
|
||||||
deposits_in_serum: 0,
|
potential_serum_tokens: 0,
|
||||||
reserved: [0; 2072],
|
maint_weight_shift_start: 0,
|
||||||
|
maint_weight_shift_end: 0,
|
||||||
|
maint_weight_shift_duration_inv: I80F48::ZERO,
|
||||||
|
maint_weight_shift_asset_target: I80F48::ZERO,
|
||||||
|
maint_weight_shift_liab_target: I80F48::ZERO,
|
||||||
|
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) =
|
||||||
|
@ -128,7 +135,7 @@ pub fn token_register_trustless(
|
||||||
mint_info.banks[0] = ctx.accounts.bank.key();
|
mint_info.banks[0] = ctx.accounts.bank.key();
|
||||||
mint_info.vaults[0] = ctx.accounts.vault.key();
|
mint_info.vaults[0] = ctx.accounts.vault.key();
|
||||||
|
|
||||||
emit!(TokenMetaDataLog {
|
emit_stack(TokenMetaDataLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mint: ctx.accounts.mint.key(),
|
mint: ctx.accounts.mint.key(),
|
||||||
token_index,
|
token_index,
|
||||||
|
|
|
@ -2,7 +2,7 @@ use anchor_lang::prelude::*;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::error::MangoError;
|
use crate::error::MangoError;
|
||||||
use crate::logs::{UpdateIndexLog, UpdateRateLog};
|
use crate::logs::{emit_stack, UpdateIndexLog, UpdateRateLogV2};
|
||||||
use crate::state::HOUR;
|
use crate::state::HOUR;
|
||||||
use crate::{
|
use crate::{
|
||||||
accounts_zerocopy::{AccountInfoRef, LoadMutZeroCopyRef, LoadZeroCopyRef},
|
accounts_zerocopy::{AccountInfoRef, LoadMutZeroCopyRef, LoadZeroCopyRef},
|
||||||
|
@ -99,7 +99,12 @@ pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Res
|
||||||
.update(now_ts as u64, price.to_num());
|
.update(now_ts as u64, price.to_num());
|
||||||
let stable_price_model = some_bank.stable_price_model;
|
let stable_price_model = some_bank.stable_price_model;
|
||||||
|
|
||||||
emit!(UpdateIndexLog {
|
// If a maint weight shift is done, copy the target into the normal values
|
||||||
|
// and clear the transition parameters.
|
||||||
|
let maint_shift_done = some_bank.maint_weight_shift_duration_inv.is_positive()
|
||||||
|
&& now_ts >= some_bank.maint_weight_shift_end;
|
||||||
|
|
||||||
|
emit_stack(UpdateIndexLog {
|
||||||
mango_group: mint_info.group.key(),
|
mango_group: mint_info.group.key(),
|
||||||
token_index: mint_info.token_index,
|
token_index: mint_info.token_index,
|
||||||
deposit_index: deposit_index.to_bits(),
|
deposit_index: deposit_index.to_bits(),
|
||||||
|
@ -135,37 +140,69 @@ pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Res
|
||||||
bank.avg_utilization = new_avg_utilization;
|
bank.avg_utilization = new_avg_utilization;
|
||||||
|
|
||||||
bank.stable_price_model = stable_price_model;
|
bank.stable_price_model = stable_price_model;
|
||||||
|
|
||||||
|
if maint_shift_done {
|
||||||
|
bank.maint_asset_weight = bank.maint_weight_shift_asset_target;
|
||||||
|
bank.maint_liab_weight = bank.maint_weight_shift_liab_target;
|
||||||
|
bank.maint_weight_shift_duration_inv = I80F48::ZERO;
|
||||||
|
bank.maint_weight_shift_asset_target = I80F48::ZERO;
|
||||||
|
bank.maint_weight_shift_liab_target = I80F48::ZERO;
|
||||||
|
bank.maint_weight_shift_start = 0;
|
||||||
|
bank.maint_weight_shift_end = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// compute optimal rates, and max rate and set them on the bank
|
// compute optimal rates, and max rate and set them on the bank
|
||||||
{
|
{
|
||||||
let some_bank = ctx.remaining_accounts[0].load::<Bank>()?;
|
let mut some_bank = ctx.remaining_accounts[0].load_mut::<Bank>()?;
|
||||||
|
|
||||||
let diff_ts = I80F48::from_num(now_ts - some_bank.bank_rate_last_updated);
|
let diff_ts = I80F48::from_num(now_ts - some_bank.bank_rate_last_updated);
|
||||||
|
|
||||||
// update each hour
|
// update each hour
|
||||||
if diff_ts > HOUR {
|
if diff_ts > HOUR {
|
||||||
let (rate0, rate1, max_rate) = some_bank.compute_rates();
|
// First setup when new parameters are introduced
|
||||||
|
if some_bank.interest_curve_scaling == 0.0 {
|
||||||
|
let old_max_rate = 0.5;
|
||||||
|
some_bank.interest_curve_scaling =
|
||||||
|
some_bank.max_rate.to_num::<f64>() / old_max_rate;
|
||||||
|
some_bank.interest_target_utilization = some_bank.util0.to_num();
|
||||||
|
|
||||||
emit!(UpdateRateLog {
|
let descale_factor = I80F48::from_num(1.0 / some_bank.interest_curve_scaling);
|
||||||
|
some_bank.rate0 *= descale_factor;
|
||||||
|
some_bank.rate1 *= descale_factor;
|
||||||
|
some_bank.max_rate *= descale_factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
some_bank.update_interest_rate_scaling();
|
||||||
|
|
||||||
|
let rate0 = some_bank.rate0;
|
||||||
|
let rate1 = some_bank.rate1;
|
||||||
|
let max_rate = some_bank.max_rate;
|
||||||
|
let scaling = some_bank.interest_curve_scaling;
|
||||||
|
let target_util = some_bank.interest_target_utilization;
|
||||||
|
|
||||||
|
emit_stack(UpdateRateLogV2 {
|
||||||
mango_group: mint_info.group.key(),
|
mango_group: mint_info.group.key(),
|
||||||
token_index: mint_info.token_index,
|
token_index: mint_info.token_index,
|
||||||
rate0: rate0.to_bits(),
|
rate0: rate0.to_bits(),
|
||||||
|
util0: some_bank.util0.to_bits(),
|
||||||
rate1: rate1.to_bits(),
|
rate1: rate1.to_bits(),
|
||||||
|
util1: some_bank.util1.to_bits(),
|
||||||
max_rate: max_rate.to_bits(),
|
max_rate: max_rate.to_bits(),
|
||||||
|
curve_scaling: some_bank.interest_curve_scaling,
|
||||||
|
target_utilization: some_bank.interest_target_utilization,
|
||||||
});
|
});
|
||||||
|
|
||||||
drop(some_bank);
|
drop(some_bank);
|
||||||
|
|
||||||
msg!("rate0 {}", rate0);
|
// Apply the new parameters to all banks
|
||||||
msg!("rate1 {}", rate1);
|
|
||||||
msg!("max_rate {}", max_rate);
|
|
||||||
|
|
||||||
for ai in ctx.remaining_accounts.iter() {
|
for ai in ctx.remaining_accounts.iter() {
|
||||||
let mut bank = ai.load_mut::<Bank>()?;
|
let mut bank = ai.load_mut::<Bank>()?;
|
||||||
|
|
||||||
bank.bank_rate_last_updated = now_ts;
|
bank.bank_rate_last_updated = now_ts;
|
||||||
|
bank.interest_curve_scaling = scaling;
|
||||||
|
bank.interest_target_utilization = target_util;
|
||||||
bank.rate0 = rate0;
|
bank.rate0 = rate0;
|
||||||
bank.rate1 = rate1;
|
bank.rate1 = rate1;
|
||||||
bank.max_rate = max_rate;
|
bank.max_rate = max_rate;
|
||||||
|
|
|
@ -7,13 +7,16 @@ use anchor_spl::token;
|
||||||
use fixed::types::I80F48;
|
use fixed::types::I80F48;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::logs::{LoanOriginationFeeInstruction, TokenBalanceLog, WithdrawLoanLog, WithdrawLog};
|
use crate::logs::{
|
||||||
|
emit_stack, LoanOriginationFeeInstruction, TokenBalanceLog, WithdrawLoanLog, WithdrawLog,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bool) -> Result<()> {
|
pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bool) -> Result<()> {
|
||||||
require_msg!(amount > 0, "withdraw amount must be positive");
|
require_msg!(amount > 0, "withdraw amount must be positive");
|
||||||
|
|
||||||
let group = ctx.accounts.group.load()?;
|
let group = ctx.accounts.group.load()?;
|
||||||
let token_index = ctx.accounts.bank.load()?.token_index;
|
let token_index = ctx.accounts.bank.load()?.token_index;
|
||||||
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
|
|
||||||
// Create the account's position for that token index
|
// Create the account's position for that token index
|
||||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
let mut account = ctx.accounts.account.load_full_mut()?;
|
||||||
|
@ -23,8 +26,8 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
||||||
let pre_health_opt = if !account.fixed.is_in_health_region() {
|
let pre_health_opt = if !account.fixed.is_in_health_region() {
|
||||||
let retriever =
|
let retriever =
|
||||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||||
let hc_result =
|
let hc_result = new_health_cache(&account.borrow(), &retriever, now_ts)
|
||||||
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw health cache");
|
.context("pre-withdraw health cache");
|
||||||
if hc_result.is_oracle_error() {
|
if hc_result.is_oracle_error() {
|
||||||
// We allow NOT checking the pre init health. That means later on the health
|
// We allow NOT checking the pre init health. That means later on the health
|
||||||
// check will be stricter (post_init > 0, without the post_init >= pre_init option)
|
// check will be stricter (post_init > 0, without the post_init >= pre_init option)
|
||||||
|
@ -102,7 +105,7 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
||||||
|
|
||||||
let native_position_after = position.native(&bank);
|
let native_position_after = position.native(&bank);
|
||||||
|
|
||||||
emit!(TokenBalanceLog {
|
emit_stack(TokenBalanceLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.account.key(),
|
mango_account: ctx.accounts.account.key(),
|
||||||
token_index,
|
token_index,
|
||||||
|
@ -132,7 +135,8 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
||||||
// Note that this must include the normal pre and post health checks.
|
// Note that this must include the normal pre and post health checks.
|
||||||
let retriever =
|
let retriever =
|
||||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||||
let health_cache = new_health_cache_skipping_bad_oracles(&account.borrow(), &retriever)
|
let health_cache =
|
||||||
|
new_health_cache_skipping_bad_oracles(&account.borrow(), &retriever, now_ts)
|
||||||
.context("special post-withdraw health-cache")?;
|
.context("special post-withdraw health-cache")?;
|
||||||
let post_init_health = health_cache.health(HealthType::Init);
|
let post_init_health = health_cache.health(HealthType::Init);
|
||||||
account.check_health_pre_checks(&health_cache, post_init_health)?;
|
account.check_health_pre_checks(&health_cache, post_init_health)?;
|
||||||
|
@ -149,7 +153,7 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
||||||
account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key());
|
account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key());
|
||||||
}
|
}
|
||||||
|
|
||||||
emit!(WithdrawLog {
|
emit_stack(WithdrawLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.account.key(),
|
mango_account: ctx.accounts.account.key(),
|
||||||
signer: ctx.accounts.owner.key(),
|
signer: ctx.accounts.owner.key(),
|
||||||
|
@ -159,7 +163,7 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
||||||
});
|
});
|
||||||
|
|
||||||
if withdraw_result.loan_origination_fee.is_positive() {
|
if withdraw_result.loan_origination_fee.is_positive() {
|
||||||
emit!(WithdrawLoanLog {
|
emit_stack(WithdrawLoanLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.account.key(),
|
mango_account: ctx.accounts.account.key(),
|
||||||
token_index,
|
token_index,
|
||||||
|
|
|
@ -150,6 +150,10 @@ pub mod mango_v4 {
|
||||||
token_conditional_swap_taker_fee_rate: f32,
|
token_conditional_swap_taker_fee_rate: f32,
|
||||||
token_conditional_swap_maker_fee_rate: f32,
|
token_conditional_swap_maker_fee_rate: f32,
|
||||||
flash_loan_swap_fee_rate: f32,
|
flash_loan_swap_fee_rate: f32,
|
||||||
|
interest_curve_scaling: f32,
|
||||||
|
interest_target_utilization: f32,
|
||||||
|
group_insurance_fund: bool,
|
||||||
|
deposit_limit: u64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
#[cfg(feature = "enable-gpl")]
|
#[cfg(feature = "enable-gpl")]
|
||||||
instructions::token_register(
|
instructions::token_register(
|
||||||
|
@ -177,6 +181,10 @@ pub mod mango_v4 {
|
||||||
token_conditional_swap_taker_fee_rate,
|
token_conditional_swap_taker_fee_rate,
|
||||||
token_conditional_swap_maker_fee_rate,
|
token_conditional_swap_maker_fee_rate,
|
||||||
flash_loan_swap_fee_rate,
|
flash_loan_swap_fee_rate,
|
||||||
|
interest_curve_scaling,
|
||||||
|
interest_target_utilization,
|
||||||
|
group_insurance_fund,
|
||||||
|
deposit_limit,
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -221,6 +229,15 @@ pub mod mango_v4 {
|
||||||
token_conditional_swap_taker_fee_rate_opt: Option<f32>,
|
token_conditional_swap_taker_fee_rate_opt: Option<f32>,
|
||||||
token_conditional_swap_maker_fee_rate_opt: Option<f32>,
|
token_conditional_swap_maker_fee_rate_opt: Option<f32>,
|
||||||
flash_loan_swap_fee_rate_opt: Option<f32>,
|
flash_loan_swap_fee_rate_opt: Option<f32>,
|
||||||
|
interest_curve_scaling_opt: Option<f32>,
|
||||||
|
interest_target_utilization_opt: Option<f32>,
|
||||||
|
maint_weight_shift_start_opt: Option<u64>,
|
||||||
|
maint_weight_shift_end_opt: Option<u64>,
|
||||||
|
maint_weight_shift_asset_target_opt: Option<f32>,
|
||||||
|
maint_weight_shift_liab_target_opt: Option<f32>,
|
||||||
|
maint_weight_shift_abort: bool,
|
||||||
|
set_fallback_oracle: bool, // unused, introduced in v0.22
|
||||||
|
deposit_limit_opt: Option<u64>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
#[cfg(feature = "enable-gpl")]
|
#[cfg(feature = "enable-gpl")]
|
||||||
instructions::token_edit(
|
instructions::token_edit(
|
||||||
|
@ -252,6 +269,15 @@ pub mod mango_v4 {
|
||||||
token_conditional_swap_taker_fee_rate_opt,
|
token_conditional_swap_taker_fee_rate_opt,
|
||||||
token_conditional_swap_maker_fee_rate_opt,
|
token_conditional_swap_maker_fee_rate_opt,
|
||||||
flash_loan_swap_fee_rate_opt,
|
flash_loan_swap_fee_rate_opt,
|
||||||
|
interest_curve_scaling_opt,
|
||||||
|
interest_target_utilization_opt,
|
||||||
|
maint_weight_shift_start_opt,
|
||||||
|
maint_weight_shift_end_opt,
|
||||||
|
maint_weight_shift_asset_target_opt,
|
||||||
|
maint_weight_shift_liab_target_opt,
|
||||||
|
maint_weight_shift_abort,
|
||||||
|
set_fallback_oracle,
|
||||||
|
deposit_limit_opt,
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -540,9 +566,10 @@ pub mod mango_v4 {
|
||||||
ctx: Context<Serum3RegisterMarket>,
|
ctx: Context<Serum3RegisterMarket>,
|
||||||
market_index: Serum3MarketIndex,
|
market_index: Serum3MarketIndex,
|
||||||
name: String,
|
name: String,
|
||||||
|
oracle_price_band: f32,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
#[cfg(feature = "enable-gpl")]
|
#[cfg(feature = "enable-gpl")]
|
||||||
instructions::serum3_register_market(ctx, market_index, name)?;
|
instructions::serum3_register_market(ctx, market_index, name, oracle_price_band)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -551,9 +578,16 @@ pub mod mango_v4 {
|
||||||
reduce_only_opt: Option<bool>,
|
reduce_only_opt: Option<bool>,
|
||||||
force_close_opt: Option<bool>,
|
force_close_opt: Option<bool>,
|
||||||
name_opt: Option<String>,
|
name_opt: Option<String>,
|
||||||
|
oracle_price_band_opt: Option<f32>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
#[cfg(feature = "enable-gpl")]
|
#[cfg(feature = "enable-gpl")]
|
||||||
instructions::serum3_edit_market(ctx, reduce_only_opt, force_close_opt, name_opt)?;
|
instructions::serum3_edit_market(
|
||||||
|
ctx,
|
||||||
|
reduce_only_opt,
|
||||||
|
force_close_opt,
|
||||||
|
name_opt,
|
||||||
|
oracle_price_band_opt,
|
||||||
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -603,6 +637,36 @@ pub mod mango_v4 {
|
||||||
order_type,
|
order_type,
|
||||||
client_order_id,
|
client_order_id,
|
||||||
limit,
|
limit,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// requires the receiver_bank in the health account list to be writable
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn serum3_place_order_v2(
|
||||||
|
ctx: Context<Serum3PlaceOrder>,
|
||||||
|
side: Serum3Side,
|
||||||
|
limit_price: u64,
|
||||||
|
max_base_qty: u64,
|
||||||
|
max_native_quote_qty_including_fees: u64,
|
||||||
|
self_trade_behavior: Serum3SelfTradeBehavior,
|
||||||
|
order_type: Serum3OrderType,
|
||||||
|
client_order_id: u64,
|
||||||
|
limit: u16,
|
||||||
|
) -> Result<()> {
|
||||||
|
#[cfg(feature = "enable-gpl")]
|
||||||
|
instructions::serum3_place_order(
|
||||||
|
ctx,
|
||||||
|
side,
|
||||||
|
limit_price,
|
||||||
|
max_base_qty,
|
||||||
|
max_native_quote_qty_including_fees,
|
||||||
|
self_trade_behavior,
|
||||||
|
order_type,
|
||||||
|
client_order_id,
|
||||||
|
limit,
|
||||||
|
true,
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,29 @@ use crate::{
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
use borsh::BorshSerialize;
|
use borsh::BorshSerialize;
|
||||||
|
|
||||||
|
#[inline(never)] // ensure fresh stack frame
|
||||||
|
pub fn emit_stack<T: anchor_lang::Event>(e: T) {
|
||||||
|
use std::io::{Cursor, Write};
|
||||||
|
|
||||||
|
// stack buffer, stack frames are 4kb
|
||||||
|
let mut buffer = [0u8; 3000];
|
||||||
|
|
||||||
|
let mut cursor = Cursor::new(&mut buffer[..]);
|
||||||
|
cursor.write_all(&T::DISCRIMINATOR).unwrap();
|
||||||
|
e.serialize(&mut cursor)
|
||||||
|
.expect("event must fit into stack buffer");
|
||||||
|
|
||||||
|
let pos = cursor.position() as usize;
|
||||||
|
anchor_lang::solana_program::log::sol_log_data(&[&buffer[..pos]]);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn emit_perp_balances(
|
pub fn emit_perp_balances(
|
||||||
mango_group: Pubkey,
|
mango_group: Pubkey,
|
||||||
mango_account: Pubkey,
|
mango_account: Pubkey,
|
||||||
pp: &PerpPosition,
|
pp: &PerpPosition,
|
||||||
pm: &PerpMarket,
|
pm: &PerpMarket,
|
||||||
) {
|
) {
|
||||||
emit!(PerpBalanceLog {
|
emit_stack(PerpBalanceLog {
|
||||||
mango_group,
|
mango_group,
|
||||||
mango_account,
|
mango_account,
|
||||||
market_index: pm.perp_market_index,
|
market_index: pm.perp_market_index,
|
||||||
|
@ -300,6 +316,20 @@ pub struct UpdateRateLog {
|
||||||
pub max_rate: i128, // I80F48
|
pub max_rate: i128, // I80F48
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[event]
|
||||||
|
pub struct UpdateRateLogV2 {
|
||||||
|
pub mango_group: Pubkey,
|
||||||
|
pub token_index: u16,
|
||||||
|
// contrary to v1 these do not have curve_scaling factored in!
|
||||||
|
pub rate0: i128, // I80F48
|
||||||
|
pub util0: i128, // I80F48
|
||||||
|
pub rate1: i128, // I80F48
|
||||||
|
pub util1: i128, // I80F48
|
||||||
|
pub max_rate: i128, // I80F48
|
||||||
|
pub curve_scaling: f64,
|
||||||
|
pub target_utilization: f32,
|
||||||
|
}
|
||||||
|
|
||||||
#[event]
|
#[event]
|
||||||
pub struct TokenLiqWithTokenLog {
|
pub struct TokenLiqWithTokenLog {
|
||||||
pub mango_group: Pubkey,
|
pub mango_group: Pubkey,
|
||||||
|
|
|
@ -18,7 +18,6 @@ pub const DAY: i64 = 86400;
|
||||||
pub const DAY_I80F48: I80F48 = I80F48::from_bits(86_400 * I80F48::ONE.to_bits());
|
pub const DAY_I80F48: I80F48 = I80F48::from_bits(86_400 * I80F48::ONE.to_bits());
|
||||||
pub const ONE_BPS: I80F48 = I80F48::from_bits(28147497671);
|
pub const ONE_BPS: I80F48 = I80F48::from_bits(28147497671);
|
||||||
pub const YEAR_I80F48: I80F48 = I80F48::from_bits(31_536_000 * I80F48::ONE.to_bits());
|
pub const YEAR_I80F48: I80F48 = I80F48::from_bits(31_536_000 * I80F48::ONE.to_bits());
|
||||||
pub const MINIMUM_MAX_RATE: I80F48 = I80F48::from_bits(I80F48::ONE.to_bits() / 2);
|
|
||||||
|
|
||||||
#[derive(Derivative)]
|
#[derive(Derivative)]
|
||||||
#[derivative(Debug)]
|
#[derivative(Debug)]
|
||||||
|
@ -151,19 +150,29 @@ pub struct Bank {
|
||||||
|
|
||||||
/// Target utilization: If actual utilization is higher, scale up interest.
|
/// Target utilization: If actual utilization is higher, scale up interest.
|
||||||
/// If it's lower, scale down interest (if possible)
|
/// If it's lower, scale down interest (if possible)
|
||||||
pub interest_target_utilization: f32, // unused in v0.20.0
|
pub interest_target_utilization: f32,
|
||||||
|
|
||||||
/// Current interest curve scaling, always >= 1.0
|
/// Current interest curve scaling, always >= 1.0
|
||||||
///
|
///
|
||||||
/// Except when first migrating to having this field, then 0.0
|
/// Except when first migrating to having this field, then 0.0
|
||||||
pub interest_curve_scaling: f64, // unused in v0.20.0
|
pub interest_curve_scaling: f64,
|
||||||
|
|
||||||
// user deposits that were moved into serum open orders
|
/// Largest amount of tokens that might be added the the bank based on
|
||||||
// can be negative due to multibank, then it'd need to be balanced in the keeper
|
/// serum open order execution.
|
||||||
pub deposits_in_serum: i64,
|
pub potential_serum_tokens: u64,
|
||||||
|
|
||||||
#[derivative(Debug = "ignore")]
|
pub maint_weight_shift_start: u64,
|
||||||
pub reserved: [u8; 2072],
|
pub maint_weight_shift_end: u64,
|
||||||
|
pub maint_weight_shift_duration_inv: I80F48,
|
||||||
|
pub maint_weight_shift_asset_target: I80F48,
|
||||||
|
pub maint_weight_shift_liab_target: I80F48,
|
||||||
|
|
||||||
|
pub fallback_oracle: Pubkey, // unused, introduced in v0.22
|
||||||
|
|
||||||
|
/// zero means none, in token native
|
||||||
|
pub deposit_limit: u64,
|
||||||
|
|
||||||
|
pub reserved: [u8; 1968],
|
||||||
}
|
}
|
||||||
const_assert_eq!(
|
const_assert_eq!(
|
||||||
size_of::<Bank>(),
|
size_of::<Bank>(),
|
||||||
|
@ -196,7 +205,11 @@ const_assert_eq!(
|
||||||
+ 8
|
+ 8
|
||||||
+ 4 * 4
|
+ 4 * 4
|
||||||
+ 8 * 2
|
+ 8 * 2
|
||||||
+ 2072
|
+ 8 * 2
|
||||||
|
+ 16 * 3
|
||||||
|
+ 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);
|
||||||
|
@ -213,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,
|
||||||
|
@ -231,7 +257,7 @@ impl Bank {
|
||||||
flash_loan_approved_amount: 0,
|
flash_loan_approved_amount: 0,
|
||||||
flash_loan_token_account_initial: u64::MAX,
|
flash_loan_token_account_initial: u64::MAX,
|
||||||
net_borrows_in_window: 0,
|
net_borrows_in_window: 0,
|
||||||
deposits_in_serum: 0,
|
potential_serum_tokens: 0,
|
||||||
bump,
|
bump,
|
||||||
bank_num,
|
bank_num,
|
||||||
|
|
||||||
|
@ -263,15 +289,15 @@ impl Bank {
|
||||||
token_index: existing_bank.token_index,
|
token_index: existing_bank.token_index,
|
||||||
mint_decimals: existing_bank.mint_decimals,
|
mint_decimals: existing_bank.mint_decimals,
|
||||||
oracle_config: existing_bank.oracle_config,
|
oracle_config: existing_bank.oracle_config,
|
||||||
stable_price_model: StablePriceModel::default(),
|
stable_price_model: existing_bank.stable_price_model,
|
||||||
min_vault_to_deposits_ratio: existing_bank.min_vault_to_deposits_ratio,
|
min_vault_to_deposits_ratio: existing_bank.min_vault_to_deposits_ratio,
|
||||||
net_borrow_limit_per_window_quote: existing_bank.net_borrow_limit_per_window_quote,
|
net_borrow_limit_per_window_quote: existing_bank.net_borrow_limit_per_window_quote,
|
||||||
net_borrow_limit_window_size_ts: existing_bank.net_borrow_limit_window_size_ts,
|
net_borrow_limit_window_size_ts: existing_bank.net_borrow_limit_window_size_ts,
|
||||||
last_net_borrows_window_start_ts: existing_bank.last_net_borrows_window_start_ts,
|
last_net_borrows_window_start_ts: existing_bank.last_net_borrows_window_start_ts,
|
||||||
borrow_weight_scale_start_quote: f64::MAX,
|
borrow_weight_scale_start_quote: existing_bank.borrow_weight_scale_start_quote,
|
||||||
deposit_weight_scale_start_quote: f64::MAX,
|
deposit_weight_scale_start_quote: existing_bank.deposit_weight_scale_start_quote,
|
||||||
reduce_only: 0,
|
reduce_only: existing_bank.reduce_only,
|
||||||
force_close: 0,
|
force_close: existing_bank.force_close,
|
||||||
padding: [0; 6],
|
padding: [0; 6],
|
||||||
token_conditional_swap_taker_fee_rate: existing_bank
|
token_conditional_swap_taker_fee_rate: existing_bank
|
||||||
.token_conditional_swap_taker_fee_rate,
|
.token_conditional_swap_taker_fee_rate,
|
||||||
|
@ -280,7 +306,14 @@ impl Bank {
|
||||||
flash_loan_swap_fee_rate: existing_bank.flash_loan_swap_fee_rate,
|
flash_loan_swap_fee_rate: existing_bank.flash_loan_swap_fee_rate,
|
||||||
interest_target_utilization: existing_bank.interest_target_utilization,
|
interest_target_utilization: existing_bank.interest_target_utilization,
|
||||||
interest_curve_scaling: existing_bank.interest_curve_scaling,
|
interest_curve_scaling: existing_bank.interest_curve_scaling,
|
||||||
reserved: [0; 2072],
|
maint_weight_shift_start: existing_bank.maint_weight_shift_start,
|
||||||
|
maint_weight_shift_end: existing_bank.maint_weight_shift_end,
|
||||||
|
maint_weight_shift_duration_inv: existing_bank.maint_weight_shift_duration_inv,
|
||||||
|
maint_weight_shift_asset_target: existing_bank.maint_weight_shift_asset_target,
|
||||||
|
maint_weight_shift_liab_target: existing_bank.maint_weight_shift_liab_target,
|
||||||
|
fallback_oracle: existing_bank.oracle,
|
||||||
|
deposit_limit: existing_bank.deposit_limit,
|
||||||
|
reserved: [0; 1968],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,7 +323,7 @@ impl Bank {
|
||||||
require_gte!(self.rate0, I80F48::ZERO);
|
require_gte!(self.rate0, I80F48::ZERO);
|
||||||
require_gte!(self.util1, I80F48::ZERO);
|
require_gte!(self.util1, I80F48::ZERO);
|
||||||
require_gte!(self.rate1, I80F48::ZERO);
|
require_gte!(self.rate1, I80F48::ZERO);
|
||||||
require_gte!(self.max_rate, MINIMUM_MAX_RATE);
|
require_gte!(self.max_rate, I80F48::ZERO);
|
||||||
require_gte!(self.loan_fee_rate, 0.0);
|
require_gte!(self.loan_fee_rate, 0.0);
|
||||||
require_gte!(self.loan_origination_fee_rate, 0.0);
|
require_gte!(self.loan_origination_fee_rate, 0.0);
|
||||||
require_gte!(self.maint_asset_weight, 0.0);
|
require_gte!(self.maint_asset_weight, 0.0);
|
||||||
|
@ -306,6 +339,11 @@ impl Bank {
|
||||||
require_gte!(self.token_conditional_swap_taker_fee_rate, 0.0);
|
require_gte!(self.token_conditional_swap_taker_fee_rate, 0.0);
|
||||||
require_gte!(self.token_conditional_swap_maker_fee_rate, 0.0);
|
require_gte!(self.token_conditional_swap_maker_fee_rate, 0.0);
|
||||||
require_gte!(self.flash_loan_swap_fee_rate, 0.0);
|
require_gte!(self.flash_loan_swap_fee_rate, 0.0);
|
||||||
|
require_gte!(self.interest_curve_scaling, 1.0);
|
||||||
|
require_gte!(self.interest_target_utilization, 0.0);
|
||||||
|
require_gte!(self.maint_weight_shift_duration_inv, 0.0);
|
||||||
|
require_gte!(self.maint_weight_shift_asset_target, 0.0);
|
||||||
|
require_gte!(self.maint_weight_shift_liab_target, 0.0);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,6 +375,26 @@ impl Bank {
|
||||||
self.deposit_index * self.indexed_deposits
|
self.deposit_index * self.indexed_deposits
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn maint_weights(&self, now_ts: u64) -> (I80F48, I80F48) {
|
||||||
|
if self.maint_weight_shift_duration_inv.is_zero() || now_ts <= self.maint_weight_shift_start
|
||||||
|
{
|
||||||
|
(self.maint_asset_weight, self.maint_liab_weight)
|
||||||
|
} else if now_ts >= self.maint_weight_shift_end {
|
||||||
|
(
|
||||||
|
self.maint_weight_shift_asset_target,
|
||||||
|
self.maint_weight_shift_liab_target,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let scale = I80F48::from(now_ts - self.maint_weight_shift_start)
|
||||||
|
* self.maint_weight_shift_duration_inv;
|
||||||
|
let asset = self.maint_asset_weight
|
||||||
|
+ scale * (self.maint_weight_shift_asset_target - self.maint_asset_weight);
|
||||||
|
let liab = self.maint_liab_weight
|
||||||
|
+ scale * (self.maint_weight_shift_liab_target - self.maint_liab_weight);
|
||||||
|
(asset, liab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Prevent borrowing away the full bank vault.
|
/// Prevent borrowing away the full bank vault.
|
||||||
/// Keep some in reserve to satisfy non-borrow withdraws.
|
/// Keep some in reserve to satisfy non-borrow withdraws.
|
||||||
pub fn enforce_min_vault_to_deposits_ratio(&self, vault_ai: &AccountInfo) -> Result<()> {
|
pub fn enforce_min_vault_to_deposits_ratio(&self, vault_ai: &AccountInfo) -> Result<()> {
|
||||||
|
@ -557,7 +615,7 @@ impl Bank {
|
||||||
require_gte!(native_amount, 0);
|
require_gte!(native_amount, 0);
|
||||||
let native_position = position.native(self);
|
let native_position = position.native(self);
|
||||||
|
|
||||||
if native_position.is_positive() {
|
if !native_position.is_negative() {
|
||||||
let new_native_position = native_position - native_amount;
|
let new_native_position = native_position - native_amount;
|
||||||
if !new_native_position.is_negative() {
|
if !new_native_position.is_negative() {
|
||||||
// withdraw deposits only
|
// withdraw deposits only
|
||||||
|
@ -687,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.
|
||||||
|
@ -732,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,
|
||||||
|
@ -815,6 +974,7 @@ impl Bank {
|
||||||
self.util1,
|
self.util1,
|
||||||
self.rate1,
|
self.rate1,
|
||||||
self.max_rate,
|
self.max_rate,
|
||||||
|
self.interest_curve_scaling,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -828,8 +988,9 @@ impl Bank {
|
||||||
util1: I80F48,
|
util1: I80F48,
|
||||||
rate1: I80F48,
|
rate1: I80F48,
|
||||||
max_rate: I80F48,
|
max_rate: I80F48,
|
||||||
|
scaling: f64,
|
||||||
) -> I80F48 {
|
) -> I80F48 {
|
||||||
if utilization <= util0 {
|
let v = if utilization <= util0 {
|
||||||
let slope = rate0 / util0;
|
let slope = rate0 / util0;
|
||||||
slope * utilization
|
slope * utilization
|
||||||
} else if utilization <= util1 {
|
} else if utilization <= util1 {
|
||||||
|
@ -840,6 +1001,13 @@ impl Bank {
|
||||||
let extra_util = utilization - util1;
|
let extra_util = utilization - util1;
|
||||||
let slope = (max_rate - rate1) / (I80F48::ONE - util1);
|
let slope = (max_rate - rate1) / (I80F48::ONE - util1);
|
||||||
rate1 + slope * extra_util
|
rate1 + slope * extra_util
|
||||||
|
};
|
||||||
|
|
||||||
|
// scaling will be 0 when it's introduced
|
||||||
|
if scaling == 0.0 {
|
||||||
|
v
|
||||||
|
} else {
|
||||||
|
v * I80F48::from_num(scaling)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -875,34 +1043,23 @@ impl Bank {
|
||||||
}
|
}
|
||||||
|
|
||||||
// computes new optimal rates and max rate
|
// computes new optimal rates and max rate
|
||||||
pub fn compute_rates(&self) -> (I80F48, I80F48, I80F48) {
|
pub fn update_interest_rate_scaling(&mut self) {
|
||||||
// interest rate legs 2 and 3 are seen as punitive legs, encouraging utilization to move towards optimal utilization
|
// Interest increases above target_util, decreases below
|
||||||
// lets choose util0 as optimal utilization and 0 to utli0 as the leg where we want the utlization to preferably be
|
let target_util = self.interest_target_utilization as f64;
|
||||||
let optimal_util = self.util0;
|
|
||||||
// use avg_utilization and not instantaneous_utilization so that rates cannot be manipulated easily
|
// use avg_utilization and not instantaneous_utilization so that rates cannot be manipulated easily
|
||||||
let avg_util = self.avg_utilization;
|
let avg_util = self.avg_utilization.to_num::<f64>();
|
||||||
|
|
||||||
// move rates up when utilization is above optimal utilization, and vice versa
|
// move rates up when utilization is above optimal utilization, and vice versa
|
||||||
// util factor is between -1 (avg util = 0) and +1 (avg util = 100%)
|
// util factor is between -1 (avg util = 0) and +1 (avg util = 100%)
|
||||||
let util_factor = if avg_util > optimal_util {
|
let util_factor = if avg_util > target_util {
|
||||||
(avg_util - optimal_util) / (I80F48::ONE - optimal_util)
|
(avg_util - target_util) / (1.0 - target_util)
|
||||||
} else {
|
} else {
|
||||||
(avg_util - optimal_util) / optimal_util
|
(avg_util - target_util) / target_util
|
||||||
};
|
};
|
||||||
let adjustment = I80F48::ONE + self.adjustment_factor * util_factor;
|
let adjustment = 1.0 + self.adjustment_factor.to_num::<f64>() * util_factor;
|
||||||
|
|
||||||
// 1. irrespective of which leg current utilization is in, update all rates
|
self.interest_curve_scaling = (self.interest_curve_scaling * adjustment).max(1.0)
|
||||||
// 2. only update rates as long as new adjusted rates are above MINIMUM_MAX_RATE,
|
|
||||||
// since we don't want to fall to such low rates that it would take a long time to
|
|
||||||
// recover to high rates if utilization suddently increases to a high value
|
|
||||||
if (self.max_rate * adjustment) > MINIMUM_MAX_RATE {
|
|
||||||
(
|
|
||||||
(self.rate0 * adjustment),
|
|
||||||
(self.rate1 * adjustment),
|
|
||||||
(self.max_rate * adjustment),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(self.rate0, self.rate1, self.max_rate)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn oracle_price(
|
pub fn oracle_price(
|
||||||
|
@ -934,7 +1091,8 @@ impl Bank {
|
||||||
if self.deposit_weight_scale_start_quote == f64::MAX {
|
if self.deposit_weight_scale_start_quote == f64::MAX {
|
||||||
return self.init_asset_weight;
|
return self.init_asset_weight;
|
||||||
}
|
}
|
||||||
let all_deposits = self.native_deposits().to_num::<f64>() + self.deposits_in_serum as f64;
|
let all_deposits =
|
||||||
|
self.native_deposits().to_num::<f64>() + self.potential_serum_tokens as f64;
|
||||||
let deposits_quote = all_deposits * price.to_num::<f64>();
|
let deposits_quote = all_deposits * price.to_num::<f64>();
|
||||||
if deposits_quote <= self.deposit_weight_scale_start_quote {
|
if deposits_quote <= self.deposit_weight_scale_start_quote {
|
||||||
self.init_asset_weight
|
self.init_asset_weight
|
||||||
|
@ -963,6 +1121,16 @@ impl Bank {
|
||||||
self.init_liab_weight * I80F48::from_num(scale)
|
self.init_liab_weight * I80F48::from_num(scale)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Grows potential_serum_tokens if new > old, shrinks it otherwise
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn update_potential_serum_tokens(&mut self, old: u64, new: u64) {
|
||||||
|
if new >= old {
|
||||||
|
self.potential_serum_tokens += new - old;
|
||||||
|
} else {
|
||||||
|
self.potential_serum_tokens = self.potential_serum_tokens.saturating_sub(old - new);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
|
@ -987,41 +1155,13 @@ mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
fn bank_change_runner(start: f64, change: i32, is_in_use: bool, use_withdraw: bool) {
|
||||||
pub fn change() -> Result<()> {
|
|
||||||
let epsilon = I80F48::from_bits(1);
|
|
||||||
let cases = [
|
|
||||||
(-10.1, 1),
|
|
||||||
(-10.1, 10),
|
|
||||||
(-10.1, 11),
|
|
||||||
(-10.1, 50),
|
|
||||||
(-10.0, 10),
|
|
||||||
(-10.0, 11),
|
|
||||||
(-2.0, 2),
|
|
||||||
(-2.0, 3),
|
|
||||||
(-0.1, 1),
|
|
||||||
(0.0, 1),
|
|
||||||
(0.1, 1),
|
|
||||||
(10.1, -1),
|
|
||||||
(10.1, -9),
|
|
||||||
(10.1, -10),
|
|
||||||
(10.1, -11),
|
|
||||||
(10.0, -10),
|
|
||||||
(10.0, -9),
|
|
||||||
(1.0, -1),
|
|
||||||
(0.1, -1),
|
|
||||||
(0.0, -1),
|
|
||||||
(-0.1, -1),
|
|
||||||
(-1.1, -10),
|
|
||||||
];
|
|
||||||
|
|
||||||
for is_in_use in [false, true] {
|
|
||||||
for (start, change) in cases {
|
|
||||||
println!(
|
println!(
|
||||||
"testing: in use: {}, start: {}, change: {}",
|
"testing: in use: {is_in_use}, start: {start}, change: {change}, use_withdraw: {use_withdraw}",
|
||||||
is_in_use, start, change
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let epsilon = I80F48::from_bits(1);
|
||||||
|
|
||||||
//
|
//
|
||||||
// SETUP
|
// SETUP
|
||||||
//
|
//
|
||||||
|
@ -1034,7 +1174,12 @@ mod tests {
|
||||||
bank.loan_origination_fee_rate = I80F48::from_num(0.1);
|
bank.loan_origination_fee_rate = I80F48::from_num(0.1);
|
||||||
let indexed = |v: I80F48, b: &Bank| {
|
let indexed = |v: I80F48, b: &Bank| {
|
||||||
if v > 0 {
|
if v > 0 {
|
||||||
v / b.deposit_index
|
let i = v / b.deposit_index;
|
||||||
|
if i * b.deposit_index < v {
|
||||||
|
i + I80F48::DELTA
|
||||||
|
} else {
|
||||||
|
i
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
v / b.borrow_index
|
v / b.borrow_index
|
||||||
}
|
}
|
||||||
|
@ -1068,12 +1213,23 @@ mod tests {
|
||||||
let change = I80F48::from(change);
|
let change = I80F48::from(change);
|
||||||
let dummy_now_ts = 1 as u64;
|
let dummy_now_ts = 1 as u64;
|
||||||
let dummy_price = I80F48::ZERO;
|
let dummy_price = I80F48::ZERO;
|
||||||
let is_active = bank
|
let is_active = if use_withdraw {
|
||||||
.change_with_fee(&mut account, change, dummy_now_ts)?
|
bank.withdraw_with_fee(&mut account, change, dummy_now_ts)
|
||||||
.position_is_active;
|
.unwrap()
|
||||||
|
.position_is_active
|
||||||
|
} else {
|
||||||
|
bank.change_with_fee(&mut account, change, dummy_now_ts)
|
||||||
|
.unwrap()
|
||||||
|
.position_is_active
|
||||||
|
};
|
||||||
|
|
||||||
let mut expected_native = start_native + change;
|
let mut expected_native = start_native + change;
|
||||||
if expected_native >= 0.0 && expected_native < 1.0 && !is_in_use {
|
let is_deposit_into_nonnegative = start >= 0.0 && change >= 0 && !use_withdraw;
|
||||||
|
if expected_native >= 0.0
|
||||||
|
&& expected_native < 1.0
|
||||||
|
&& !is_in_use
|
||||||
|
&& !is_deposit_into_nonnegative
|
||||||
|
{
|
||||||
assert!(!is_active);
|
assert!(!is_active);
|
||||||
assert_eq!(bank.dust, expected_native);
|
assert_eq!(bank.dust, expected_native);
|
||||||
expected_native = I80F48::ZERO;
|
expected_native = I80F48::ZERO;
|
||||||
|
@ -1098,10 +1254,193 @@ mod tests {
|
||||||
assert_eq!(bank.indexed_borrows, -account.indexed_position);
|
assert_eq!(bank.indexed_borrows, -account.indexed_position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn bank_change() -> Result<()> {
|
||||||
|
let cases = [
|
||||||
|
(-10.1, 1),
|
||||||
|
(-10.1, 10),
|
||||||
|
(-10.1, 11),
|
||||||
|
(-10.1, 50),
|
||||||
|
(-10.0, 10),
|
||||||
|
(-10.0, 11),
|
||||||
|
(-2.0, 2),
|
||||||
|
(-2.0, 3),
|
||||||
|
(-0.1, 1),
|
||||||
|
(0.0, 1),
|
||||||
|
(0.1, 1),
|
||||||
|
(10.1, -1),
|
||||||
|
(10.1, -9),
|
||||||
|
(10.1, -10),
|
||||||
|
(10.1, -11),
|
||||||
|
(10.0, -10),
|
||||||
|
(10.0, -9),
|
||||||
|
(1.0, -1),
|
||||||
|
(0.1, -1),
|
||||||
|
(0.0, -1),
|
||||||
|
(-0.1, -1),
|
||||||
|
(-1.1, -10),
|
||||||
|
(10.0, 0),
|
||||||
|
(1.0, 0),
|
||||||
|
(0.1, 0),
|
||||||
|
(0.0, 0),
|
||||||
|
(-0.1, 0),
|
||||||
|
];
|
||||||
|
|
||||||
|
for is_in_use in [false, true] {
|
||||||
|
for (start, change) in cases {
|
||||||
|
bank_change_runner(start, change, is_in_use, false);
|
||||||
|
if change == 0 {
|
||||||
|
// check withdrawing 0
|
||||||
|
bank_change_runner(start, change, is_in_use, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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();
|
||||||
|
@ -1185,4 +1524,48 @@ mod tests {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_bank_maint_weight_shift() -> Result<()> {
|
||||||
|
let mut bank = Bank::zeroed();
|
||||||
|
bank.maint_asset_weight = I80F48::ONE;
|
||||||
|
bank.maint_liab_weight = I80F48::ZERO;
|
||||||
|
bank.maint_weight_shift_start = 100;
|
||||||
|
bank.maint_weight_shift_end = 1100;
|
||||||
|
bank.maint_weight_shift_duration_inv = I80F48::ONE / I80F48::from(1000);
|
||||||
|
bank.maint_weight_shift_asset_target = I80F48::from(2);
|
||||||
|
bank.maint_weight_shift_liab_target = I80F48::from(10);
|
||||||
|
|
||||||
|
let (a, l) = bank.maint_weights(0);
|
||||||
|
assert_eq!(a, 1.0);
|
||||||
|
assert_eq!(l, 0.0);
|
||||||
|
|
||||||
|
let (a, l) = bank.maint_weights(100);
|
||||||
|
assert_eq!(a, 1.0);
|
||||||
|
assert_eq!(l, 0.0);
|
||||||
|
|
||||||
|
let (a, l) = bank.maint_weights(1100);
|
||||||
|
assert_eq!(a, 2.0);
|
||||||
|
assert_eq!(l, 10.0);
|
||||||
|
|
||||||
|
let (a, l) = bank.maint_weights(2000);
|
||||||
|
assert_eq!(a, 2.0);
|
||||||
|
assert_eq!(l, 10.0);
|
||||||
|
|
||||||
|
let abs_diff = |x: I80F48, y: f64| (x.to_num::<f64>() - y).abs();
|
||||||
|
|
||||||
|
let (a, l) = bank.maint_weights(600);
|
||||||
|
assert!(abs_diff(a, 1.5) < 1e-8);
|
||||||
|
assert!(abs_diff(l, 5.0) < 1e-8);
|
||||||
|
|
||||||
|
let (a, l) = bank.maint_weights(200);
|
||||||
|
assert!(abs_diff(a, 1.1) < 1e-8);
|
||||||
|
assert!(abs_diff(l, 1.0) < 1e-8);
|
||||||
|
|
||||||
|
let (a, l) = bank.maint_weights(1000);
|
||||||
|
assert!(abs_diff(a, 1.9) < 1e-8);
|
||||||
|
assert!(abs_diff(l, 9.0) < 1e-8);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -220,6 +220,7 @@ pub enum IxGate {
|
||||||
TokenConditionalSwapStart = 68,
|
TokenConditionalSwapStart = 68,
|
||||||
TokenConditionalSwapCreatePremiumAuction = 69,
|
TokenConditionalSwapCreatePremiumAuction = 69,
|
||||||
TokenConditionalSwapCreateLinearAuction = 70,
|
TokenConditionalSwapCreateLinearAuction = 70,
|
||||||
|
Serum3PlaceOrderV2 = 71,
|
||||||
// NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction.
|
// NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ use static_assertions::const_assert_eq;
|
||||||
|
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::health::{HealthCache, HealthType};
|
use crate::health::{HealthCache, HealthType};
|
||||||
use crate::logs::{DeactivatePerpPositionLog, DeactivateTokenPositionLog};
|
use crate::logs::{emit_stack, DeactivatePerpPositionLog, DeactivateTokenPositionLog};
|
||||||
use crate::util;
|
use crate::util;
|
||||||
|
|
||||||
use super::BookSideOrderTree;
|
use super::BookSideOrderTree;
|
||||||
|
@ -1014,7 +1014,7 @@ impl<
|
||||||
let mango_group = self.fixed().group;
|
let mango_group = self.fixed().group;
|
||||||
let token_position = self.token_position_mut_by_raw_index(raw_index);
|
let token_position = self.token_position_mut_by_raw_index(raw_index);
|
||||||
assert!(token_position.in_use_count == 0);
|
assert!(token_position.in_use_count == 0);
|
||||||
emit!(DeactivateTokenPositionLog {
|
emit_stack(DeactivateTokenPositionLog {
|
||||||
mango_group,
|
mango_group,
|
||||||
mango_account: mango_account_pubkey,
|
mango_account: mango_account_pubkey,
|
||||||
token_index: token_position.token_index,
|
token_index: token_position.token_index,
|
||||||
|
@ -1168,7 +1168,7 @@ impl<
|
||||||
let mango_group = self.fixed().group;
|
let mango_group = self.fixed().group;
|
||||||
let perp_position = self.perp_position_mut(perp_market_index)?;
|
let perp_position = self.perp_position_mut(perp_market_index)?;
|
||||||
|
|
||||||
emit!(DeactivatePerpPositionLog {
|
emit_stack(DeactivatePerpPositionLog {
|
||||||
mango_group,
|
mango_group,
|
||||||
mango_account: mango_account_pubkey,
|
mango_account: mango_account_pubkey,
|
||||||
market_index: perp_market_index,
|
market_index: perp_market_index,
|
||||||
|
|
|
@ -145,20 +145,28 @@ pub struct Serum3Orders {
|
||||||
pub highest_placed_bid_inv: f64,
|
pub highest_placed_bid_inv: f64,
|
||||||
pub lowest_placed_ask: f64,
|
pub lowest_placed_ask: f64,
|
||||||
|
|
||||||
/// Tracks the amount of deposits that flowed into the serum open orders account.
|
/// An overestimate of the amount of tokens that might flow out of the open orders account.
|
||||||
///
|
///
|
||||||
/// The bank still considers these amounts user deposits (see deposits_in_serum)
|
/// The bank still considers these amounts user deposits (see Bank::potential_serum_tokens)
|
||||||
/// and they need to be deducted from there when they flow back into the bank
|
/// and that value needs to be updated in conjunction with these numbers.
|
||||||
/// as real tokens.
|
///
|
||||||
pub base_deposits_reserved: u64,
|
/// This estimation is based on the amount of tokens in the open orders account
|
||||||
pub quote_deposits_reserved: u64,
|
/// (see update_bank_potential_tokens() in serum3_place_order and settle)
|
||||||
|
pub potential_base_tokens: u64,
|
||||||
|
pub potential_quote_tokens: u64,
|
||||||
|
|
||||||
|
/// Track lowest bid/highest ask, same way as for highest bid/lowest ask.
|
||||||
|
///
|
||||||
|
/// 0 is a special "unset" state.
|
||||||
|
pub lowest_placed_bid_inv: f64,
|
||||||
|
pub highest_placed_ask: f64,
|
||||||
|
|
||||||
#[derivative(Debug = "ignore")]
|
#[derivative(Debug = "ignore")]
|
||||||
pub reserved: [u8; 32],
|
pub reserved: [u8; 16],
|
||||||
}
|
}
|
||||||
const_assert_eq!(
|
const_assert_eq!(
|
||||||
size_of::<Serum3Orders>(),
|
size_of::<Serum3Orders>(),
|
||||||
32 + 8 * 2 + 2 * 3 + 2 + 4 * 8 + 32
|
32 + 8 * 2 + 2 * 3 + 2 + 6 * 8 + 16
|
||||||
);
|
);
|
||||||
const_assert_eq!(size_of::<Serum3Orders>(), 120);
|
const_assert_eq!(size_of::<Serum3Orders>(), 120);
|
||||||
const_assert_eq!(size_of::<Serum3Orders>() % 8, 0);
|
const_assert_eq!(size_of::<Serum3Orders>() % 8, 0);
|
||||||
|
@ -185,9 +193,11 @@ impl Default for Serum3Orders {
|
||||||
quote_borrows_without_fee: 0,
|
quote_borrows_without_fee: 0,
|
||||||
highest_placed_bid_inv: 0.0,
|
highest_placed_bid_inv: 0.0,
|
||||||
lowest_placed_ask: 0.0,
|
lowest_placed_ask: 0.0,
|
||||||
base_deposits_reserved: 0,
|
potential_base_tokens: 0,
|
||||||
quote_deposits_reserved: 0,
|
potential_quote_tokens: 0,
|
||||||
reserved: [0; 32],
|
lowest_placed_bid_inv: 0.0,
|
||||||
|
highest_placed_ask: 0.0,
|
||||||
|
reserved: [0; 16],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
use crate::logs::{FilledPerpOrderLog, PerpTakerTradeLog};
|
use crate::error::*;
|
||||||
use crate::state::MangoAccountRefMut;
|
use crate::logs::{emit_stack, FilledPerpOrderLog, PerpTakerTradeLog};
|
||||||
use crate::{
|
use crate::state::{orderbook::bookside::*, EventQueue, MangoAccountRefMut, PerpMarket};
|
||||||
error::*,
|
|
||||||
state::{orderbook::bookside::*, EventQueue, PerpMarket},
|
|
||||||
};
|
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
use bytemuck::cast;
|
use bytemuck::cast;
|
||||||
use fixed::types::I80F48;
|
use fixed::types::I80F48;
|
||||||
|
@ -205,7 +202,7 @@ impl<'a> Orderbook<'a> {
|
||||||
event_queue.push_back(cast(fill)).unwrap();
|
event_queue.push_back(cast(fill)).unwrap();
|
||||||
limit -= 1;
|
limit -= 1;
|
||||||
|
|
||||||
emit!(FilledPerpOrderLog {
|
emit_stack(FilledPerpOrderLog {
|
||||||
mango_group: market.group.key(),
|
mango_group: market.group.key(),
|
||||||
perp_market_index: market.perp_market_index,
|
perp_market_index: market.perp_market_index,
|
||||||
seq_num,
|
seq_num,
|
||||||
|
@ -226,7 +223,7 @@ impl<'a> Orderbook<'a> {
|
||||||
mango_account,
|
mango_account,
|
||||||
total_quote_lots_taken - decremented_quote_lots,
|
total_quote_lots_taken - decremented_quote_lots,
|
||||||
)?;
|
)?;
|
||||||
emit!(PerpTakerTradeLog {
|
emit_stack(PerpTakerTradeLog {
|
||||||
mango_group: market.group.key(),
|
mango_group: market.group.key(),
|
||||||
mango_account: *mango_account_pk,
|
mango_account: *mango_account_pk,
|
||||||
perp_market_index: market.perp_market_index,
|
perp_market_index: market.perp_market_index,
|
||||||
|
|
|
@ -8,7 +8,7 @@ use static_assertions::const_assert_eq;
|
||||||
|
|
||||||
use crate::accounts_zerocopy::KeyedAccountReader;
|
use crate::accounts_zerocopy::KeyedAccountReader;
|
||||||
use crate::error::MangoError;
|
use crate::error::MangoError;
|
||||||
use crate::logs::PerpUpdateFundingLogV2;
|
use crate::logs::{emit_stack, PerpUpdateFundingLogV2};
|
||||||
use crate::state::orderbook::Side;
|
use crate::state::orderbook::Side;
|
||||||
use crate::state::{oracle, TokenIndex};
|
use crate::state::{oracle, TokenIndex};
|
||||||
use crate::util;
|
use crate::util;
|
||||||
|
@ -343,7 +343,7 @@ impl PerpMarket {
|
||||||
self.stable_price_model
|
self.stable_price_model
|
||||||
.update(now_ts, oracle_price.to_num());
|
.update(now_ts, oracle_price.to_num());
|
||||||
|
|
||||||
emit!(PerpUpdateFundingLogV2 {
|
emit_stack(PerpUpdateFundingLogV2 {
|
||||||
mango_group: self.group,
|
mango_group: self.group,
|
||||||
market_index: self.perp_market_index,
|
market_index: self.perp_market_index,
|
||||||
long_funding: self.long_funding.to_bits(),
|
long_funding: self.long_funding.to_bits(),
|
||||||
|
|
|
@ -26,7 +26,13 @@ pub struct Serum3Market {
|
||||||
|
|
||||||
pub bump: u8,
|
pub bump: u8,
|
||||||
|
|
||||||
pub padding2: [u8; 5],
|
pub padding2: [u8; 1],
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
pub oracle_price_band: f32,
|
||||||
|
|
||||||
pub registration_time: u64,
|
pub registration_time: u64,
|
||||||
|
|
||||||
|
@ -34,7 +40,7 @@ pub struct Serum3Market {
|
||||||
}
|
}
|
||||||
const_assert_eq!(
|
const_assert_eq!(
|
||||||
size_of::<Serum3Market>(),
|
size_of::<Serum3Market>(),
|
||||||
32 + 2 + 2 + 1 + 3 + 16 + 2 * 32 + 2 + 1 + 5 + 8 + 128
|
32 + 2 + 2 + 1 + 3 + 16 + 2 * 32 + 2 + 1 + 1 + 4 + 8 + 128
|
||||||
);
|
);
|
||||||
const_assert_eq!(size_of::<Serum3Market>(), 264);
|
const_assert_eq!(size_of::<Serum3Market>(), 264);
|
||||||
const_assert_eq!(size_of::<Serum3Market>() % 8, 0);
|
const_assert_eq!(size_of::<Serum3Market>() % 8, 0);
|
||||||
|
@ -53,6 +59,14 @@ impl Serum3Market {
|
||||||
pub fn is_force_close(&self) -> bool {
|
pub fn is_force_close(&self) -> bool {
|
||||||
self.force_close == 1
|
self.force_close == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn oracle_price_band(&self) -> f32 {
|
||||||
|
if self.oracle_price_band == 0.0 {
|
||||||
|
f32::MAX // default disabled
|
||||||
|
} else {
|
||||||
|
self.oracle_price_band
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[account(zero_copy)]
|
#[account(zero_copy)]
|
||||||
|
|
|
@ -414,3 +414,270 @@ async fn test_account_size_migration() -> Result<(), TransportError> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_bank_maint_weight_shift() -> 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 mints = &context.mints[0..1];
|
||||||
|
|
||||||
|
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||||
|
admin,
|
||||||
|
payer,
|
||||||
|
mints: mints.to_vec(),
|
||||||
|
zero_token_is_quote: true,
|
||||||
|
..mango_setup::GroupWithTokensConfig::default()
|
||||||
|
}
|
||||||
|
.create(solana)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let funding_amount = 1000;
|
||||||
|
let account = create_funded_account(
|
||||||
|
&solana,
|
||||||
|
group,
|
||||||
|
owner,
|
||||||
|
1,
|
||||||
|
&context.users[1],
|
||||||
|
mints,
|
||||||
|
funding_amount,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let maint_health = account_maint_health(solana, account).await;
|
||||||
|
assert!(assert_equal_f64_f64(maint_health, 1000.0, 1e-2));
|
||||||
|
|
||||||
|
let start_time = solana.clock_timestamp().await;
|
||||||
|
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
TokenEdit {
|
||||||
|
group,
|
||||||
|
admin,
|
||||||
|
mint: mints[0].pubkey,
|
||||||
|
options: mango_v4::instruction::TokenEdit {
|
||||||
|
maint_weight_shift_start_opt: Some(start_time + 1000),
|
||||||
|
maint_weight_shift_end_opt: Some(start_time + 2000),
|
||||||
|
maint_weight_shift_asset_target_opt: Some(0.5),
|
||||||
|
maint_weight_shift_liab_target_opt: Some(1.5),
|
||||||
|
..token_edit_instruction_default()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let maint_health = account_maint_health(solana, account).await;
|
||||||
|
assert!(assert_equal_f64_f64(maint_health, 1000.0, 1e-2));
|
||||||
|
|
||||||
|
solana.set_clock_timestamp(start_time + 1500).await;
|
||||||
|
|
||||||
|
let maint_health = account_maint_health(solana, account).await;
|
||||||
|
assert!(assert_equal_f64_f64(maint_health, 750.0, 1e-2));
|
||||||
|
|
||||||
|
solana.set_clock_timestamp(start_time + 3000).await;
|
||||||
|
|
||||||
|
let maint_health = account_maint_health(solana, account).await;
|
||||||
|
assert!(assert_equal_f64_f64(maint_health, 500.0, 1e-2));
|
||||||
|
|
||||||
|
solana.set_clock_timestamp(start_time + 1600).await;
|
||||||
|
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
TokenEdit {
|
||||||
|
group,
|
||||||
|
admin,
|
||||||
|
mint: mints[0].pubkey,
|
||||||
|
options: mango_v4::instruction::TokenEdit {
|
||||||
|
maint_weight_shift_abort: true,
|
||||||
|
..token_edit_instruction_default()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let maint_health = account_maint_health(solana, account).await;
|
||||||
|
assert!(assert_equal_f64_f64(maint_health, 700.0, 1e-2));
|
||||||
|
|
||||||
|
let bank: Bank = solana.get_account(tokens[0].bank).await;
|
||||||
|
assert!(assert_equal_fixed_f64(bank.maint_asset_weight, 0.7, 1e-4));
|
||||||
|
assert!(assert_equal_fixed_f64(bank.maint_liab_weight, 1.3, 1e-4));
|
||||||
|
assert_eq!(bank.maint_weight_shift_duration_inv, I80F48::ZERO);
|
||||||
|
|
||||||
|
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,
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
|
@ -73,6 +73,75 @@ async fn test_health_compute_tokens() -> Result<(), TransportError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_health_compute_tokens_during_maint_weight_shift() -> 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 mints = &context.mints[0..8];
|
||||||
|
|
||||||
|
//
|
||||||
|
// SETUP: Create a group and an account
|
||||||
|
//
|
||||||
|
|
||||||
|
let GroupWithTokens { group, .. } = GroupWithTokensConfig {
|
||||||
|
admin,
|
||||||
|
payer,
|
||||||
|
mints: mints.to_vec(),
|
||||||
|
..GroupWithTokensConfig::default()
|
||||||
|
}
|
||||||
|
.create(solana)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let account =
|
||||||
|
create_funded_account(&solana, group, owner, 0, &context.users[1], &[], 1000, 0).await;
|
||||||
|
|
||||||
|
let now = solana.clock_timestamp().await;
|
||||||
|
for mint in mints {
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
TokenEdit {
|
||||||
|
group,
|
||||||
|
admin,
|
||||||
|
mint: mint.pubkey,
|
||||||
|
options: mango_v4::instruction::TokenEdit {
|
||||||
|
maint_weight_shift_start_opt: Some(now - 1000),
|
||||||
|
maint_weight_shift_end_opt: Some(now + 1000),
|
||||||
|
maint_weight_shift_asset_target_opt: Some(0.1),
|
||||||
|
maint_weight_shift_liab_target_opt: Some(1.1),
|
||||||
|
..token_edit_instruction_default()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cu_measurements = vec![];
|
||||||
|
for token_account in &context.users[0].token_accounts[..mints.len()] {
|
||||||
|
cu_measurements.push(deposit_cu_datapoint(solana, account, owner, *token_account).await);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, pair) in cu_measurements.windows(2).enumerate() {
|
||||||
|
println!(
|
||||||
|
"after adding token {}: {} (+{})",
|
||||||
|
i,
|
||||||
|
pair[1],
|
||||||
|
pair[1] - pair[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let avg_cu_increase = cu_measurements.windows(2).map(|p| p[1] - p[0]).sum::<u64>()
|
||||||
|
/ (cu_measurements.len() - 1) as u64;
|
||||||
|
println!("average cu increase: {avg_cu_increase}");
|
||||||
|
assert!(avg_cu_increase < 4200);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// Try to reach compute limits in health checks by having many serum markets in an account
|
// Try to reach compute limits in health checks by having many serum markets in an account
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_health_compute_serum() -> Result<(), TransportError> {
|
async fn test_health_compute_serum() -> Result<(), TransportError> {
|
||||||
|
|
|
@ -613,3 +613,117 @@ 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,
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
|
@ -309,13 +309,13 @@ async fn test_serum_basics() -> Result<(), TransportError> {
|
||||||
//
|
//
|
||||||
// TEST: Place an order
|
// TEST: Place an order
|
||||||
//
|
//
|
||||||
let (order_id, _) = order_placer.bid_maker(1.0, 100).await.unwrap();
|
let (order_id, _) = order_placer.bid_maker(0.9, 100).await.unwrap();
|
||||||
check_prev_instruction_post_health(&solana, account).await;
|
check_prev_instruction_post_health(&solana, account).await;
|
||||||
|
|
||||||
let native0 = account_position(solana, account, base_token.bank).await;
|
let native0 = account_position(solana, account, base_token.bank).await;
|
||||||
let native1 = account_position(solana, account, quote_token.bank).await;
|
let native1 = account_position(solana, account, quote_token.bank).await;
|
||||||
assert_eq!(native0, 1000);
|
assert_eq!(native0, 1000);
|
||||||
assert_eq!(native1, 900);
|
assert_eq!(native1, 910);
|
||||||
|
|
||||||
let account_data = get_mango_account(solana, account).await;
|
let account_data = get_mango_account(solana, account).await;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -342,13 +342,13 @@ async fn test_serum_basics() -> Result<(), TransportError> {
|
||||||
let serum_orders = account_data.serum3_orders_by_raw_index(0).unwrap();
|
let serum_orders = account_data.serum3_orders_by_raw_index(0).unwrap();
|
||||||
assert_eq!(serum_orders.base_borrows_without_fee, 0);
|
assert_eq!(serum_orders.base_borrows_without_fee, 0);
|
||||||
assert_eq!(serum_orders.quote_borrows_without_fee, 0);
|
assert_eq!(serum_orders.quote_borrows_without_fee, 0);
|
||||||
assert_eq!(serum_orders.base_deposits_reserved, 0);
|
assert_eq!(serum_orders.potential_base_tokens, 100);
|
||||||
assert_eq!(serum_orders.quote_deposits_reserved, 100);
|
assert_eq!(serum_orders.potential_quote_tokens, 90);
|
||||||
|
|
||||||
let base_bank = solana.get_account::<Bank>(base_token.bank).await;
|
let base_bank = solana.get_account::<Bank>(base_token.bank).await;
|
||||||
assert_eq!(base_bank.deposits_in_serum, 0);
|
assert_eq!(base_bank.potential_serum_tokens, 100);
|
||||||
let quote_bank = solana.get_account::<Bank>(quote_token.bank).await;
|
let quote_bank = solana.get_account::<Bank>(quote_token.bank).await;
|
||||||
assert_eq!(quote_bank.deposits_in_serum, 100);
|
assert_eq!(quote_bank.potential_serum_tokens, 90);
|
||||||
|
|
||||||
assert!(order_id != 0);
|
assert!(order_id != 0);
|
||||||
|
|
||||||
|
@ -371,13 +371,13 @@ async fn test_serum_basics() -> Result<(), TransportError> {
|
||||||
let serum_orders = account_data.serum3_orders_by_raw_index(0).unwrap();
|
let serum_orders = account_data.serum3_orders_by_raw_index(0).unwrap();
|
||||||
assert_eq!(serum_orders.base_borrows_without_fee, 0);
|
assert_eq!(serum_orders.base_borrows_without_fee, 0);
|
||||||
assert_eq!(serum_orders.quote_borrows_without_fee, 0);
|
assert_eq!(serum_orders.quote_borrows_without_fee, 0);
|
||||||
assert_eq!(serum_orders.base_deposits_reserved, 0);
|
assert_eq!(serum_orders.potential_base_tokens, 0);
|
||||||
assert_eq!(serum_orders.quote_deposits_reserved, 0);
|
assert_eq!(serum_orders.potential_quote_tokens, 0);
|
||||||
|
|
||||||
let base_bank = solana.get_account::<Bank>(base_token.bank).await;
|
let base_bank = solana.get_account::<Bank>(base_token.bank).await;
|
||||||
assert_eq!(base_bank.deposits_in_serum, 0);
|
assert_eq!(base_bank.potential_serum_tokens, 0);
|
||||||
let quote_bank = solana.get_account::<Bank>(quote_token.bank).await;
|
let quote_bank = solana.get_account::<Bank>(quote_token.bank).await;
|
||||||
assert_eq!(quote_bank.deposits_in_serum, 0);
|
assert_eq!(quote_bank.potential_serum_tokens, 0);
|
||||||
|
|
||||||
// Process events such that the OutEvent deactivates the closed order on open_orders
|
// Process events such that the OutEvent deactivates the closed order on open_orders
|
||||||
context
|
context
|
||||||
|
@ -1150,89 +1150,72 @@ async fn test_serum_track_bid_ask() -> Result<(), TransportError> {
|
||||||
// TEST: highest bid/lowest ask updating
|
// TEST: highest bid/lowest ask updating
|
||||||
//
|
//
|
||||||
|
|
||||||
assert_eq!(
|
let srm = order_placer.mango_serum_orders().await;
|
||||||
order_placer
|
assert_eq!(srm.highest_placed_bid_inv, 0.0);
|
||||||
.mango_serum_orders()
|
assert_eq!(srm.lowest_placed_bid_inv, 0.0);
|
||||||
.await
|
assert_eq!(srm.highest_placed_ask, 0.0);
|
||||||
.highest_placed_bid_inv,
|
assert_eq!(srm.lowest_placed_ask, 0.0);
|
||||||
0.0
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
order_placer.mango_serum_orders().await.lowest_placed_ask,
|
|
||||||
0.0
|
|
||||||
);
|
|
||||||
|
|
||||||
order_placer.bid_maker(10.0, 100).await.unwrap();
|
order_placer.bid_maker(10.0, 100).await.unwrap();
|
||||||
assert_eq!(
|
|
||||||
order_placer
|
let srm = order_placer.mango_serum_orders().await;
|
||||||
.mango_serum_orders()
|
assert_eq!(srm.highest_placed_bid_inv, 1.0 / 10.0);
|
||||||
.await
|
assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 10.0);
|
||||||
.highest_placed_bid_inv,
|
assert_eq!(srm.highest_placed_ask, 0.0);
|
||||||
1.0 / 10.0
|
assert_eq!(srm.lowest_placed_ask, 0.0);
|
||||||
);
|
|
||||||
|
|
||||||
order_placer.bid_maker(9.0, 100).await.unwrap();
|
order_placer.bid_maker(9.0, 100).await.unwrap();
|
||||||
assert_eq!(
|
|
||||||
order_placer
|
let srm = order_placer.mango_serum_orders().await;
|
||||||
.mango_serum_orders()
|
assert_eq!(srm.highest_placed_bid_inv, 1.0 / 10.0);
|
||||||
.await
|
assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 9.0);
|
||||||
.highest_placed_bid_inv,
|
assert_eq!(srm.highest_placed_ask, 0.0);
|
||||||
1.0 / 10.0
|
assert_eq!(srm.lowest_placed_ask, 0.0);
|
||||||
);
|
|
||||||
|
|
||||||
order_placer.bid_maker(11.0, 100).await.unwrap();
|
order_placer.bid_maker(11.0, 100).await.unwrap();
|
||||||
assert_eq!(
|
|
||||||
order_placer
|
|
||||||
.mango_serum_orders()
|
|
||||||
.await
|
|
||||||
.highest_placed_bid_inv,
|
|
||||||
1.0 / 11.0
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
let srm = order_placer.mango_serum_orders().await;
|
||||||
order_placer.mango_serum_orders().await.lowest_placed_ask,
|
assert_eq!(srm.highest_placed_bid_inv, 1.0 / 11.0);
|
||||||
0.0
|
assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 9.0);
|
||||||
);
|
assert_eq!(srm.highest_placed_ask, 0.0);
|
||||||
|
assert_eq!(srm.lowest_placed_ask, 0.0);
|
||||||
|
|
||||||
order_placer.ask(20.0, 100).await.unwrap();
|
order_placer.ask(20.0, 100).await.unwrap();
|
||||||
assert_eq!(
|
|
||||||
order_placer.mango_serum_orders().await.lowest_placed_ask,
|
|
||||||
20.0
|
|
||||||
);
|
|
||||||
order_placer.ask(19.0, 100).await.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
order_placer.mango_serum_orders().await.lowest_placed_ask,
|
|
||||||
19.0
|
|
||||||
);
|
|
||||||
order_placer.ask(21.0, 100).await.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
order_placer.mango_serum_orders().await.lowest_placed_ask,
|
|
||||||
19.0
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
let srm = order_placer.mango_serum_orders().await;
|
||||||
order_placer
|
assert_eq!(srm.highest_placed_bid_inv, 1.0 / 11.0);
|
||||||
.mango_serum_orders()
|
assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 9.0);
|
||||||
.await
|
assert_eq!(srm.highest_placed_ask, 20.0);
|
||||||
.highest_placed_bid_inv,
|
assert_eq!(srm.lowest_placed_ask, 20.0);
|
||||||
1.0 / 11.0
|
|
||||||
);
|
order_placer.ask(19.0, 100).await.unwrap();
|
||||||
|
|
||||||
|
let srm = order_placer.mango_serum_orders().await;
|
||||||
|
assert_eq!(srm.highest_placed_bid_inv, 1.0 / 11.0);
|
||||||
|
assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 9.0);
|
||||||
|
assert_eq!(srm.highest_placed_ask, 20.0);
|
||||||
|
assert_eq!(srm.lowest_placed_ask, 19.0);
|
||||||
|
|
||||||
|
order_placer.ask(21.0, 100).await.unwrap();
|
||||||
|
|
||||||
|
let srm = order_placer.mango_serum_orders().await;
|
||||||
|
assert_eq!(srm.highest_placed_bid_inv, 1.0 / 11.0);
|
||||||
|
assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 9.0);
|
||||||
|
assert_eq!(srm.highest_placed_ask, 21.0);
|
||||||
|
assert_eq!(srm.lowest_placed_ask, 19.0);
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: cancellation allows for resets
|
// TEST: cancellation allows for resets
|
||||||
//
|
//
|
||||||
|
|
||||||
order_placer.cancel_all().await;
|
order_placer.cancel_all().await;
|
||||||
assert_eq!(
|
|
||||||
order_placer.mango_serum_orders().await.lowest_placed_ask,
|
// no immediate change
|
||||||
19.0
|
let srm = order_placer.mango_serum_orders().await;
|
||||||
);
|
assert_eq!(srm.highest_placed_bid_inv, 1.0 / 11.0);
|
||||||
assert_eq!(
|
assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 9.0);
|
||||||
order_placer
|
assert_eq!(srm.highest_placed_ask, 21.0);
|
||||||
.mango_serum_orders()
|
assert_eq!(srm.lowest_placed_ask, 19.0);
|
||||||
.await
|
|
||||||
.highest_placed_bid_inv,
|
|
||||||
1.0 / 11.0
|
|
||||||
);
|
|
||||||
|
|
||||||
// Process events such that the OutEvent deactivates the closed order on open_orders
|
// Process events such that the OutEvent deactivates the closed order on open_orders
|
||||||
context
|
context
|
||||||
|
@ -1242,36 +1225,36 @@ async fn test_serum_track_bid_ask() -> Result<(), TransportError> {
|
||||||
|
|
||||||
// takes new value for bid, resets ask
|
// takes new value for bid, resets ask
|
||||||
order_placer.bid_maker(1.0, 100).await.unwrap();
|
order_placer.bid_maker(1.0, 100).await.unwrap();
|
||||||
assert_eq!(
|
|
||||||
order_placer.mango_serum_orders().await.lowest_placed_ask,
|
let srm = order_placer.mango_serum_orders().await;
|
||||||
0.0
|
assert_eq!(srm.highest_placed_bid_inv, 1.0);
|
||||||
);
|
assert_eq!(srm.lowest_placed_bid_inv, 1.0);
|
||||||
assert_eq!(
|
assert_eq!(srm.highest_placed_ask, 0.0);
|
||||||
order_placer
|
assert_eq!(srm.lowest_placed_ask, 0.0);
|
||||||
.mango_serum_orders()
|
|
||||||
.await
|
|
||||||
.highest_placed_bid_inv,
|
|
||||||
1.0
|
|
||||||
);
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: can reset even when there's still an order on the other side
|
// TEST: can reset even when there's still an order on the other side
|
||||||
//
|
//
|
||||||
let (oid, _) = order_placer.ask(10.0, 100).await.unwrap();
|
let (oid, _) = order_placer.ask(10.0, 100).await.unwrap();
|
||||||
assert_eq!(
|
|
||||||
order_placer.mango_serum_orders().await.lowest_placed_ask,
|
let srm = order_placer.mango_serum_orders().await;
|
||||||
10.0
|
assert_eq!(srm.highest_placed_bid_inv, 1.0);
|
||||||
);
|
assert_eq!(srm.lowest_placed_bid_inv, 1.0);
|
||||||
|
assert_eq!(srm.highest_placed_ask, 10.0);
|
||||||
|
assert_eq!(srm.lowest_placed_ask, 10.0);
|
||||||
|
|
||||||
order_placer.cancel(oid).await;
|
order_placer.cancel(oid).await;
|
||||||
context
|
context
|
||||||
.serum
|
.serum
|
||||||
.consume_spot_events(&serum_market_cookie, &[order_placer.open_orders])
|
.consume_spot_events(&serum_market_cookie, &[order_placer.open_orders])
|
||||||
.await;
|
.await;
|
||||||
order_placer.ask(9.0, 100).await.unwrap();
|
order_placer.ask(9.0, 100).await.unwrap();
|
||||||
assert_eq!(
|
|
||||||
order_placer.mango_serum_orders().await.lowest_placed_ask,
|
let srm = order_placer.mango_serum_orders().await;
|
||||||
9.0
|
assert_eq!(srm.highest_placed_bid_inv, 1.0);
|
||||||
);
|
assert_eq!(srm.lowest_placed_bid_inv, 1.0);
|
||||||
|
assert_eq!(srm.highest_placed_ask, 9.0);
|
||||||
|
assert_eq!(srm.lowest_placed_ask, 9.0);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1305,10 +1288,10 @@ async fn test_serum_track_reserved_deposits() -> Result<(), TransportError> {
|
||||||
let base_bank = solana.get_account::<Bank>(base_bank).await;
|
let base_bank = solana.get_account::<Bank>(base_bank).await;
|
||||||
let quote_bank = solana.get_account::<Bank>(quote_bank).await;
|
let quote_bank = solana.get_account::<Bank>(quote_bank).await;
|
||||||
(
|
(
|
||||||
orders.base_deposits_reserved,
|
orders.potential_base_tokens,
|
||||||
base_bank.deposits_in_serum,
|
base_bank.potential_serum_tokens,
|
||||||
orders.quote_deposits_reserved,
|
orders.potential_quote_tokens,
|
||||||
quote_bank.deposits_in_serum,
|
quote_bank.potential_serum_tokens,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1317,9 +1300,14 @@ async fn test_serum_track_reserved_deposits() -> Result<(), TransportError> {
|
||||||
//
|
//
|
||||||
|
|
||||||
order_placer.bid_maker(0.8, 2000).await.unwrap();
|
order_placer.bid_maker(0.8, 2000).await.unwrap();
|
||||||
order_placer.ask(1.2, 2000).await.unwrap();
|
|
||||||
assert_eq!(get_vals(solana).await, (2000, 2000, 1600, 1600));
|
assert_eq!(get_vals(solana).await, (2000, 2000, 1600, 1600));
|
||||||
|
|
||||||
|
order_placer.ask(1.2, 2000).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
get_vals(solana).await,
|
||||||
|
(2 * 2000, 2 * 2000, 1600 + 2400, 1600 + 2400)
|
||||||
|
);
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: match partially on both sides, increasing the on-bank reserved amounts
|
// TEST: match partially on both sides, increasing the on-bank reserved amounts
|
||||||
// because order_placer2 puts funds into the serum oo
|
// because order_placer2 puts funds into the serum oo
|
||||||
|
@ -1333,9 +1321,12 @@ async fn test_serum_track_reserved_deposits() -> Result<(), TransportError> {
|
||||||
&[order_placer.open_orders, order_placer2.open_orders],
|
&[order_placer.open_orders, order_placer2.open_orders],
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert_eq!(get_vals(solana).await, (2000, 2000, 1600, 2801));
|
// taker order directly converted to base, no change to quote
|
||||||
|
assert_eq!(get_vals(solana).await, (4000, 4000 + 1000, 4000, 4000));
|
||||||
|
|
||||||
|
// takes out 1000 base
|
||||||
order_placer2.settle_v2(false).await;
|
order_placer2.settle_v2(false).await;
|
||||||
assert_eq!(get_vals(solana).await, (2000, 2000, 1600, 1600));
|
assert_eq!(get_vals(solana).await, (4000, 4000, 4000, 4000));
|
||||||
|
|
||||||
order_placer2.ask(0.8, 1000).await.unwrap();
|
order_placer2.ask(0.8, 1000).await.unwrap();
|
||||||
context
|
context
|
||||||
|
@ -1345,16 +1336,19 @@ async fn test_serum_track_reserved_deposits() -> Result<(), TransportError> {
|
||||||
&[order_placer.open_orders, order_placer2.open_orders],
|
&[order_placer.open_orders, order_placer2.open_orders],
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert_eq!(get_vals(solana).await, (2000, 3000, 1600, 1600));
|
// taker order directly converted to quote
|
||||||
|
assert_eq!(get_vals(solana).await, (4000, 4000, 4000, 4000 + 799));
|
||||||
|
|
||||||
order_placer2.settle_v2(false).await;
|
order_placer2.settle_v2(false).await;
|
||||||
assert_eq!(get_vals(solana).await, (2000, 2000, 1600, 1600));
|
assert_eq!(get_vals(solana).await, (4000, 4000, 4000, 4000));
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: Settlement updates the values
|
// TEST: Settlement updates the values
|
||||||
//
|
//
|
||||||
|
|
||||||
order_placer.settle_v2(false).await;
|
order_placer.settle_v2(false).await;
|
||||||
assert_eq!(get_vals(solana).await, (1000, 1000, 800, 800));
|
// remaining is bid 1000 @ 0.8; ask 1000 @ 1.2
|
||||||
|
assert_eq!(get_vals(solana).await, (2000, 2000, 2000, 2000));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1481,6 +1475,239 @@ async fn test_serum_compute() -> Result<(), TransportError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_serum_bands() -> 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 = 10000;
|
||||||
|
let CommonSetup {
|
||||||
|
group_with_tokens,
|
||||||
|
mut order_placer,
|
||||||
|
quote_token,
|
||||||
|
base_token,
|
||||||
|
..
|
||||||
|
} = common_setup(&context, deposit_amount).await;
|
||||||
|
|
||||||
|
//
|
||||||
|
// SETUP: Set oracle price for market to 100
|
||||||
|
//
|
||||||
|
set_bank_stub_oracle_price(
|
||||||
|
solana,
|
||||||
|
group_with_tokens.group,
|
||||||
|
&base_token,
|
||||||
|
group_with_tokens.admin,
|
||||||
|
200.0,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
set_bank_stub_oracle_price(
|
||||||
|
solana,
|
||||||
|
group_with_tokens.group,
|
||||||
|
"e_token,
|
||||||
|
group_with_tokens.admin,
|
||||||
|
2.0,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
//
|
||||||
|
// TEST: can place way over/under oracle
|
||||||
|
//
|
||||||
|
|
||||||
|
order_placer.bid_maker(1.0, 100).await.unwrap();
|
||||||
|
order_placer.ask(200.0, 100).await.unwrap();
|
||||||
|
order_placer.cancel_all().await;
|
||||||
|
|
||||||
|
//
|
||||||
|
// TEST: Can't when bands are enabled
|
||||||
|
//
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
Serum3EditMarketInstruction {
|
||||||
|
group: group_with_tokens.group,
|
||||||
|
admin: group_with_tokens.admin,
|
||||||
|
market: order_placer.serum_market,
|
||||||
|
options: mango_v4::instruction::Serum3EditMarket {
|
||||||
|
oracle_price_band_opt: Some(0.5),
|
||||||
|
..serum3_edit_market_instruction_default()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let r = order_placer.try_bid(65.0, 100, false).await;
|
||||||
|
assert!(r.is_err());
|
||||||
|
let r = order_placer.try_ask(151.0, 100).await;
|
||||||
|
assert!(r.is_err());
|
||||||
|
|
||||||
|
order_placer.try_bid(67.0, 100, false).await.unwrap();
|
||||||
|
order_placer.try_ask(149.0, 100).await.unwrap();
|
||||||
|
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
@ -1491,6 +1718,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;
|
||||||
|
@ -1565,6 +1800,7 @@ 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
|
||||||
|
if vault_funding > 0 {
|
||||||
create_funded_account(
|
create_funded_account(
|
||||||
&solana,
|
&solana,
|
||||||
group,
|
group,
|
||||||
|
@ -1576,6 +1812,7 @@ async fn common_setup(context: &TestContext, deposit_amount: u64) -> CommonSetup
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
let open_orders = send_tx(
|
let open_orders = send_tx(
|
||||||
solana,
|
solana,
|
||||||
|
|
|
@ -1013,3 +1013,145 @@ 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,
|
||||||
|
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,
|
||||||
|
@ -386,6 +429,17 @@ pub async fn account_init_health(solana: &SolanaCookie, account: Pubkey) -> f64
|
||||||
health_data.init_health.to_num::<f64>()
|
health_data.init_health.to_num::<f64>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn account_maint_health(solana: &SolanaCookie, account: Pubkey) -> f64 {
|
||||||
|
send_tx(solana, ComputeAccountDataInstruction { account })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let health_data = solana
|
||||||
|
.program_log_events::<mango_v4::events::MangoAccountData>()
|
||||||
|
.pop()
|
||||||
|
.unwrap();
|
||||||
|
health_data.maint_health.to_num::<f64>()
|
||||||
|
}
|
||||||
|
|
||||||
// Verifies that the "post_health: ..." log emitted by the previous instruction
|
// Verifies that the "post_health: ..." log emitted by the previous instruction
|
||||||
// matches the init health of the account.
|
// matches the init health of the account.
|
||||||
pub async fn check_prev_instruction_post_health(solana: &SolanaCookie, account: Pubkey) {
|
pub async fn check_prev_instruction_post_health(solana: &SolanaCookie, account: Pubkey) {
|
||||||
|
@ -997,6 +1051,10 @@ impl ClientInstruction for TokenRegisterInstruction {
|
||||||
token_conditional_swap_taker_fee_rate: 0.0,
|
token_conditional_swap_taker_fee_rate: 0.0,
|
||||||
token_conditional_swap_maker_fee_rate: 0.0,
|
token_conditional_swap_maker_fee_rate: 0.0,
|
||||||
flash_loan_swap_fee_rate: 0.0,
|
flash_loan_swap_fee_rate: 0.0,
|
||||||
|
interest_curve_scaling: 1.0,
|
||||||
|
interest_target_utilization: 0.5,
|
||||||
|
group_insurance_fund: true,
|
||||||
|
deposit_limit: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let bank = Pubkey::find_program_address(
|
let bank = Pubkey::find_program_address(
|
||||||
|
@ -1241,6 +1299,15 @@ pub fn token_edit_instruction_default() -> mango_v4::instruction::TokenEdit {
|
||||||
token_conditional_swap_taker_fee_rate_opt: None,
|
token_conditional_swap_taker_fee_rate_opt: None,
|
||||||
token_conditional_swap_maker_fee_rate_opt: None,
|
token_conditional_swap_maker_fee_rate_opt: None,
|
||||||
flash_loan_swap_fee_rate_opt: None,
|
flash_loan_swap_fee_rate_opt: None,
|
||||||
|
interest_curve_scaling_opt: None,
|
||||||
|
interest_target_utilization_opt: None,
|
||||||
|
maint_weight_shift_start_opt: None,
|
||||||
|
maint_weight_shift_end_opt: None,
|
||||||
|
maint_weight_shift_asset_target_opt: None,
|
||||||
|
maint_weight_shift_liab_target_opt: None,
|
||||||
|
maint_weight_shift_abort: false,
|
||||||
|
set_fallback_oracle: false,
|
||||||
|
deposit_limit_opt: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2254,6 +2321,7 @@ impl ClientInstruction for Serum3RegisterMarketInstruction {
|
||||||
let instruction = Self::Instruction {
|
let instruction = Self::Instruction {
|
||||||
market_index: self.market_index,
|
market_index: self.market_index,
|
||||||
name: "UUU/usdc".to_string(),
|
name: "UUU/usdc".to_string(),
|
||||||
|
oracle_price_band: f32::MAX,
|
||||||
};
|
};
|
||||||
|
|
||||||
let serum_market = Pubkey::find_program_address(
|
let serum_market = Pubkey::find_program_address(
|
||||||
|
@ -2298,6 +2366,46 @@ impl ClientInstruction for Serum3RegisterMarketInstruction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn serum3_edit_market_instruction_default() -> mango_v4::instruction::Serum3EditMarket {
|
||||||
|
mango_v4::instruction::Serum3EditMarket {
|
||||||
|
reduce_only_opt: None,
|
||||||
|
force_close_opt: None,
|
||||||
|
name_opt: None,
|
||||||
|
oracle_price_band_opt: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Serum3EditMarketInstruction {
|
||||||
|
pub group: Pubkey,
|
||||||
|
pub admin: TestKeypair,
|
||||||
|
pub market: Pubkey,
|
||||||
|
pub options: mango_v4::instruction::Serum3EditMarket,
|
||||||
|
}
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ClientInstruction for Serum3EditMarketInstruction {
|
||||||
|
type Accounts = mango_v4::accounts::Serum3EditMarket;
|
||||||
|
type Instruction = mango_v4::instruction::Serum3EditMarket;
|
||||||
|
async fn to_instruction(
|
||||||
|
&self,
|
||||||
|
_account_loader: impl ClientAccountLoader + 'async_trait,
|
||||||
|
) -> (Self::Accounts, instruction::Instruction) {
|
||||||
|
let program_id = mango_v4::id();
|
||||||
|
|
||||||
|
let accounts = Self::Accounts {
|
||||||
|
group: self.group,
|
||||||
|
admin: self.admin.pubkey(),
|
||||||
|
market: self.market,
|
||||||
|
};
|
||||||
|
|
||||||
|
let instruction = make_instruction(program_id, &accounts, &self.options);
|
||||||
|
(accounts, instruction)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signers(&self) -> Vec<TestKeypair> {
|
||||||
|
vec![self.admin]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Serum3DeregisterMarketInstruction {
|
pub struct Serum3DeregisterMarketInstruction {
|
||||||
pub group: Pubkey,
|
pub group: Pubkey,
|
||||||
pub admin: TestKeypair,
|
pub admin: TestKeypair,
|
||||||
|
@ -2472,7 +2580,7 @@ pub struct Serum3PlaceOrderInstruction {
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl ClientInstruction for Serum3PlaceOrderInstruction {
|
impl ClientInstruction for Serum3PlaceOrderInstruction {
|
||||||
type Accounts = mango_v4::accounts::Serum3PlaceOrder;
|
type Accounts = mango_v4::accounts::Serum3PlaceOrder;
|
||||||
type Instruction = mango_v4::instruction::Serum3PlaceOrder;
|
type Instruction = mango_v4::instruction::Serum3PlaceOrderV2;
|
||||||
async fn to_instruction(
|
async fn to_instruction(
|
||||||
&self,
|
&self,
|
||||||
account_loader: impl ClientAccountLoader + 'async_trait,
|
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||||
|
@ -2526,7 +2634,7 @@ impl ClientInstruction for Serum3PlaceOrderInstruction {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let health_check_metas = derive_health_check_remaining_account_metas(
|
let mut health_check_metas = derive_health_check_remaining_account_metas(
|
||||||
&account_loader,
|
&account_loader,
|
||||||
&account,
|
&account,
|
||||||
None,
|
None,
|
||||||
|
@ -2535,11 +2643,17 @@ impl ClientInstruction for Serum3PlaceOrderInstruction {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let payer_info = &match self.side {
|
let (payer_info, receiver_info) = &match self.side {
|
||||||
Serum3Side::Bid => "e_info,
|
Serum3Side::Bid => ("e_info, &base_info),
|
||||||
Serum3Side::Ask => &base_info,
|
Serum3Side::Ask => (&base_info, "e_info),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let receiver_active_index = account
|
||||||
|
.active_token_positions()
|
||||||
|
.position(|tp| tp.token_index == receiver_info.token_index)
|
||||||
|
.unwrap();
|
||||||
|
health_check_metas[receiver_active_index].is_writable = true;
|
||||||
|
|
||||||
let accounts = Self::Accounts {
|
let accounts = Self::Accounts {
|
||||||
group: account.fixed.group,
|
group: account.fixed.group,
|
||||||
account: self.account,
|
account: self.account,
|
||||||
|
|
|
@ -87,16 +87,13 @@ 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"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ export interface BankForHealth {
|
||||||
scaledInitLiabWeight(price: I80F48): I80F48;
|
scaledInitLiabWeight(price: I80F48): I80F48;
|
||||||
nativeDeposits(): I80F48;
|
nativeDeposits(): I80F48;
|
||||||
nativeBorrows(): I80F48;
|
nativeBorrows(): I80F48;
|
||||||
|
maintWeights(): [I80F48, I80F48];
|
||||||
|
|
||||||
depositWeightScaleStartQuote: number;
|
depositWeightScaleStartQuote: number;
|
||||||
borrowWeightScaleStartQuote: number;
|
borrowWeightScaleStartQuote: number;
|
||||||
|
@ -75,6 +76,9 @@ export class Bank implements BankForHealth {
|
||||||
public maintLiabWeight: I80F48;
|
public maintLiabWeight: I80F48;
|
||||||
public liquidationFee: I80F48;
|
public liquidationFee: I80F48;
|
||||||
public dust: I80F48;
|
public dust: I80F48;
|
||||||
|
public maintWeightShiftDurationInv: I80F48;
|
||||||
|
public maintWeightShiftAssetTarget: I80F48;
|
||||||
|
public maintWeightShiftLiabTarget: I80F48;
|
||||||
|
|
||||||
static from(
|
static from(
|
||||||
publicKey: PublicKey,
|
publicKey: PublicKey,
|
||||||
|
@ -128,7 +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;
|
||||||
|
maintWeightShiftEnd: BN;
|
||||||
|
maintWeightShiftDurationInv: I80F48Dto;
|
||||||
|
maintWeightShiftAssetTarget: I80F48Dto;
|
||||||
|
maintWeightShiftLiabTarget: I80F48Dto;
|
||||||
|
depositLimit: BN;
|
||||||
},
|
},
|
||||||
): Bank {
|
): Bank {
|
||||||
return new Bank(
|
return new Bank(
|
||||||
|
@ -182,7 +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.maintWeightShiftEnd,
|
||||||
|
obj.maintWeightShiftDurationInv,
|
||||||
|
obj.maintWeightShiftAssetTarget,
|
||||||
|
obj.maintWeightShiftLiabTarget,
|
||||||
|
obj.depositLimit,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,7 +252,14 @@ export class Bank implements BankForHealth {
|
||||||
public tokenConditionalSwapMakerFeeRate: number,
|
public tokenConditionalSwapMakerFeeRate: number,
|
||||||
public flashLoanSwapFeeRate: number,
|
public flashLoanSwapFeeRate: number,
|
||||||
public interestTargetUtilization: number,
|
public interestTargetUtilization: number,
|
||||||
public interestCurveScaling: number, // public depositsInSerum: BN,
|
public interestCurveScaling: number,
|
||||||
|
public potentialSerumTokens: BN,
|
||||||
|
public maintWeightShiftStart: BN,
|
||||||
|
public maintWeightShiftEnd: BN,
|
||||||
|
maintWeightShiftDurationInv: I80F48Dto,
|
||||||
|
maintWeightShiftAssetTarget: I80F48Dto,
|
||||||
|
maintWeightShiftLiabTarget: I80F48Dto,
|
||||||
|
public depositLimit: BN,
|
||||||
) {
|
) {
|
||||||
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
|
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
|
||||||
this.oracleConfig = {
|
this.oracleConfig = {
|
||||||
|
@ -263,6 +286,9 @@ export class Bank implements BankForHealth {
|
||||||
this.initLiabWeight = I80F48.from(initLiabWeight);
|
this.initLiabWeight = I80F48.from(initLiabWeight);
|
||||||
this.liquidationFee = I80F48.from(liquidationFee);
|
this.liquidationFee = I80F48.from(liquidationFee);
|
||||||
this.dust = I80F48.from(dust);
|
this.dust = I80F48.from(dust);
|
||||||
|
this.maintWeightShiftDurationInv = I80F48.from(maintWeightShiftDurationInv);
|
||||||
|
this.maintWeightShiftAssetTarget = I80F48.from(maintWeightShiftAssetTarget);
|
||||||
|
this.maintWeightShiftLiabTarget = I80F48.from(maintWeightShiftLiabTarget);
|
||||||
this._price = undefined;
|
this._price = undefined;
|
||||||
this._uiPrice = undefined;
|
this._uiPrice = undefined;
|
||||||
this._oracleLastUpdatedSlot = undefined;
|
this._oracleLastUpdatedSlot = undefined;
|
||||||
|
@ -384,6 +410,32 @@ export class Bank implements BankForHealth {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
maintWeights(): [I80F48, I80F48] {
|
||||||
|
const nowTs = new BN(Date.now() / 1000);
|
||||||
|
if (
|
||||||
|
this.maintWeightShiftDurationInv.isZero() ||
|
||||||
|
nowTs.lte(this.maintWeightShiftStart)
|
||||||
|
) {
|
||||||
|
return [this.maintAssetWeight, this.maintLiabWeight];
|
||||||
|
} else if (nowTs.gte(this.maintWeightShiftEnd)) {
|
||||||
|
return [
|
||||||
|
this.maintWeightShiftAssetTarget,
|
||||||
|
this.maintWeightShiftLiabTarget,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
const scale = I80F48.fromU64(nowTs.sub(this.maintWeightShiftStart)).mul(
|
||||||
|
this.maintWeightShiftDurationInv,
|
||||||
|
);
|
||||||
|
const asset = this.maintAssetWeight.add(
|
||||||
|
this.maintWeightShiftAssetTarget.sub(this.maintAssetWeight).mul(scale),
|
||||||
|
);
|
||||||
|
const liab = this.maintLiabWeight.add(
|
||||||
|
this.maintWeightShiftLiabTarget.sub(this.maintLiabWeight).mul(scale),
|
||||||
|
);
|
||||||
|
return [asset, liab];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getAssetPrice(): I80F48 {
|
getAssetPrice(): I80F48 {
|
||||||
return this.price.min(I80F48.fromNumber(this.stablePriceModel.stablePrice));
|
return this.price.min(I80F48.fromNumber(this.stablePriceModel.stablePrice));
|
||||||
}
|
}
|
||||||
|
@ -457,19 +509,22 @@ export class Bank implements BankForHealth {
|
||||||
}
|
}
|
||||||
|
|
||||||
const utilization = totalBorrows.div(totalDeposits);
|
const utilization = totalBorrows.div(totalDeposits);
|
||||||
|
const scaling = I80F48.fromNumber(
|
||||||
|
this.interestCurveScaling == 0.0 ? 1.0 : this.interestCurveScaling,
|
||||||
|
);
|
||||||
if (utilization.lt(this.util0)) {
|
if (utilization.lt(this.util0)) {
|
||||||
const slope = this.rate0.div(this.util0);
|
const slope = this.rate0.div(this.util0);
|
||||||
return slope.mul(utilization);
|
return slope.mul(utilization).mul(scaling);
|
||||||
} else if (utilization.lt(this.util1)) {
|
} else if (utilization.lt(this.util1)) {
|
||||||
const extraUtil = utilization.sub(this.util0);
|
const extraUtil = utilization.sub(this.util0);
|
||||||
const slope = this.rate1.sub(this.rate0).div(this.util1.sub(this.util0));
|
const slope = this.rate1.sub(this.rate0).div(this.util1.sub(this.util0));
|
||||||
return this.rate0.add(slope.mul(extraUtil));
|
return this.rate0.add(slope.mul(extraUtil)).mul(scaling);
|
||||||
} else {
|
} else {
|
||||||
const extraUtil = utilization.sub(this.util1);
|
const extraUtil = utilization.sub(this.util1);
|
||||||
const slope = this.maxRate
|
const slope = this.maxRate
|
||||||
.sub(this.rate1)
|
.sub(this.rate1)
|
||||||
.div(I80F48.fromNumber(1).sub(this.util1));
|
.div(I80F48.fromNumber(1).sub(this.util1));
|
||||||
return this.rate1.add(slope.mul(extraUtil));
|
return this.rate1.add(slope.mul(extraUtil)).mul(scaling);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,10 @@ function mockBankAndOracle(
|
||||||
},
|
},
|
||||||
nativeDeposits: () => I80F48.fromNumber(deposits),
|
nativeDeposits: () => I80F48.fromNumber(deposits),
|
||||||
nativeBorrows: () => I80F48.fromNumber(borrows),
|
nativeBorrows: () => I80F48.fromNumber(borrows),
|
||||||
|
maintWeights: () => [
|
||||||
|
I80F48.fromNumber(1 - maintWeight),
|
||||||
|
I80F48.fromNumber(1 + maintWeight),
|
||||||
|
],
|
||||||
borrowWeightScaleStartQuote: borrowWeightScaleStartQuote,
|
borrowWeightScaleStartQuote: borrowWeightScaleStartQuote,
|
||||||
depositWeightScaleStartQuote: depositWeightScaleStartQuote,
|
depositWeightScaleStartQuote: depositWeightScaleStartQuote,
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,13 +2,7 @@ import { BN } from '@coral-xyz/anchor';
|
||||||
import { OpenOrders } from '@project-serum/serum';
|
import { OpenOrders } from '@project-serum/serum';
|
||||||
import { PublicKey } from '@solana/web3.js';
|
import { PublicKey } from '@solana/web3.js';
|
||||||
import cloneDeep from 'lodash/cloneDeep';
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import {
|
import { I80F48, MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
|
||||||
I80F48,
|
|
||||||
I80F48Dto,
|
|
||||||
MAX_I80F48,
|
|
||||||
ONE_I80F48,
|
|
||||||
ZERO_I80F48,
|
|
||||||
} from '../numbers/I80F48';
|
|
||||||
import {
|
import {
|
||||||
toNativeI80F48ForQuote,
|
toNativeI80F48ForQuote,
|
||||||
toUiDecimals,
|
toUiDecimals,
|
||||||
|
@ -158,14 +152,6 @@ export class HealthCache {
|
||||||
return new HealthCache(tokenInfos, serum3Infos, perpInfos);
|
return new HealthCache(tokenInfos, serum3Infos, perpInfos);
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromDto(dto): HealthCache {
|
|
||||||
return new HealthCache(
|
|
||||||
dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)),
|
|
||||||
dto.serum3Infos.map((dto) => Serum3Info.fromDto(dto)),
|
|
||||||
dto.perpInfos.map((dto) => PerpInfo.fromDto(dto)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
computeSerum3Reservations(healthType: HealthType | undefined): {
|
computeSerum3Reservations(healthType: HealthType | undefined): {
|
||||||
tokenMaxReserved: TokenMaxReserved[];
|
tokenMaxReserved: TokenMaxReserved[];
|
||||||
serum3Reserved: Serum3Reserved[];
|
serum3Reserved: Serum3Reserved[];
|
||||||
|
@ -1437,23 +1423,6 @@ export class TokenInfo {
|
||||||
public balanceSpot: I80F48,
|
public balanceSpot: I80F48,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
static fromDto(dto: TokenInfoDto): TokenInfo {
|
|
||||||
return new TokenInfo(
|
|
||||||
dto.tokenIndex as TokenIndex,
|
|
||||||
I80F48.from(dto.maintAssetWeight),
|
|
||||||
I80F48.from(dto.initAssetWeight),
|
|
||||||
I80F48.from(dto.initScaledAssetWeight),
|
|
||||||
I80F48.from(dto.maintLiabWeight),
|
|
||||||
I80F48.from(dto.initLiabWeight),
|
|
||||||
I80F48.from(dto.initScaledLiabWeight),
|
|
||||||
new Prices(
|
|
||||||
I80F48.from(dto.prices.oracle),
|
|
||||||
I80F48.from(dto.prices.stable),
|
|
||||||
),
|
|
||||||
I80F48.from(dto.balanceSpot),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromBank(bank: BankForHealth, nativeBalance?: I80F48): TokenInfo {
|
static fromBank(bank: BankForHealth, nativeBalance?: I80F48): TokenInfo {
|
||||||
const p = new Prices(
|
const p = new Prices(
|
||||||
bank.price,
|
bank.price,
|
||||||
|
@ -1462,12 +1431,15 @@ export class TokenInfo {
|
||||||
// Use the liab price for computing weight scaling, because it's pessimistic and
|
// Use the liab price for computing weight scaling, because it's pessimistic and
|
||||||
// causes the most unfavorable scaling.
|
// causes the most unfavorable scaling.
|
||||||
const liabPrice = p.liab(HealthType.init);
|
const liabPrice = p.liab(HealthType.init);
|
||||||
|
|
||||||
|
const [maintAssetWeight, maintLiabWeight] = bank.maintWeights();
|
||||||
|
|
||||||
return new TokenInfo(
|
return new TokenInfo(
|
||||||
bank.tokenIndex,
|
bank.tokenIndex,
|
||||||
bank.maintAssetWeight,
|
maintAssetWeight,
|
||||||
bank.initAssetWeight,
|
bank.initAssetWeight,
|
||||||
bank.scaledInitAssetWeight(liabPrice),
|
bank.scaledInitAssetWeight(liabPrice),
|
||||||
bank.maintLiabWeight,
|
maintLiabWeight,
|
||||||
bank.initLiabWeight,
|
bank.initLiabWeight,
|
||||||
bank.scaledInitLiabWeight(liabPrice),
|
bank.scaledInitLiabWeight(liabPrice),
|
||||||
p,
|
p,
|
||||||
|
@ -1564,18 +1536,6 @@ export class Serum3Info {
|
||||||
public marketIndex: MarketIndex,
|
public marketIndex: MarketIndex,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
static fromDto(dto: Serum3InfoDto): Serum3Info {
|
|
||||||
return new Serum3Info(
|
|
||||||
I80F48.from(dto.reservedBase),
|
|
||||||
I80F48.from(dto.reservedQuote),
|
|
||||||
I80F48.from(dto.reservedBaseAsQuoteLowestAsk),
|
|
||||||
I80F48.from(dto.reservedQuoteAsBaseHighestBid),
|
|
||||||
dto.baseInfoIndex,
|
|
||||||
dto.quoteInfoIndex,
|
|
||||||
dto.marketIndex as MarketIndex,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static emptyFromSerum3Market(
|
static emptyFromSerum3Market(
|
||||||
serum3Market: Serum3Market,
|
serum3Market: Serum3Market,
|
||||||
baseEntryIndex: number,
|
baseEntryIndex: number,
|
||||||
|
@ -1756,29 +1716,6 @@ export class PerpInfo {
|
||||||
public hasOpenOrders: boolean,
|
public hasOpenOrders: boolean,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
static fromDto(dto: PerpInfoDto): PerpInfo {
|
|
||||||
return new PerpInfo(
|
|
||||||
dto.perpMarketIndex,
|
|
||||||
dto.settleTokenIndex as TokenIndex,
|
|
||||||
I80F48.from(dto.maintBaseAssetWeight),
|
|
||||||
I80F48.from(dto.initBaseAssetWeight),
|
|
||||||
I80F48.from(dto.maintBaseLiabWeight),
|
|
||||||
I80F48.from(dto.initBaseLiabWeight),
|
|
||||||
I80F48.from(dto.maintOverallAssetWeight),
|
|
||||||
I80F48.from(dto.initOverallAssetWeight),
|
|
||||||
dto.baseLotSize,
|
|
||||||
dto.baseLots,
|
|
||||||
dto.bidsBaseLots,
|
|
||||||
dto.asksBaseLots,
|
|
||||||
I80F48.from(dto.quote),
|
|
||||||
new Prices(
|
|
||||||
I80F48.from(dto.prices.oracle),
|
|
||||||
I80F48.from(dto.prices.stable),
|
|
||||||
),
|
|
||||||
dto.hasOpenOrders,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromPerpPosition(
|
static fromPerpPosition(
|
||||||
perpMarket: PerpMarket,
|
perpMarket: PerpMarket,
|
||||||
perpPosition: PerpPosition,
|
perpPosition: PerpPosition,
|
||||||
|
@ -1972,82 +1909,3 @@ export class PerpInfo {
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HealthCacheDto {
|
|
||||||
tokenInfos: TokenInfoDto[];
|
|
||||||
serum3Infos: Serum3InfoDto[];
|
|
||||||
perpInfos: PerpInfoDto[];
|
|
||||||
}
|
|
||||||
export class TokenInfoDto {
|
|
||||||
tokenIndex: number;
|
|
||||||
maintAssetWeight: I80F48Dto;
|
|
||||||
initAssetWeight: I80F48Dto;
|
|
||||||
initScaledAssetWeight: I80F48Dto;
|
|
||||||
maintLiabWeight: I80F48Dto;
|
|
||||||
initLiabWeight: I80F48Dto;
|
|
||||||
initScaledLiabWeight: I80F48Dto;
|
|
||||||
prices: { oracle: I80F48Dto; stable: I80F48Dto };
|
|
||||||
balanceSpot: I80F48Dto;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
tokenIndex: number,
|
|
||||||
maintAssetWeight: I80F48Dto,
|
|
||||||
initAssetWeight: I80F48Dto,
|
|
||||||
initScaledAssetWeight: I80F48Dto,
|
|
||||||
maintLiabWeight: I80F48Dto,
|
|
||||||
initLiabWeight: I80F48Dto,
|
|
||||||
initScaledLiabWeight: I80F48Dto,
|
|
||||||
prices: { oracle: I80F48Dto; stable: I80F48Dto },
|
|
||||||
balanceNative: I80F48Dto,
|
|
||||||
) {
|
|
||||||
this.tokenIndex = tokenIndex;
|
|
||||||
this.maintAssetWeight = maintAssetWeight;
|
|
||||||
this.initAssetWeight = initAssetWeight;
|
|
||||||
this.initScaledAssetWeight = initScaledAssetWeight;
|
|
||||||
this.maintLiabWeight = maintLiabWeight;
|
|
||||||
this.initLiabWeight = initLiabWeight;
|
|
||||||
this.initScaledLiabWeight = initScaledLiabWeight;
|
|
||||||
this.prices = prices;
|
|
||||||
this.balanceSpot = balanceNative;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Serum3InfoDto {
|
|
||||||
reservedBase: I80F48Dto;
|
|
||||||
reservedQuote: I80F48Dto;
|
|
||||||
reservedBaseAsQuoteLowestAsk: I80F48Dto;
|
|
||||||
reservedQuoteAsBaseHighestBid: I80F48Dto;
|
|
||||||
baseInfoIndex: number;
|
|
||||||
quoteInfoIndex: number;
|
|
||||||
marketIndex: number;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
reservedBase: I80F48Dto,
|
|
||||||
reservedQuote: I80F48Dto,
|
|
||||||
baseInfoIndex: number,
|
|
||||||
quoteInfoIndex: number,
|
|
||||||
) {
|
|
||||||
this.reservedBase = reservedBase;
|
|
||||||
this.reservedQuote = reservedQuote;
|
|
||||||
this.baseInfoIndex = baseInfoIndex;
|
|
||||||
this.quoteInfoIndex = quoteInfoIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PerpInfoDto {
|
|
||||||
perpMarketIndex: number;
|
|
||||||
settleTokenIndex: number;
|
|
||||||
maintBaseAssetWeight: I80F48Dto;
|
|
||||||
initBaseAssetWeight: I80F48Dto;
|
|
||||||
maintBaseLiabWeight: I80F48Dto;
|
|
||||||
initBaseLiabWeight: I80F48Dto;
|
|
||||||
maintOverallAssetWeight: I80F48Dto;
|
|
||||||
initOverallAssetWeight: I80F48Dto;
|
|
||||||
public baseLotSize: BN;
|
|
||||||
public baseLots: BN;
|
|
||||||
public bidsBaseLots: BN;
|
|
||||||
public asksBaseLots: BN;
|
|
||||||
quote: I80F48Dto;
|
|
||||||
prices: { oracle: I80F48Dto; stable: I80F48Dto };
|
|
||||||
hasOpenOrders: boolean;
|
|
||||||
}
|
|
||||||
|
|
|
@ -462,6 +462,10 @@ export class MangoClient {
|
||||||
params.tokenConditionalSwapTakerFeeRate,
|
params.tokenConditionalSwapTakerFeeRate,
|
||||||
params.tokenConditionalSwapMakerFeeRate,
|
params.tokenConditionalSwapMakerFeeRate,
|
||||||
params.flashLoanSwapFeeRate,
|
params.flashLoanSwapFeeRate,
|
||||||
|
params.interestCurveScaling,
|
||||||
|
params.interestTargetUtilization,
|
||||||
|
params.groupInsuranceFund,
|
||||||
|
params.depositLimit,
|
||||||
)
|
)
|
||||||
.accounts({
|
.accounts({
|
||||||
group: group.publicKey,
|
group: group.publicKey,
|
||||||
|
@ -537,6 +541,15 @@ export class MangoClient {
|
||||||
params.tokenConditionalSwapTakerFeeRate,
|
params.tokenConditionalSwapTakerFeeRate,
|
||||||
params.tokenConditionalSwapMakerFeeRate,
|
params.tokenConditionalSwapMakerFeeRate,
|
||||||
params.flashLoanSwapFeeRate,
|
params.flashLoanSwapFeeRate,
|
||||||
|
params.interestCurveScaling,
|
||||||
|
params.interestTargetUtilization,
|
||||||
|
params.maintWeightShiftStart,
|
||||||
|
params.maintWeightShiftEnd,
|
||||||
|
params.maintWeightShiftAssetTarget,
|
||||||
|
params.maintWeightShiftLiabTarget,
|
||||||
|
params.maintWeightShiftAbort ?? false,
|
||||||
|
false, // setFallbackOracle, unused
|
||||||
|
params.depositLimit,
|
||||||
)
|
)
|
||||||
.accounts({
|
.accounts({
|
||||||
group: group.publicKey,
|
group: group.publicKey,
|
||||||
|
@ -1604,9 +1617,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,
|
||||||
|
@ -1626,11 +1640,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,
|
||||||
|
@ -1885,6 +1900,32 @@ export class MangoClient {
|
||||||
orderType: Serum3OrderType,
|
orderType: Serum3OrderType,
|
||||||
clientOrderId: number,
|
clientOrderId: number,
|
||||||
limit: number,
|
limit: number,
|
||||||
|
): Promise<TransactionInstruction[]> {
|
||||||
|
return await this.serum3PlaceOrderV2Ix(
|
||||||
|
group,
|
||||||
|
mangoAccount,
|
||||||
|
externalMarketPk,
|
||||||
|
side,
|
||||||
|
price,
|
||||||
|
size,
|
||||||
|
selfTradeBehavior,
|
||||||
|
orderType,
|
||||||
|
clientOrderId,
|
||||||
|
limit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async serum3PlaceOrderV1Ix(
|
||||||
|
group: Group,
|
||||||
|
mangoAccount: MangoAccount,
|
||||||
|
externalMarketPk: PublicKey,
|
||||||
|
side: Serum3Side,
|
||||||
|
price: number,
|
||||||
|
size: number,
|
||||||
|
selfTradeBehavior: Serum3SelfTradeBehavior,
|
||||||
|
orderType: Serum3OrderType,
|
||||||
|
clientOrderId: number,
|
||||||
|
limit: number,
|
||||||
): Promise<TransactionInstruction[]> {
|
): Promise<TransactionInstruction[]> {
|
||||||
const ixs: TransactionInstruction[] = [];
|
const ixs: TransactionInstruction[] = [];
|
||||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||||
|
@ -1998,6 +2039,143 @@ export class MangoClient {
|
||||||
return ixs;
|
return ixs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async serum3PlaceOrderV2Ix(
|
||||||
|
group: Group,
|
||||||
|
mangoAccount: MangoAccount,
|
||||||
|
externalMarketPk: PublicKey,
|
||||||
|
side: Serum3Side,
|
||||||
|
price: number,
|
||||||
|
size: number,
|
||||||
|
selfTradeBehavior: Serum3SelfTradeBehavior,
|
||||||
|
orderType: Serum3OrderType,
|
||||||
|
clientOrderId: number,
|
||||||
|
limit: number,
|
||||||
|
): Promise<TransactionInstruction[]> {
|
||||||
|
const ixs: TransactionInstruction[] = [];
|
||||||
|
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||||
|
externalMarketPk.toBase58(),
|
||||||
|
)!;
|
||||||
|
|
||||||
|
let openOrderPk: PublicKey | undefined = undefined;
|
||||||
|
const banks: Bank[] = [];
|
||||||
|
const openOrdersForMarket: [Serum3Market, PublicKey][] = [];
|
||||||
|
if (!mangoAccount.getSerum3Account(serum3Market.marketIndex)) {
|
||||||
|
const ix = await this.serum3CreateOpenOrdersIx(
|
||||||
|
group,
|
||||||
|
mangoAccount,
|
||||||
|
serum3Market.serumMarketExternal,
|
||||||
|
);
|
||||||
|
ixs.push(ix);
|
||||||
|
openOrderPk = await serum3Market.findOoPda(
|
||||||
|
this.program.programId,
|
||||||
|
mangoAccount.publicKey,
|
||||||
|
);
|
||||||
|
openOrdersForMarket.push([serum3Market, openOrderPk]);
|
||||||
|
const baseTokenIndex = serum3Market.baseTokenIndex;
|
||||||
|
const quoteTokenIndex = serum3Market.quoteTokenIndex;
|
||||||
|
// only include banks if no deposit has been previously made for same token
|
||||||
|
banks.push(group.getFirstBankByTokenIndex(quoteTokenIndex));
|
||||||
|
banks.push(group.getFirstBankByTokenIndex(baseTokenIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
const healthRemainingAccounts: PublicKey[] =
|
||||||
|
this.buildHealthRemainingAccounts(
|
||||||
|
group,
|
||||||
|
[mangoAccount],
|
||||||
|
banks,
|
||||||
|
[],
|
||||||
|
openOrdersForMarket,
|
||||||
|
);
|
||||||
|
|
||||||
|
const serum3MarketExternal = group.serum3ExternalMarketsMap.get(
|
||||||
|
externalMarketPk.toBase58(),
|
||||||
|
)!;
|
||||||
|
const serum3MarketExternalVaultSigner =
|
||||||
|
await generateSerum3MarketExternalVaultSignerAddress(
|
||||||
|
this.cluster,
|
||||||
|
serum3Market,
|
||||||
|
serum3MarketExternal,
|
||||||
|
);
|
||||||
|
|
||||||
|
const limitPrice = serum3MarketExternal.priceNumberToLots(price);
|
||||||
|
const maxBaseQuantity = serum3MarketExternal.baseSizeNumberToLots(size);
|
||||||
|
const isTaker = orderType !== Serum3OrderType.postOnly;
|
||||||
|
const maxQuoteQuantity = new BN(
|
||||||
|
Math.ceil(
|
||||||
|
serum3MarketExternal.decoded.quoteLotSize.toNumber() *
|
||||||
|
(1 + Math.max(serum3Market.getFeeRates(isTaker), 0)) *
|
||||||
|
serum3MarketExternal.baseSizeNumberToLots(size).toNumber() *
|
||||||
|
serum3MarketExternal.priceNumberToLots(price).toNumber(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const payerTokenIndex = ((): TokenIndex => {
|
||||||
|
if (side == Serum3Side.bid) {
|
||||||
|
return serum3Market.quoteTokenIndex;
|
||||||
|
} else {
|
||||||
|
return serum3Market.baseTokenIndex;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const receiverTokenIndex = ((): TokenIndex => {
|
||||||
|
if (side == Serum3Side.bid) {
|
||||||
|
return serum3Market.baseTokenIndex;
|
||||||
|
} else {
|
||||||
|
return serum3Market.quoteTokenIndex;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const payerBank = group.getFirstBankByTokenIndex(payerTokenIndex);
|
||||||
|
const receiverBank = group.getFirstBankByTokenIndex(receiverTokenIndex);
|
||||||
|
const ix = await this.program.methods
|
||||||
|
.serum3PlaceOrderV2(
|
||||||
|
side,
|
||||||
|
limitPrice,
|
||||||
|
maxBaseQuantity,
|
||||||
|
maxQuoteQuantity,
|
||||||
|
selfTradeBehavior,
|
||||||
|
orderType,
|
||||||
|
new BN(clientOrderId),
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
.accounts({
|
||||||
|
group: group.publicKey,
|
||||||
|
account: mangoAccount.publicKey,
|
||||||
|
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||||
|
openOrders:
|
||||||
|
openOrderPk ||
|
||||||
|
mangoAccount.getSerum3Account(serum3Market.marketIndex)?.openOrders,
|
||||||
|
serumMarket: serum3Market.publicKey,
|
||||||
|
serumProgram: OPENBOOK_PROGRAM_ID[this.cluster],
|
||||||
|
serumMarketExternal: serum3Market.serumMarketExternal,
|
||||||
|
marketBids: serum3MarketExternal.bidsAddress,
|
||||||
|
marketAsks: serum3MarketExternal.asksAddress,
|
||||||
|
marketEventQueue: serum3MarketExternal.decoded.eventQueue,
|
||||||
|
marketRequestQueue: serum3MarketExternal.decoded.requestQueue,
|
||||||
|
marketBaseVault: serum3MarketExternal.decoded.baseVault,
|
||||||
|
marketQuoteVault: serum3MarketExternal.decoded.quoteVault,
|
||||||
|
marketVaultSigner: serum3MarketExternalVaultSigner,
|
||||||
|
payerBank: payerBank.publicKey,
|
||||||
|
payerVault: payerBank.vault,
|
||||||
|
payerOracle: payerBank.oracle,
|
||||||
|
})
|
||||||
|
.remainingAccounts(
|
||||||
|
healthRemainingAccounts.map(
|
||||||
|
(pk) =>
|
||||||
|
({
|
||||||
|
pubkey: pk,
|
||||||
|
isWritable: receiverBank.publicKey.equals(pk) ? true : false,
|
||||||
|
isSigner: false,
|
||||||
|
} as AccountMeta),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.instruction();
|
||||||
|
|
||||||
|
ixs.push(ix);
|
||||||
|
|
||||||
|
return ixs;
|
||||||
|
}
|
||||||
|
|
||||||
public async serum3PlaceOrder(
|
public async serum3PlaceOrder(
|
||||||
group: Group,
|
group: Group,
|
||||||
mangoAccount: MangoAccount,
|
mangoAccount: MangoAccount,
|
||||||
|
@ -2010,7 +2188,7 @@ export class MangoClient {
|
||||||
clientOrderId: number,
|
clientOrderId: number,
|
||||||
limit: number,
|
limit: number,
|
||||||
): Promise<MangoSignatureStatus> {
|
): Promise<MangoSignatureStatus> {
|
||||||
const placeOrderIxs = await this.serum3PlaceOrderIx(
|
const placeOrderIxs = await this.serum3PlaceOrderV2Ix(
|
||||||
group,
|
group,
|
||||||
mangoAccount,
|
mangoAccount,
|
||||||
externalMarketPk,
|
externalMarketPk,
|
||||||
|
@ -4875,7 +5053,7 @@ export class MangoClient {
|
||||||
orderId,
|
orderId,
|
||||||
),
|
),
|
||||||
this.serum3SettleFundsV2Ix(group, mangoAccount, externalMarketPk),
|
this.serum3SettleFundsV2Ix(group, mangoAccount, externalMarketPk),
|
||||||
this.serum3PlaceOrderIx(
|
this.serum3PlaceOrderV2Ix(
|
||||||
group,
|
group,
|
||||||
mangoAccount,
|
mangoAccount,
|
||||||
externalMarketPk,
|
externalMarketPk,
|
||||||
|
|
|
@ -25,6 +25,9 @@ export interface TokenRegisterParams {
|
||||||
tokenConditionalSwapTakerFeeRate: number;
|
tokenConditionalSwapTakerFeeRate: number;
|
||||||
tokenConditionalSwapMakerFeeRate: number;
|
tokenConditionalSwapMakerFeeRate: number;
|
||||||
flashLoanSwapFeeRate: number;
|
flashLoanSwapFeeRate: number;
|
||||||
|
interestCurveScaling: number;
|
||||||
|
interestTargetUtilization: number;
|
||||||
|
depositLimit: BN;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DefaultTokenRegisterParams: TokenRegisterParams = {
|
export const DefaultTokenRegisterParams: TokenRegisterParams = {
|
||||||
|
@ -35,10 +38,10 @@ export const DefaultTokenRegisterParams: TokenRegisterParams = {
|
||||||
groupInsuranceFund: false,
|
groupInsuranceFund: false,
|
||||||
interestRateParams: {
|
interestRateParams: {
|
||||||
util0: 0.5,
|
util0: 0.5,
|
||||||
rate0: 0.072,
|
rate0: 0.018,
|
||||||
util1: 0.8,
|
util1: 0.8,
|
||||||
rate1: 0.2,
|
rate1: 0.05,
|
||||||
maxRate: 2,
|
maxRate: 0.5,
|
||||||
adjustmentFactor: 0.004,
|
adjustmentFactor: 0.004,
|
||||||
},
|
},
|
||||||
loanFeeRate: 0.0005,
|
loanFeeRate: 0.0005,
|
||||||
|
@ -60,6 +63,9 @@ export const DefaultTokenRegisterParams: TokenRegisterParams = {
|
||||||
tokenConditionalSwapTakerFeeRate: 0.0005,
|
tokenConditionalSwapTakerFeeRate: 0.0005,
|
||||||
tokenConditionalSwapMakerFeeRate: 0.0005,
|
tokenConditionalSwapMakerFeeRate: 0.0005,
|
||||||
flashLoanSwapFeeRate: 0.0005,
|
flashLoanSwapFeeRate: 0.0005,
|
||||||
|
interestCurveScaling: 4.0,
|
||||||
|
interestTargetUtilization: 0.5,
|
||||||
|
depositLimit: new BN(0),
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface TokenEditParams {
|
export interface TokenEditParams {
|
||||||
|
@ -90,6 +96,14 @@ export interface TokenEditParams {
|
||||||
tokenConditionalSwapTakerFeeRate: number | null;
|
tokenConditionalSwapTakerFeeRate: number | null;
|
||||||
tokenConditionalSwapMakerFeeRate: number | null;
|
tokenConditionalSwapMakerFeeRate: number | null;
|
||||||
flashLoanSwapFeeRate: number | null;
|
flashLoanSwapFeeRate: number | null;
|
||||||
|
interestCurveScaling: number | null;
|
||||||
|
interestTargetUtilization: number | null;
|
||||||
|
maintWeightShiftStart: BN | null;
|
||||||
|
maintWeightShiftEnd: BN | null;
|
||||||
|
maintWeightShiftAssetTarget: number | null;
|
||||||
|
maintWeightShiftLiabTarget: number | null;
|
||||||
|
maintWeightShiftAbort: boolean | null;
|
||||||
|
depositLimit: BN | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NullTokenEditParams: TokenEditParams = {
|
export const NullTokenEditParams: TokenEditParams = {
|
||||||
|
@ -120,6 +134,14 @@ export const NullTokenEditParams: TokenEditParams = {
|
||||||
tokenConditionalSwapTakerFeeRate: null,
|
tokenConditionalSwapTakerFeeRate: null,
|
||||||
tokenConditionalSwapMakerFeeRate: null,
|
tokenConditionalSwapMakerFeeRate: null,
|
||||||
flashLoanSwapFeeRate: null,
|
flashLoanSwapFeeRate: null,
|
||||||
|
interestCurveScaling: null,
|
||||||
|
interestTargetUtilization: null,
|
||||||
|
maintWeightShiftStart: null,
|
||||||
|
maintWeightShiftEnd: null,
|
||||||
|
maintWeightShiftAssetTarget: null,
|
||||||
|
maintWeightShiftLiabTarget: null,
|
||||||
|
maintWeightShiftAbort: null,
|
||||||
|
depositLimit: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface PerpEditParams {
|
export interface PerpEditParams {
|
||||||
|
@ -264,6 +286,7 @@ export interface IxGateParams {
|
||||||
TokenConditionalSwapStart: boolean;
|
TokenConditionalSwapStart: boolean;
|
||||||
TokenConditionalSwapCreatePremiumAuction: boolean;
|
TokenConditionalSwapCreatePremiumAuction: boolean;
|
||||||
TokenConditionalSwapCreateLinearAuction: boolean;
|
TokenConditionalSwapCreateLinearAuction: boolean;
|
||||||
|
Serum3PlaceOrderV2: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default with all ixs enabled, use with buildIxGate
|
// Default with all ixs enabled, use with buildIxGate
|
||||||
|
@ -342,6 +365,7 @@ export const TrueIxGateParams: IxGateParams = {
|
||||||
TokenConditionalSwapStart: true,
|
TokenConditionalSwapStart: true,
|
||||||
TokenConditionalSwapCreatePremiumAuction: true,
|
TokenConditionalSwapCreatePremiumAuction: true,
|
||||||
TokenConditionalSwapCreateLinearAuction: true,
|
TokenConditionalSwapCreateLinearAuction: true,
|
||||||
|
Serum3PlaceOrderV2: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// build ix gate e.g. buildIxGate(Builder(TrueIxGateParams).TokenDeposit(false).build()).toNumber(),
|
// build ix gate e.g. buildIxGate(Builder(TrueIxGateParams).TokenDeposit(false).build()).toNumber(),
|
||||||
|
@ -430,6 +454,7 @@ export function buildIxGate(p: IxGateParams): BN {
|
||||||
toggleIx(ixGate, p, 'TokenConditionalSwapStart', 68);
|
toggleIx(ixGate, p, 'TokenConditionalSwapStart', 68);
|
||||||
toggleIx(ixGate, p, 'TokenConditionalSwapCreatePremiumAuction', 69);
|
toggleIx(ixGate, p, 'TokenConditionalSwapCreatePremiumAuction', 69);
|
||||||
toggleIx(ixGate, p, 'TokenConditionalSwapCreateLinearAuction', 70);
|
toggleIx(ixGate, p, 'TokenConditionalSwapCreateLinearAuction', 70);
|
||||||
|
toggleIx(ixGate, p, 'Serum3PlaceOrderV2', 71);
|
||||||
|
|
||||||
return ixGate;
|
return ixGate;
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue