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
|
||||
|
||||
### 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)
|
||||
|
||||
|
@ -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 too-strict validation of max rate on token edit (#734)
|
||||
|
||||
## mainnet
|
||||
|
||||
### v0.19.1, 2023-9-16
|
||||
|
||||
Deployment: Sep 16, 2023 at 11:20:20 Central European Summer Time, https://explorer.solana.com/tx/K9BJ1uDBH6Xe8erhS6C8Rmz6k6V1cKJ8z6wNmf4DV2aF5Woin4H5xXKj1ypTNDSTccNvcsAUTHStoai3k2hYY5E
|
||||
|
|
|
@ -3301,7 +3301,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "mango-v4"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"anchor-lang",
|
||||
"anchor-spl",
|
||||
|
|
|
@ -309,7 +309,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
// TODO: This is where we could multiply in the liquidation fee factors
|
||||
let price = source_price / target_price;
|
||||
|
||||
util::max_swap_source(
|
||||
util::max_swap_source_ignoring_limits(
|
||||
self.client,
|
||||
self.account_fetcher,
|
||||
&liqor,
|
||||
|
|
|
@ -26,10 +26,9 @@ use crate::{token_swap_info, util, ErrorTracking};
|
|||
/// making the whole execution fail.
|
||||
const SLIPPAGE_BUFFER: f64 = 0.01; // 1%
|
||||
|
||||
/// If a tcs gets limited due to exhausted net borrows, don't trigger execution if
|
||||
/// the possible value is below this amount. This avoids spamming executions when net
|
||||
/// borrows are exhausted.
|
||||
const NET_BORROW_EXECUTION_THRESHOLD: u64 = 1_000_000; // 1 USD
|
||||
/// If a tcs gets limited due to exhausted net borrows or deposit limits, don't trigger execution if
|
||||
/// the possible value is below this amount. This avoids spamming executions when limits are exhausted.
|
||||
const EXECUTION_THRESHOLD: u64 = 1_000_000; // 1 USD
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
|
@ -440,12 +439,17 @@ impl Context {
|
|||
/// This includes
|
||||
/// - tcs restrictions (remaining buy/sell, create borrows/deposits)
|
||||
/// - reduce only banks
|
||||
/// - net borrow limits on BOTH sides, even though the buy side is technically
|
||||
/// a liqor limitation: the liqor could acquire the token before trying the
|
||||
/// execution... but in practice the liqor will work on margin
|
||||
/// - net borrow limits:
|
||||
/// - the account may borrow the sell token (and the liqor side may not be a repay)
|
||||
/// - the liqor may borrow the buy token (and the account side may not be a repay)
|
||||
/// this is technically a liqor limitation: the liqor could acquire the token before trying the
|
||||
/// execution... but in practice the liqor may work on margin
|
||||
/// - deposit limits:
|
||||
/// - the account may deposit the buy token (while the liqor borrowed it)
|
||||
/// - the liqor may deposit the sell token (while the account borrowed it)
|
||||
///
|
||||
/// Returns Some((native buy amount, native sell amount)) if execution is sensible
|
||||
/// Returns None if the execution should be skipped (due to net borrow limits...)
|
||||
/// Returns None if the execution should be skipped (due to limits)
|
||||
pub fn tcs_max_liqee_execution(
|
||||
&self,
|
||||
account: &MangoAccountValue,
|
||||
|
@ -458,18 +462,18 @@ impl Context {
|
|||
let premium_price = tcs.premium_price(base_price.to_num(), self.now_ts);
|
||||
let maker_price = tcs.maker_price(premium_price);
|
||||
|
||||
let buy_position = account
|
||||
let liqee_buy_position = account
|
||||
.token_position(tcs.buy_token_index)
|
||||
.map(|p| p.native(&buy_bank))
|
||||
.unwrap_or(I80F48::ZERO);
|
||||
let sell_position = account
|
||||
let liqee_sell_position = account
|
||||
.token_position(tcs.sell_token_index)
|
||||
.map(|p| p.native(&sell_bank))
|
||||
.unwrap_or(I80F48::ZERO);
|
||||
|
||||
// this is in "buy token received per sell token given" units
|
||||
let swap_price = I80F48::from_num((1.0 - SLIPPAGE_BUFFER) / maker_price);
|
||||
let max_sell_ignoring_net_borrows = util::max_swap_source_ignore_net_borrows(
|
||||
let max_sell_ignoring_limits = util::max_swap_source_ignoring_limits(
|
||||
&self.mango_client,
|
||||
&self.account_fetcher,
|
||||
account,
|
||||
|
@ -480,41 +484,31 @@ impl Context {
|
|||
)?
|
||||
.floor()
|
||||
.to_num::<u64>()
|
||||
.min(tcs.max_sell_for_position(sell_position, &sell_bank));
|
||||
.min(tcs.max_sell_for_position(liqee_sell_position, &sell_bank));
|
||||
|
||||
let max_buy_ignoring_net_borrows = tcs.max_buy_for_position(buy_position, &buy_bank);
|
||||
let max_buy_ignoring_limits = tcs.max_buy_for_position(liqee_buy_position, &buy_bank);
|
||||
|
||||
// What follows is a complex manual handling of net borrow limits, for the following reason:
|
||||
// What follows is a complex manual handling of net borrow/deposit limits, for
|
||||
// the following reason:
|
||||
// Usually, we want to execute tcs even for small amounts because that will close the
|
||||
// tcs order: either due to full execution or due to the health threshold being reached.
|
||||
//
|
||||
// However, when the net borrow limits are hit, it will not closed when no further execution
|
||||
// is possible, because net borrow limit issues are considered transient. Furthermore, we
|
||||
// don't even want to send a tiny tcs trigger transactions, because there's a good chance we
|
||||
// would then be sending lot of those as oracle prices fluctuate.
|
||||
// However, when the limits are hit, it will not closed when no further execution
|
||||
// is possible, because limit issues are transient. Furthermore, we don't want to send
|
||||
// tiny tcs trigger transactions, because there's a good chance we would then be sending
|
||||
// lot of those as oracle prices fluctuate.
|
||||
//
|
||||
// Thus, we need to detect if the possible execution amount is tiny _because_ of the
|
||||
// net borrow limits. Then skip. If it's tiny for other reasons we can proceed.
|
||||
// limits. Then skip. If it's tiny for other reasons we can proceed.
|
||||
|
||||
fn available_borrows(bank: &Bank, price: I80F48) -> u64 {
|
||||
(bank.remaining_net_borrows_quote(price) / price).clamp_to_u64()
|
||||
}
|
||||
let available_buy_borrows = available_borrows(&buy_bank, buy_token_price);
|
||||
let available_sell_borrows = available_borrows(&sell_bank, sell_token_price);
|
||||
|
||||
// New borrows if max_sell_ignoring_net_borrows was withdrawn on the liqee
|
||||
let sell_borrows = (I80F48::from(max_sell_ignoring_net_borrows)
|
||||
- sell_position.max(I80F48::ZERO))
|
||||
.clamp_to_u64();
|
||||
|
||||
// On the buy side, the liqor might need to borrow
|
||||
let buy_borrows = match self.config.mode {
|
||||
// Do the liqor buy tokens come from deposits or are they borrowed?
|
||||
let mut liqor_buy_borrows = match self.config.mode {
|
||||
Mode::BorrowBuyToken => {
|
||||
// Assume that the liqor has enough buy token if it's collateral
|
||||
if tcs.buy_token_index == self.config.collateral_token_index {
|
||||
0
|
||||
} else {
|
||||
max_buy_ignoring_net_borrows
|
||||
max_buy_ignoring_limits
|
||||
}
|
||||
}
|
||||
Mode::SwapCollateralIntoBuy { .. } => 0,
|
||||
|
@ -525,19 +519,77 @@ impl Context {
|
|||
}
|
||||
};
|
||||
|
||||
// New maximums adjusted for net borrow limits
|
||||
let max_sell =
|
||||
max_sell_ignoring_net_borrows - sell_borrows + sell_borrows.min(available_sell_borrows);
|
||||
let max_buy =
|
||||
max_buy_ignoring_net_borrows - buy_borrows + buy_borrows.min(available_buy_borrows);
|
||||
// First, net borrow limits
|
||||
let max_sell_net_borrows;
|
||||
let max_buy_net_borrows;
|
||||
{
|
||||
fn available_borrows(bank: &Bank, price: I80F48) -> u64 {
|
||||
bank.remaining_net_borrows_quote(price)
|
||||
.saturating_div(price)
|
||||
.clamp_to_u64()
|
||||
}
|
||||
let available_buy_borrows = available_borrows(&buy_bank, buy_token_price);
|
||||
let available_sell_borrows = available_borrows(&sell_bank, sell_token_price);
|
||||
|
||||
let tiny_due_to_net_borrows = {
|
||||
let buy_threshold = I80F48::from(NET_BORROW_EXECUTION_THRESHOLD) / buy_token_price;
|
||||
let sell_threshold = I80F48::from(NET_BORROW_EXECUTION_THRESHOLD) / sell_token_price;
|
||||
max_buy < buy_threshold && max_buy_ignoring_net_borrows > buy_threshold
|
||||
|| max_sell < sell_threshold && max_sell_ignoring_net_borrows > sell_threshold
|
||||
// New borrows if max_sell_ignoring_limits was withdrawn on the liqee
|
||||
// We assume that on the liqor side the position is >= 0, so these are true
|
||||
// new borrows.
|
||||
let sell_borrows = (I80F48::from(max_sell_ignoring_limits)
|
||||
- liqee_sell_position.max(I80F48::ZERO))
|
||||
.ceil()
|
||||
.clamp_to_u64();
|
||||
|
||||
// On the buy side, the liqor might need to borrow, see liqor_buy_borrows.
|
||||
// On the liqee side, the bought tokens may repay a borrow, reducing net borrows again
|
||||
let buy_borrows = (I80F48::from(liqor_buy_borrows)
|
||||
+ liqee_buy_position.min(I80F48::ZERO))
|
||||
.ceil()
|
||||
.clamp_to_u64();
|
||||
|
||||
// New maximums adjusted for net borrow limits
|
||||
max_sell_net_borrows = max_sell_ignoring_limits
|
||||
- (sell_borrows - sell_borrows.min(available_sell_borrows));
|
||||
max_buy_net_borrows =
|
||||
max_buy_ignoring_limits - (buy_borrows - buy_borrows.min(available_buy_borrows));
|
||||
liqor_buy_borrows = liqor_buy_borrows.min(max_buy_net_borrows);
|
||||
}
|
||||
|
||||
// Second, deposit limits
|
||||
let max_sell;
|
||||
let max_buy;
|
||||
{
|
||||
let available_buy_deposits = buy_bank.remaining_deposits_until_limit().clamp_to_u64();
|
||||
let available_sell_deposits = sell_bank.remaining_deposits_until_limit().clamp_to_u64();
|
||||
|
||||
// New deposits on the liqee side (reduced by repaid borrows)
|
||||
let liqee_buy_deposits = (I80F48::from(max_buy_net_borrows)
|
||||
+ liqee_buy_position.min(I80F48::ZERO))
|
||||
.ceil()
|
||||
.clamp_to_u64();
|
||||
// the net new deposits can only be as big as the liqor borrows
|
||||
// (assume no borrows, then deposits only move from liqor to liqee)
|
||||
let buy_deposits = liqee_buy_deposits.min(liqor_buy_borrows);
|
||||
|
||||
// We assume the liqor position is always >= 0, meaning there are new sell token deposits if
|
||||
// the sell token gets borrowed on the liqee side.
|
||||
let sell_deposits = (I80F48::from(max_sell_net_borrows)
|
||||
- liqee_sell_position.max(I80F48::ZERO))
|
||||
.ceil()
|
||||
.clamp_to_u64();
|
||||
|
||||
max_sell =
|
||||
max_sell_net_borrows - (sell_deposits - sell_deposits.min(available_sell_deposits));
|
||||
max_buy =
|
||||
max_buy_net_borrows - (buy_deposits - buy_deposits.min(available_buy_deposits));
|
||||
}
|
||||
|
||||
let tiny_due_to_limits = {
|
||||
let buy_threshold = I80F48::from(EXECUTION_THRESHOLD) / buy_token_price;
|
||||
let sell_threshold = I80F48::from(EXECUTION_THRESHOLD) / sell_token_price;
|
||||
max_buy < buy_threshold && max_buy_ignoring_limits > buy_threshold
|
||||
|| max_sell < sell_threshold && max_sell_ignoring_limits > sell_threshold
|
||||
};
|
||||
if tiny_due_to_net_borrows {
|
||||
if tiny_due_to_limits {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
|
@ -715,7 +767,7 @@ impl Context {
|
|||
.0
|
||||
.native(&buy_bank);
|
||||
let liqor_available_buy_token = match mode {
|
||||
Mode::BorrowBuyToken => util::max_swap_source(
|
||||
Mode::BorrowBuyToken => util::max_swap_source_with_limits(
|
||||
&self.mango_client,
|
||||
&self.account_fetcher,
|
||||
&liqor,
|
||||
|
@ -734,7 +786,7 @@ impl Context {
|
|||
self.token_bank_price_mint(collateral_token_index)?;
|
||||
let buy_per_collateral_price = (collateral_price / buy_token_price)
|
||||
* I80F48::from_num(jupiter_slippage_fraction);
|
||||
let collateral_amount = util::max_swap_source(
|
||||
let collateral_amount = util::max_swap_source_with_limits(
|
||||
&self.mango_client,
|
||||
&self.account_fetcher,
|
||||
&liqor,
|
||||
|
@ -751,7 +803,7 @@ impl Context {
|
|||
// How big can the sell -> buy swap be?
|
||||
let buy_per_sell_price =
|
||||
(I80F48::from(1) / taker_price) * I80F48::from_num(jupiter_slippage_fraction);
|
||||
let max_sell = util::max_swap_source(
|
||||
let max_sell = util::max_swap_source_with_limits(
|
||||
&self.mango_client,
|
||||
&self.account_fetcher,
|
||||
&liqor,
|
||||
|
|
|
@ -38,7 +38,9 @@ pub fn is_perp_market<'a>(
|
|||
}
|
||||
|
||||
/// Convenience wrapper for getting max swap amounts for a token pair
|
||||
pub fn max_swap_source(
|
||||
///
|
||||
/// This applies net borrow and deposit limits, which is useful for true swaps.
|
||||
pub fn max_swap_source_with_limits(
|
||||
client: &MangoClient,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
account: &MangoAccountValue,
|
||||
|
@ -66,7 +68,7 @@ pub fn max_swap_source(
|
|||
let source_price = health_cache.token_info(source).unwrap().prices.oracle;
|
||||
|
||||
let amount = health_cache
|
||||
.max_swap_source_for_health_ratio(
|
||||
.max_swap_source_for_health_ratio_with_limits(
|
||||
&account,
|
||||
&source_bank,
|
||||
source_price,
|
||||
|
@ -79,7 +81,10 @@ pub fn max_swap_source(
|
|||
}
|
||||
|
||||
/// Convenience wrapper for getting max swap amounts for a token pair
|
||||
pub fn max_swap_source_ignore_net_borrows(
|
||||
///
|
||||
/// This is useful for liquidations, which don't increase deposits or net borrows.
|
||||
/// Tcs execution can also increase deposits/net borrows.
|
||||
pub fn max_swap_source_ignoring_limits(
|
||||
client: &MangoClient,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
account: &MangoAccountValue,
|
||||
|
@ -99,17 +104,15 @@ pub fn max_swap_source_ignore_net_borrows(
|
|||
mango_v4_client::health_cache::new_sync(&client.context, account_fetcher, &account)
|
||||
.expect("always ok");
|
||||
|
||||
let mut source_bank: Bank =
|
||||
let source_bank: Bank =
|
||||
account_fetcher.fetch(&client.context.mint_info(source).first_bank())?;
|
||||
source_bank.net_borrow_limit_per_window_quote = -1;
|
||||
let mut target_bank: Bank =
|
||||
let target_bank: Bank =
|
||||
account_fetcher.fetch(&client.context.mint_info(target).first_bank())?;
|
||||
target_bank.net_borrow_limit_per_window_quote = -1;
|
||||
|
||||
let source_price = health_cache.token_info(source).unwrap().prices.oracle;
|
||||
|
||||
let amount = health_cache
|
||||
.max_swap_source_for_health_ratio(
|
||||
.max_swap_source_for_health_ratio_ignoring_limits(
|
||||
&account,
|
||||
&source_bank,
|
||||
source_price,
|
||||
|
|
|
@ -5,6 +5,8 @@ use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
|
|||
use mango_v4::health::{FixedOrderAccountRetriever, HealthCache};
|
||||
use mango_v4::state::MangoAccountValue;
|
||||
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub async fn new(
|
||||
context: &MangoGroupContext,
|
||||
account_fetcher: &impl AccountFetcher,
|
||||
|
@ -33,7 +35,9 @@ pub async fn new(
|
|||
begin_serum3: active_token_len * 2 + active_perp_len * 2,
|
||||
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(
|
||||
|
@ -64,5 +68,7 @@ pub fn new_sync(
|
|||
begin_serum3: active_token_len * 2 + active_perp_len * 2,
|
||||
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",
|
||||
"instructions": [
|
||||
{
|
||||
|
@ -602,6 +602,22 @@
|
|||
{
|
||||
"name": "flashLoanSwapFeeRate",
|
||||
"type": "f32"
|
||||
},
|
||||
{
|
||||
"name": "interestCurveScaling",
|
||||
"type": "f32"
|
||||
},
|
||||
{
|
||||
"name": "interestTargetUtilization",
|
||||
"type": "f32"
|
||||
},
|
||||
{
|
||||
"name": "groupInsuranceFund",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "depositLimit",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -936,6 +952,56 @@
|
|||
"type": {
|
||||
"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,
|
||||
"isSigner": false,
|
||||
"relations": [
|
||||
"group",
|
||||
"owner"
|
||||
"group"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -2306,6 +2371,10 @@
|
|||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "oraclePriceBand",
|
||||
"type": "f32"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -2349,6 +2418,12 @@
|
|||
"type": {
|
||||
"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",
|
||||
"accounts": [
|
||||
|
@ -7057,15 +7290,56 @@
|
|||
"type": "f64"
|
||||
},
|
||||
{
|
||||
"name": "depositsInSerum",
|
||||
"type": "i64"
|
||||
"name": "potentialSerumTokens",
|
||||
"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",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
2072
|
||||
1968
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -7990,7 +8264,7 @@
|
|||
"name": "settleFeeFlat",
|
||||
"docs": [
|
||||
"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"
|
||||
},
|
||||
|
@ -8004,7 +8278,8 @@
|
|||
{
|
||||
"name": "settleFeeFractionLowHealth",
|
||||
"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"
|
||||
},
|
||||
|
@ -8163,10 +8438,20 @@
|
|||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
5
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "oraclePriceBand",
|
||||
"docs": [
|
||||
"Limit orders must be <= oracle * (1+band) and >= oracle / (1+band)",
|
||||
"",
|
||||
"Zero value is the default due to migration and disables the limit,",
|
||||
"same as f32::MAX."
|
||||
],
|
||||
"type": "f32"
|
||||
},
|
||||
{
|
||||
"name": "registrationTime",
|
||||
"type": "u64"
|
||||
|
@ -8602,26 +8887,41 @@
|
|||
"type": "f64"
|
||||
},
|
||||
{
|
||||
"name": "baseDepositsReserved",
|
||||
"name": "potentialBaseTokens",
|
||||
"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)",
|
||||
"and they need to be deducted from there when they flow back into the bank",
|
||||
"as real tokens."
|
||||
"The bank still considers these amounts user deposits (see Bank::potential_serum_tokens)",
|
||||
"and that value needs to be updated in conjunction with these numbers.",
|
||||
"",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"name": "quoteDepositsReserved",
|
||||
"name": "potentialQuoteTokens",
|
||||
"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",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
32
|
||||
16
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -10284,6 +10584,9 @@
|
|||
},
|
||||
{
|
||||
"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",
|
||||
"fields": [
|
||||
|
@ -13155,6 +13508,21 @@
|
|||
"code": 6059,
|
||||
"name": "TokenConditionalSwapTypeNotStartable",
|
||||
"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]
|
||||
name = "mango-v4"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
description = "Created with Anchor"
|
||||
edition = "2021"
|
||||
|
||||
|
|
|
@ -31,11 +31,10 @@ pub enum Serum3Side {
|
|||
Ask = 1,
|
||||
}
|
||||
|
||||
// Used for Serum3PlaceOrder v1 and v2
|
||||
#[derive(Accounts)]
|
||||
pub struct Serum3PlaceOrder<'info> {
|
||||
#[account(
|
||||
constraint = group.load()?.is_ix_enabled(IxGate::Serum3PlaceOrder) @ MangoError::IxIsDisabled,
|
||||
)]
|
||||
// ix gate check is done at #4
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(
|
||||
|
|
|
@ -10,7 +10,12 @@ pub struct TokenAddBank<'info> {
|
|||
#[account(
|
||||
has_one = admin,
|
||||
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 admin: Signer<'info>,
|
||||
|
|
|
@ -125,6 +125,12 @@ pub enum MangoError {
|
|||
TokenConditionalSwapTooSmallForStartIncentive,
|
||||
#[msg("token conditional swap type cannot be started")]
|
||||
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 {
|
||||
|
|
|
@ -94,9 +94,10 @@ pub fn compute_health_from_fixed_accounts(
|
|||
account: &MangoAccountRef,
|
||||
health_type: HealthType,
|
||||
ais: &[AccountInfo],
|
||||
now_ts: u64,
|
||||
) -> Result<I80F48> {
|
||||
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
|
||||
|
@ -104,8 +105,9 @@ pub fn compute_health(
|
|||
account: &MangoAccountRef,
|
||||
health_type: HealthType,
|
||||
retriever: &impl AccountRetriever,
|
||||
now_ts: u64,
|
||||
) -> 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?
|
||||
|
@ -1221,8 +1223,9 @@ pub(crate) fn find_token_info_index(infos: &[TokenInfo], token_index: TokenIndex
|
|||
pub fn new_health_cache(
|
||||
account: &MangoAccountRef,
|
||||
retriever: &impl AccountRetriever,
|
||||
now_ts: u64,
|
||||
) -> 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
|
||||
|
@ -1233,20 +1236,22 @@ pub fn new_health_cache(
|
|||
pub fn new_health_cache_skipping_bad_oracles(
|
||||
account: &MangoAccountRef,
|
||||
retriever: &impl AccountRetriever,
|
||||
now_ts: u64,
|
||||
) -> Result<HealthCache> {
|
||||
new_health_cache_impl(account, retriever, true)
|
||||
new_health_cache_impl(account, retriever, now_ts, true)
|
||||
}
|
||||
|
||||
fn new_health_cache_impl(
|
||||
account: &MangoAccountRef,
|
||||
retriever: &impl AccountRetriever,
|
||||
now_ts: u64,
|
||||
// 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
|
||||
// still positive?
|
||||
skip_bad_oracles: bool,
|
||||
) -> Result<HealthCache> {
|
||||
// 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() {
|
||||
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
|
||||
// causes the most unfavorable scaling.
|
||||
let liab_price = prices.liab(HealthType::Init);
|
||||
|
||||
let (maint_asset_weight, maint_liab_weight) = bank.maint_weights(now_ts);
|
||||
|
||||
token_infos.push(TokenInfo {
|
||||
token_index: bank.token_index,
|
||||
maint_asset_weight: bank.maint_asset_weight,
|
||||
maint_asset_weight,
|
||||
init_asset_weight: bank.init_asset_weight,
|
||||
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_scaled_liab_weight: bank.scaled_init_liab_weight(liab_price),
|
||||
prices,
|
||||
|
@ -1282,7 +1290,7 @@ fn new_health_cache_impl(
|
|||
}
|
||||
|
||||
// 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() {
|
||||
let oo = retriever.serum_oo(i, &serum_account.open_orders)?;
|
||||
|
||||
|
@ -1443,7 +1451,7 @@ mod tests {
|
|||
// for bank2/oracle2
|
||||
let health2 = (-10.0 + 3.0) * 5.0 * 1.5;
|
||||
assert!(health_eq(
|
||||
compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(),
|
||||
compute_health(&account.borrow(), HealthType::Init, &retriever, 0).unwrap(),
|
||||
health1 + health2
|
||||
));
|
||||
}
|
||||
|
@ -1454,7 +1462,7 @@ mod tests {
|
|||
borrows: u64,
|
||||
deposit_weight_scale_start_quote: u64,
|
||||
borrow_weight_scale_start_quote: u64,
|
||||
deposits_in_serum: i64,
|
||||
potential_serum_tokens: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -1510,7 +1518,7 @@ mod tests {
|
|||
let bank = bank.data();
|
||||
bank.indexed_deposits = I80F48::from(settings.deposits) / bank.deposit_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 {
|
||||
bank.deposit_weight_scale_start_quote =
|
||||
settings.deposit_weight_scale_start_quote as f64;
|
||||
|
@ -1568,7 +1576,7 @@ mod tests {
|
|||
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
||||
|
||||
assert!(health_eq(
|
||||
compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(),
|
||||
compute_health(&account.borrow(), HealthType::Init, &retriever, 0).unwrap(),
|
||||
testcase.expected_health
|
||||
));
|
||||
}
|
||||
|
@ -1826,7 +1834,7 @@ mod tests {
|
|||
..Default::default()
|
||||
},
|
||||
TestHealth1Case {
|
||||
// 17, deposits_in_serum counts for deposit weight scaling
|
||||
// 17, potential_serum_tokens counts for deposit weight scaling
|
||||
token1: 100,
|
||||
token2: 100,
|
||||
token3: 100,
|
||||
|
@ -1839,13 +1847,13 @@ mod tests {
|
|||
BankSettings {
|
||||
deposits: 100,
|
||||
deposit_weight_scale_start_quote: 100 * 5,
|
||||
deposits_in_serum: 100,
|
||||
potential_serum_tokens: 100,
|
||||
..BankSettings::default()
|
||||
},
|
||||
BankSettings {
|
||||
deposits: 600,
|
||||
deposit_weight_scale_start_quote: 500 * 10,
|
||||
deposits_in_serum: 100,
|
||||
potential_serum_tokens: 100,
|
||||
..BankSettings::default()
|
||||
},
|
||||
],
|
||||
|
|
|
@ -42,7 +42,6 @@ impl HealthCache {
|
|||
|
||||
let mut source_bank = source_bank.clone();
|
||||
source_bank.withdraw_with_fee(&mut source_position, amount, now_ts)?;
|
||||
source_bank.check_net_borrows(source_oracle_price)?;
|
||||
let mut target_bank = target_bank.clone();
|
||||
target_bank.deposit(&mut target_position, target_amount, now_ts)?;
|
||||
|
||||
|
@ -52,7 +51,40 @@ impl HealthCache {
|
|||
Ok(resulting_cache)
|
||||
}
|
||||
|
||||
pub fn max_swap_source_for_health_ratio(
|
||||
fn apply_limits_to_swap(
|
||||
account: &MangoAccountValue,
|
||||
source_bank: &Bank,
|
||||
source_oracle_price: I80F48,
|
||||
target_bank: &Bank,
|
||||
price: I80F48,
|
||||
source_unlimited: I80F48,
|
||||
) -> Result<I80F48> {
|
||||
let source_pos = account
|
||||
.token_position(source_bank.token_index)?
|
||||
.native(source_bank);
|
||||
let target_pos = account
|
||||
.token_position(target_bank.token_index)?
|
||||
.native(target_bank);
|
||||
|
||||
// net borrow limit on source
|
||||
let available_net_borrows = source_bank
|
||||
.remaining_net_borrows_quote(source_oracle_price)
|
||||
.saturating_div(source_oracle_price);
|
||||
let potential_source = source_unlimited
|
||||
.min(available_net_borrows.saturating_add(source_pos.max(I80F48::ZERO)));
|
||||
|
||||
// deposit limit on target
|
||||
let available_deposits = target_bank.remaining_deposits_until_limit();
|
||||
let potential_target_unlimited = potential_source.saturating_mul(price);
|
||||
let potential_target = potential_target_unlimited
|
||||
.min(available_deposits.saturating_add(-target_pos.min(I80F48::ZERO)));
|
||||
|
||||
let source = potential_source.min(potential_target.saturating_div(price));
|
||||
Ok(source)
|
||||
}
|
||||
|
||||
/// Verifies neither the net borrow or deposit limits
|
||||
pub fn max_swap_source_for_health_ratio_ignoring_limits(
|
||||
&self,
|
||||
account: &MangoAccountValue,
|
||||
source_bank: &Bank,
|
||||
|
@ -72,7 +104,7 @@ impl HealthCache {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn max_swap_source_for_health(
|
||||
pub fn max_swap_source_for_health_ratio_with_limits(
|
||||
&self,
|
||||
account: &MangoAccountValue,
|
||||
source_bank: &Bank,
|
||||
|
@ -81,14 +113,23 @@ impl HealthCache {
|
|||
price: I80F48,
|
||||
min_ratio: I80F48,
|
||||
) -> Result<I80F48> {
|
||||
self.max_swap_source_for_health_fn(
|
||||
let source_unlimited = self.max_swap_source_for_health_fn(
|
||||
account,
|
||||
source_bank,
|
||||
source_oracle_price,
|
||||
target_bank,
|
||||
price,
|
||||
min_ratio,
|
||||
|cache| cache.health(HealthType::Init),
|
||||
|cache| cache.health_ratio(HealthType::Init),
|
||||
)?;
|
||||
|
||||
Self::apply_limits_to_swap(
|
||||
account,
|
||||
source_bank,
|
||||
source_oracle_price,
|
||||
target_bank,
|
||||
price,
|
||||
source_unlimited,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -707,7 +748,7 @@ mod tests {
|
|||
assert_eq!(health_cache.health_ratio(HealthType::Init), I80F48::MAX);
|
||||
assert_eq!(
|
||||
health_cache
|
||||
.max_swap_source_for_health_ratio(
|
||||
.max_swap_source_for_health_ratio_with_limits(
|
||||
&account,
|
||||
&banks[0],
|
||||
I80F48::from(1),
|
||||
|
@ -748,7 +789,7 @@ mod tests {
|
|||
|
||||
let swap_price =
|
||||
I80F48::from_num(price_factor) * source_price.oracle / target_price.oracle;
|
||||
let source_amount = c
|
||||
let source_unlimited = c
|
||||
.max_swap_source_for_health_fn(
|
||||
&account,
|
||||
&source_bank,
|
||||
|
@ -759,6 +800,15 @@ mod tests {
|
|||
max_swap_fn,
|
||||
)
|
||||
.unwrap();
|
||||
let source_amount = HealthCache::apply_limits_to_swap(
|
||||
&account,
|
||||
&source_bank,
|
||||
source_price.oracle,
|
||||
&target_bank,
|
||||
swap_price,
|
||||
source_unlimited,
|
||||
)
|
||||
.unwrap();
|
||||
if source_amount == I80F48::MAX {
|
||||
return (f64::MAX, f64::MAX, f64::MAX, f64::MAX);
|
||||
}
|
||||
|
@ -865,10 +915,7 @@ mod tests {
|
|||
}
|
||||
|
||||
// At this unlikely price it's healthy to swap infinitely
|
||||
assert_eq!(
|
||||
find_max_swap(&health_cache, 0, 1, 50.0, 1.5, banks).0,
|
||||
f64::MAX
|
||||
);
|
||||
assert!(find_max_swap(&health_cache, 0, 1, 50.0, 1.5, banks).0 > 1e16);
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -1264,7 +1311,7 @@ mod tests {
|
|||
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
||||
|
||||
assert!(health_eq(
|
||||
compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(),
|
||||
compute_health(&account.borrow(), HealthType::Init, &retriever, 0).unwrap(),
|
||||
// token
|
||||
0.8 * (100.0
|
||||
// perp base
|
||||
|
@ -1353,27 +1400,27 @@ mod tests {
|
|||
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
|
||||
|
||||
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
|
||||
));
|
||||
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
|
||||
));
|
||||
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
|
||||
));
|
||||
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
|
||||
));
|
||||
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)
|
||||
));
|
||||
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)
|
||||
));
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ use crate::state::*;
|
|||
|
||||
use crate::accounts_ix::*;
|
||||
|
||||
use crate::logs::{AccountBuybackFeesWithMngoLog, TokenBalanceLog};
|
||||
use crate::logs::{emit_stack, AccountBuybackFeesWithMngoLog, TokenBalanceLog};
|
||||
|
||||
pub fn account_buyback_fees_with_mngo(
|
||||
ctx: Context<AccountBuybackFeesWithMngo>,
|
||||
|
@ -105,7 +105,7 @@ pub fn account_buyback_fees_with_mngo(
|
|||
);
|
||||
let in_use =
|
||||
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_account: ctx.accounts.account.key(),
|
||||
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)?;
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
token_index: fees_bank.token_index,
|
||||
|
@ -162,7 +162,7 @@ pub fn account_buyback_fees_with_mngo(
|
|||
max_buyback_fees,
|
||||
);
|
||||
|
||||
emit!(AccountBuybackFeesWithMngoLog {
|
||||
emit_stack(AccountBuybackFeesWithMngoLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
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 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 maint_health = health_cache.health(HealthType::Maint);
|
||||
|
||||
let equity = compute_equity(&account.borrow(), &account_retriever)?;
|
||||
|
||||
// Potentially too big for the stack!
|
||||
emit!(MangoAccountData {
|
||||
init_health,
|
||||
maint_health,
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::accounts_zerocopy::*;
|
|||
use crate::error::*;
|
||||
use crate::group_seeds;
|
||||
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 anchor_lang::prelude::*;
|
||||
|
@ -388,7 +388,8 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
|||
|
||||
// Check health before balance adjustments
|
||||
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)?;
|
||||
|
||||
// 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)?;
|
||||
}
|
||||
|
||||
if change_amount > 0 && native_after_change > 0 {
|
||||
bank.check_deposit_and_oo_limit()?;
|
||||
}
|
||||
|
||||
bank.flash_loan_approved_amount = 0;
|
||||
bank.flash_loan_token_account_initial = u64::MAX;
|
||||
|
||||
|
@ -481,7 +486,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
|||
approved_amount: approved_amount_u64,
|
||||
});
|
||||
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
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_account: ctx.accounts.account.key(),
|
||||
flash_loan_type,
|
||||
token_loan_details
|
||||
token_loan_details,
|
||||
});
|
||||
|
||||
// Check health after account position changes
|
||||
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)?;
|
||||
|
||||
// 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::Serum3CancelOrder::discriminator(),
|
||||
crate::instruction::Serum3PlaceOrder::discriminator(),
|
||||
crate::instruction::Serum3PlaceOrderV2::discriminator(),
|
||||
crate::instruction::Serum3SettleFunds::discriminator(),
|
||||
crate::instruction::Serum3SettleFundsV2::discriminator(),
|
||||
];
|
||||
|
@ -87,7 +88,8 @@ pub fn health_region_begin<'key, 'accounts, 'remaining, 'info>(
|
|||
.context("create account retriever")?;
|
||||
|
||||
// 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)?;
|
||||
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 account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &group)
|
||||
.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);
|
||||
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,
|
||||
IxGate::TokenConditionalSwapCreateLinearAuction,
|
||||
);
|
||||
log_if_changed(&group, ix_gate, IxGate::Serum3PlaceOrderV2);
|
||||
|
||||
group.ix_gate = ix_gate;
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::error::MangoError;
|
|||
use crate::state::*;
|
||||
|
||||
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.
|
||||
///
|
||||
|
@ -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;
|
||||
(maker_closed_pnl, taker_closed_pnl)
|
||||
};
|
||||
emit!(FillLogV3 {
|
||||
emit_stack(FillLogV3 {
|
||||
mango_group: group_key,
|
||||
market_index: perp_market_index,
|
||||
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,
|
||||
quantity: fill.quantity,
|
||||
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 => {
|
||||
|
|
|
@ -7,7 +7,7 @@ use crate::state::*;
|
|||
use crate::util::fill_from_str;
|
||||
|
||||
use crate::accounts_ix::*;
|
||||
use crate::logs::PerpMarketMetaDataLog;
|
||||
use crate::logs::{emit_stack, PerpMarketMetaDataLog};
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn perp_create_market(
|
||||
|
@ -111,7 +111,7 @@ pub fn perp_create_market(
|
|||
};
|
||||
orderbook.init();
|
||||
|
||||
emit!(PerpMarketMetaDataLog {
|
||||
emit_stack(PerpMarketMetaDataLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
perp_market: ctx.accounts.perp_market.key(),
|
||||
perp_market_index,
|
||||
|
|
|
@ -4,7 +4,7 @@ use anchor_lang::prelude::*;
|
|||
use fixed::types::I80F48;
|
||||
|
||||
use crate::accounts_ix::*;
|
||||
use crate::logs::PerpMarketMetaDataLog;
|
||||
use crate::logs::{emit_stack, PerpMarketMetaDataLog};
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
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(),
|
||||
perp_market: ctx.accounts.perp_market.key(),
|
||||
perp_market_index: perp_market.perp_market_index,
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::accounts_ix::*;
|
|||
|
||||
use crate::accounts_zerocopy::AccountInfoRef;
|
||||
use crate::error::MangoError;
|
||||
use crate::logs::{emit_perp_balances, PerpForceClosePositionLog};
|
||||
use crate::logs::{emit_perp_balances, emit_stack, PerpForceClosePositionLog};
|
||||
use crate::state::*;
|
||||
use fixed::types::I80F48;
|
||||
|
||||
|
@ -56,7 +56,7 @@ pub fn perp_force_close_position(ctx: Context<PerpForceClosePosition>) -> Result
|
|||
&perp_market,
|
||||
);
|
||||
|
||||
emit!(PerpForceClosePositionLog {
|
||||
emit_stack(PerpForceClosePositionLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
perp_market_index: perp_market.perp_market_index,
|
||||
account_a: ctx.accounts.account_a.key(),
|
||||
|
|
|
@ -8,7 +8,7 @@ use crate::health::*;
|
|||
use crate::state::*;
|
||||
|
||||
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:
|
||||
/// - 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);
|
||||
|
||||
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());
|
||||
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 account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)
|
||||
.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")?
|
||||
};
|
||||
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
|
||||
liqee_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);
|
||||
|
||||
//
|
||||
|
@ -131,7 +131,7 @@ pub fn perp_liq_base_or_positive_pnl(
|
|||
let liqee_token_position = liqee.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_account: ctx.accounts.liqee.key(),
|
||||
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(),
|
||||
});
|
||||
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.liqor.key(),
|
||||
token_index: settle_token_index,
|
||||
|
@ -151,7 +151,7 @@ pub fn perp_liq_base_or_positive_pnl(
|
|||
}
|
||||
|
||||
if base_transfer != 0 || pnl_transfer != 0 {
|
||||
emit!(PerpLiqBaseOrPositivePnlLog {
|
||||
emit_stack(PerpLiqBaseOrPositivePnlLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
perp_market_index: perp_market.perp_market_index,
|
||||
liqor: ctx.accounts.liqor.key(),
|
||||
|
@ -183,8 +183,13 @@ pub fn perp_liq_base_or_positive_pnl(
|
|||
if !liqor.fixed.is_in_health_region() {
|
||||
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)
|
||||
.context("create account retriever end")?;
|
||||
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
|
||||
.context("compute liqor health")?;
|
||||
let liqor_health = compute_health(
|
||||
&liqor.borrow(),
|
||||
HealthType::Init,
|
||||
&account_retriever,
|
||||
now_ts,
|
||||
)
|
||||
.context("compute liqor health")?;
|
||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||
}
|
||||
|
||||
|
@ -675,7 +680,7 @@ mod tests {
|
|||
let retriever =
|
||||
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> {
|
||||
|
|
|
@ -11,10 +11,11 @@ pub fn perp_liq_force_cancel_orders(
|
|||
) -> Result<()> {
|
||||
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 retriever =
|
||||
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()?;
|
||||
|
|
|
@ -10,7 +10,8 @@ use crate::accounts_zerocopy::AccountInfoRef;
|
|||
use crate::error::*;
|
||||
use crate::health::*;
|
||||
use crate::logs::{
|
||||
emit_perp_balances, PerpLiqBankruptcyLog, PerpLiqNegativePnlOrBankruptcyLog, TokenBalanceLog,
|
||||
emit_perp_balances, emit_stack, PerpLiqBankruptcyLog, PerpLiqNegativePnlOrBankruptcyLog,
|
||||
TokenBalanceLog,
|
||||
};
|
||||
use crate::state::*;
|
||||
|
||||
|
@ -71,7 +72,7 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
|
|||
|
||||
let retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &mango_group)
|
||||
.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);
|
||||
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 {
|
||||
let settle_bank = ctx.accounts.settle_bank.load()?;
|
||||
let liqor_token_position = liqor.token_position(settle_token_index)?;
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group,
|
||||
mango_account: ctx.accounts.liqor.key(),
|
||||
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)?;
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group,
|
||||
mango_account: ctx.accounts.liqee.key(),
|
||||
token_index: settle_token_index,
|
||||
|
@ -159,7 +160,7 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
|
|||
if insurance_transfer > 0 {
|
||||
let insurance_bank = ctx.accounts.insurance_bank.load()?;
|
||||
let liqor_token_position = liqor.token_position(insurance_bank.token_index)?;
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group,
|
||||
mango_account: ctx.accounts.liqor.key(),
|
||||
token_index: insurance_bank.token_index,
|
||||
|
@ -197,8 +198,13 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
|
|||
if !liqor.fixed.is_in_health_region() {
|
||||
let account_retriever =
|
||||
ScanningAccountRetriever::new(ctx.remaining_accounts, &mango_group)?;
|
||||
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
|
||||
.context("compute liqor health")?;
|
||||
let liqor_health = compute_health(
|
||||
&liqor.borrow(),
|
||||
HealthType::Init,
|
||||
&account_retriever,
|
||||
now_ts,
|
||||
)
|
||||
.context("compute liqor health")?;
|
||||
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)?;
|
||||
liqee_health_cache.adjust_token_balance(&settle_bank, -settlement)?;
|
||||
|
||||
emit!(PerpLiqNegativePnlOrBankruptcyLog {
|
||||
emit_stack(PerpLiqNegativePnlOrBankruptcyLog {
|
||||
mango_group: group_key,
|
||||
liqee: liqee_key,
|
||||
liqor: liqor_key,
|
||||
|
@ -397,7 +403,7 @@ pub(crate) fn liquidation_action(
|
|||
msg!("socialized loss: {}", socialized_loss);
|
||||
}
|
||||
|
||||
emit!(PerpLiqBankruptcyLog {
|
||||
emit_stack(PerpLiqBankruptcyLog {
|
||||
mango_group: group_key,
|
||||
liqee: liqee_key,
|
||||
liqor: liqor_key,
|
||||
|
@ -509,7 +515,7 @@ mod tests {
|
|||
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -67,8 +67,8 @@ pub fn perp_place_order(
|
|||
let pre_health_opt = if !account.fixed.is_in_health_region() {
|
||||
let retriever =
|
||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let health_cache =
|
||||
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?;
|
||||
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)
|
||||
.context("pre-withdraw init health")?;
|
||||
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||
Some((health_cache, pre_init_health))
|
||||
} else {
|
||||
|
|
|
@ -8,7 +8,7 @@ use crate::health::{compute_health, new_fixed_order_account_retriever, HealthTyp
|
|||
use crate::state::*;
|
||||
|
||||
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<()> {
|
||||
// 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
|
||||
perp_market.fees_settled += settlement;
|
||||
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
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(),
|
||||
});
|
||||
|
||||
emit!(PerpSettleFeesLog {
|
||||
emit_stack(PerpSettleFeesLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
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
|
||||
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);
|
||||
|
||||
msg!("settled fees = {}", settlement);
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::accounts_ix::*;
|
|||
use crate::accounts_zerocopy::*;
|
||||
use crate::error::*;
|
||||
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::*;
|
||||
|
||||
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)?;
|
||||
}
|
||||
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
|
||||
let a_liq_end_health;
|
||||
let a_maint_health;
|
||||
let b_max_settle;
|
||||
|
@ -43,9 +45,9 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
|||
let retriever =
|
||||
ScanningAccountRetriever::new(ctx.remaining_accounts, &ctx.accounts.group.key())
|
||||
.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)?;
|
||||
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_maint_health = a_cache.health(HealthType::Maint);
|
||||
};
|
||||
|
@ -93,7 +95,6 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
|||
);
|
||||
|
||||
// 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);
|
||||
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);
|
||||
|
@ -188,7 +189,7 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
|||
// settled back and forth repeatedly.
|
||||
settle_bank.withdraw_without_fee(b_token_position, settlement, now_ts)?;
|
||||
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account_a.key(),
|
||||
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(),
|
||||
});
|
||||
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account_b.key(),
|
||||
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)?;
|
||||
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_account: ctx.accounts.settler.key(),
|
||||
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());
|
||||
}
|
||||
|
||||
emit!(PerpSettlePnlLog {
|
||||
emit_stack(PerpSettlePnlLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account_a: ctx.accounts.account_a.key(),
|
||||
mango_account_b: ctx.accounts.account_b.key(),
|
||||
|
|
|
@ -2,7 +2,7 @@ use anchor_lang::prelude::*;
|
|||
|
||||
use crate::accounts_ix::*;
|
||||
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::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 open_orders = load_open_orders_ref(oo_ai)?;
|
||||
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
|
||||
emit!(Serum3OpenOrdersBalanceLogV2 {
|
||||
emit_stack(Serum3OpenOrdersBalanceLogV2 {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
market_index: serum_market.market_index,
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::error::*;
|
|||
use crate::state::*;
|
||||
|
||||
use crate::accounts_ix::*;
|
||||
use crate::logs::Serum3OpenOrdersBalanceLogV2;
|
||||
use crate::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2};
|
||||
use crate::serum3_cpi::{load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim};
|
||||
|
||||
pub fn serum3_cancel_order(
|
||||
|
@ -49,7 +49,7 @@ pub fn serum3_cancel_order(
|
|||
let oo_ai = &ctx.accounts.open_orders.as_ref();
|
||||
let open_orders = load_open_orders_ref(oo_ai)?;
|
||||
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
|
||||
emit!(Serum3OpenOrdersBalanceLogV2 {
|
||||
emit_stack(Serum3OpenOrdersBalanceLogV2 {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
market_index: serum_market.market_index,
|
||||
|
|
|
@ -7,6 +7,7 @@ pub fn serum3_edit_market(
|
|||
reduce_only_opt: Option<bool>,
|
||||
force_close_opt: Option<bool>,
|
||||
name_opt: Option<String>,
|
||||
oracle_price_band_opt: Option<f32>,
|
||||
) -> Result<()> {
|
||||
let mut serum3_market = ctx.accounts.market.load_mut()?;
|
||||
|
||||
|
@ -46,6 +47,16 @@ pub fn serum3_edit_market(
|
|||
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 {
|
||||
require!(
|
||||
group.admin == ctx.accounts.admin.key(),
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::error::*;
|
|||
use crate::health::*;
|
||||
use crate::instructions::apply_settle_changes;
|
||||
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::state::*;
|
||||
|
||||
|
@ -57,8 +57,9 @@ pub fn serum3_liq_force_cancel_orders(
|
|||
let mut account = ctx.accounts.account.load_full_mut()?;
|
||||
let retriever =
|
||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let health_cache =
|
||||
new_health_cache(&account.borrow(), &retriever).context("create health cache")?;
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)
|
||||
.context("create health cache")?;
|
||||
|
||||
let liquidatable = account.check_liquidatable(&health_cache)?;
|
||||
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)?;
|
||||
after_oo = OpenOrdersSlim::from_oo(&open_orders);
|
||||
|
||||
emit!(Serum3OpenOrdersBalanceLogV2 {
|
||||
emit_stack(Serum3OpenOrdersBalanceLogV2 {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
market_index: serum_market.market_index,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use crate::accounts_zerocopy::AccountInfoRef;
|
||||
use crate::accounts_zerocopy::*;
|
||||
use crate::error::*;
|
||||
use crate::health::*;
|
||||
use crate::i80f48::ClampToInt;
|
||||
use crate::state::*;
|
||||
|
||||
use crate::accounts_ix::*;
|
||||
use crate::logs::{Serum3OpenOrdersBalanceLogV2, TokenBalanceLog};
|
||||
use crate::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2, TokenBalanceLog};
|
||||
use crate::serum3_cpi::{
|
||||
load_market_state, load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim,
|
||||
};
|
||||
|
@ -25,6 +25,7 @@ pub fn serum3_place_order(
|
|||
order_type: Serum3OrderType,
|
||||
client_order_id: u64,
|
||||
limit: u16,
|
||||
require_v2: bool,
|
||||
) -> Result<()> {
|
||||
// Also required by serum3's place order
|
||||
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 retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let mut health_cache =
|
||||
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?;
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
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_init_health = account.check_health_pre(&health_cache)?;
|
||||
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
|
||||
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.
|
||||
let (_, _, active_index) = account.ensure_token_position(receiver_token_index)?;
|
||||
let group_key = ctx.accounts.group.key();
|
||||
let receiver_bank = retriever
|
||||
.bank_and_oracle(&group_key, active_index, receiver_token_index)?
|
||||
.0;
|
||||
receiver_bank.are_deposits_reduce_only()
|
||||
};
|
||||
let (receiver_bank, oracle) =
|
||||
retriever.bank_and_oracle(&group_key, active_index, receiver_token_index)?;
|
||||
receiver_bank_oracle = oracle;
|
||||
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);
|
||||
|
||||
//
|
||||
// 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
|
||||
//
|
||||
|
@ -184,38 +220,50 @@ pub fn serum3_place_order(
|
|||
if !before_had_bids {
|
||||
// The 0 state means uninitialized/no value
|
||||
serum.highest_placed_bid_inv = 0.0;
|
||||
serum.lowest_placed_bid_inv = 0.0;
|
||||
}
|
||||
if !before_had_asks {
|
||||
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;
|
||||
if new_order_on_book {
|
||||
match side {
|
||||
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 {
|
||||
limit_price
|
||||
} else {
|
||||
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 => {
|
||||
// in base per quote units, to avoid a division in health
|
||||
let limit_price_inv =
|
||||
base_lot_size as f64 / (limit_price_lots as f64 * quote_lot_size as f64);
|
||||
let limit_price_inv = 1.0 / limit_price;
|
||||
serum.highest_placed_bid_inv = if serum.highest_placed_bid_inv == 0.0 {
|
||||
limit_price_inv
|
||||
} else {
|
||||
// the highest bid has the lowest _inv value
|
||||
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_account: ctx.accounts.account.key(),
|
||||
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()?;
|
||||
|
||||
// Enforce min vault to deposits ratio
|
||||
let withdrawn_from_vault = I80F48::from(before_vault - after_vault);
|
||||
let position_native = account
|
||||
// Update the potential token tracking in banks
|
||||
// (for init weight scaling, deposit limit checks)
|
||||
if is_v2_instruction {
|
||||
let mut receiver_bank = receiver_bank_ai.load_mut::<Bank>()?;
|
||||
let (base_bank, quote_bank) = match side {
|
||||
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)?
|
||||
.0
|
||||
.native(&payer_bank);
|
||||
|
||||
// Charge the difference in vault balance to the user's account
|
||||
// (must be done before limit checks like deposit limit)
|
||||
let vault_difference = {
|
||||
apply_vault_difference(
|
||||
ctx.accounts.account.key(),
|
||||
|
@ -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!(
|
||||
!payer_bank.are_borrows_reduce_only(),
|
||||
MangoError::TokenInReduceOnlyMode,
|
||||
"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.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)?;
|
||||
oo_difference.recompute_health_cache_serum3_state(
|
||||
&mut health_cache,
|
||||
|
@ -378,40 +499,29 @@ fn apply_vault_difference(
|
|||
.min(I80F48::ZERO)
|
||||
.abs()
|
||||
.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 market = account.serum3_orders_mut(serum_market_index).unwrap();
|
||||
let borrows_without_fee;
|
||||
let deposits_reserved;
|
||||
if bank.token_index == market.base_token_index {
|
||||
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 {
|
||||
borrows_without_fee = &mut market.quote_borrows_without_fee;
|
||||
deposits_reserved = &mut market.quote_deposits_reserved;
|
||||
} else {
|
||||
return Err(error_msg!(
|
||||
"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;
|
||||
*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
|
||||
if needed_change > 0 {
|
||||
*borrows_without_fee = (*borrows_without_fee).saturating_sub(needed_change.to_num::<u64>());
|
||||
}
|
||||
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: bank.group,
|
||||
mango_account: account_pk,
|
||||
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
|
||||
// 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 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;
|
||||
}
|
||||
update_bank_potential_tokens(serum_orders, base_bank, quote_bank, after_oo);
|
||||
}
|
||||
|
||||
if let Some(health_cache) = health_cache {
|
||||
|
@ -534,6 +629,58 @@ pub fn apply_settle_changes(
|
|||
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<()> {
|
||||
use crate::serum3_cpi;
|
||||
|
||||
|
|
|
@ -6,12 +6,13 @@ use crate::state::*;
|
|||
use crate::util::fill_from_str;
|
||||
|
||||
use crate::accounts_ix::*;
|
||||
use crate::logs::Serum3RegisterMarketLog;
|
||||
use crate::logs::{emit_stack, Serum3RegisterMarketLog};
|
||||
|
||||
pub fn serum3_register_market(
|
||||
ctx: Context<Serum3RegisterMarket>,
|
||||
market_index: Serum3MarketIndex,
|
||||
name: String,
|
||||
oracle_price_band: f32,
|
||||
) -> Result<()> {
|
||||
// TODO: must guard against accidentally using the same market_index twice!
|
||||
|
||||
|
@ -44,6 +45,7 @@ pub fn serum3_register_market(
|
|||
market_index,
|
||||
bump: *ctx.bumps.get("serum_market").ok_or(MangoError::SomeError)?,
|
||||
padding2: Default::default(),
|
||||
oracle_price_band,
|
||||
registration_time: Clock::get()?.unix_timestamp.try_into().unwrap(),
|
||||
reserved: [0; 128],
|
||||
};
|
||||
|
@ -55,7 +57,7 @@ pub fn serum3_register_market(
|
|||
reserved: [0; 38],
|
||||
};
|
||||
|
||||
emit!(Serum3RegisterMarketLog {
|
||||
emit_stack(Serum3RegisterMarketLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
serum_market: ctx.accounts.serum_market.key(),
|
||||
market_index,
|
||||
|
|
|
@ -7,8 +7,9 @@ use crate::state::*;
|
|||
|
||||
use super::apply_settle_changes;
|
||||
use crate::accounts_ix::*;
|
||||
use crate::logs::Serum3OpenOrdersBalanceLogV2;
|
||||
use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanLog};
|
||||
use crate::logs::{
|
||||
emit_stack, LoanOriginationFeeInstruction, Serum3OpenOrdersBalanceLogV2, WithdrawLoanLog,
|
||||
};
|
||||
|
||||
use crate::accounts_zerocopy::AccountInfoRef;
|
||||
|
||||
|
@ -132,7 +133,7 @@ pub fn serum3_settle_funds<'info>(
|
|||
v2.map(|d| d.quote_oracle.as_ref()),
|
||||
)?;
|
||||
|
||||
emit!(Serum3OpenOrdersBalanceLogV2 {
|
||||
emit_stack(Serum3OpenOrdersBalanceLogV2 {
|
||||
mango_group: accounts.group.key(),
|
||||
mango_account: accounts.account.key(),
|
||||
market_index: serum_market.market_index,
|
||||
|
@ -188,14 +189,14 @@ pub fn charge_loan_origination_fees(
|
|||
})
|
||||
.transpose()?;
|
||||
|
||||
emit!(WithdrawLoanLog {
|
||||
emit_stack(WithdrawLoanLog {
|
||||
mango_group: *group_pubkey,
|
||||
mango_account: *account_pubkey,
|
||||
token_index: base_bank.token_index,
|
||||
loan_amount: withdraw_result.loan_amount.to_bits(),
|
||||
loan_origination_fee: withdraw_result.loan_origination_fee.to_bits(),
|
||||
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()?;
|
||||
|
||||
emit!(WithdrawLoanLog {
|
||||
emit_stack(WithdrawLoanLog {
|
||||
mango_group: *group_pubkey,
|
||||
mango_account: *account_pubkey,
|
||||
token_index: quote_bank.token_index,
|
||||
loan_amount: withdraw_result.loan_amount.to_bits(),
|
||||
loan_origination_fee: withdraw_result.loan_origination_fee.to_bits(),
|
||||
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::error::MangoError;
|
||||
use crate::logs::TokenConditionalSwapCancelLog;
|
||||
use crate::logs::{emit_stack, TokenConditionalSwapCancelLog};
|
||||
use crate::state::*;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
@ -31,7 +31,7 @@ pub fn token_conditional_swap_cancel(
|
|||
);
|
||||
*tcs = TokenConditionalSwap::default();
|
||||
|
||||
emit!(TokenConditionalSwapCancelLog {
|
||||
emit_stack(TokenConditionalSwapCancelLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
id: token_conditional_swap_id,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
use crate::accounts_ix::*;
|
||||
use crate::logs::TokenConditionalSwapCreateLogV3;
|
||||
use crate::logs::{emit_stack, TokenConditionalSwapCreateLogV3};
|
||||
use crate::state::*;
|
||||
|
||||
#[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_upper_limit, 0.0);
|
||||
|
||||
emit!(TokenConditionalSwapCreateLogV3 {
|
||||
emit_stack(TokenConditionalSwapCreateLogV3 {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
id,
|
||||
|
|
|
@ -5,8 +5,7 @@ use crate::accounts_ix::*;
|
|||
use crate::error::*;
|
||||
use crate::health::*;
|
||||
use crate::i80f48::ClampToInt;
|
||||
use crate::logs::TokenBalanceLog;
|
||||
use crate::logs::TokenConditionalSwapStartLog;
|
||||
use crate::logs::{emit_stack, TokenBalanceLog, TokenConditionalSwapStartLog};
|
||||
use crate::state::*;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
@ -44,7 +43,7 @@ pub fn token_conditional_swap_start(
|
|||
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")?;
|
||||
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
|
||||
// far exceeds the incentive value.
|
||||
let incentive = (I80F48::from(TCS_START_INCENTIVE) / sell_oracle_price)
|
||||
.min(I80F48::from(tcs.remaining_sell()));
|
||||
// However, the tcs tracking is in u64 units. We need to live with the fact of
|
||||
.min(I80F48::from(tcs.remaining_sell()))
|
||||
// Limiting to remaining deposits is too strict, since this could be a deposit
|
||||
// to deposit transfer, but this is good enough to make the incentive deposit
|
||||
// guaranteed to not exceed the limit.
|
||||
.min(sell_bank.remaining_deposits_until_limit())
|
||||
.max(I80F48::ZERO);
|
||||
// The tcs tracking is in u64 units. We need to live with the fact of
|
||||
// not accounting the incentive fee perfectly.
|
||||
let incentive_native = incentive.clamp_to_u64();
|
||||
|
||||
|
@ -74,28 +78,27 @@ pub fn token_conditional_swap_start(
|
|||
let (liqor_sell_token, liqor_sell_raw_index, _) =
|
||||
liqor.ensure_token_position(sell_token_index)?;
|
||||
|
||||
sell_bank.deposit(liqor_sell_token, incentive, now_ts)?;
|
||||
|
||||
// This withdraw might be a borrow, so can fail due to net borrows or reduce-only
|
||||
let liqee_sell_pre_balance = liqee_sell_token.native(sell_bank);
|
||||
sell_bank.withdraw_with_fee(liqee_sell_token, incentive, now_ts)?;
|
||||
sell_bank.checked_transfer_with_fee(
|
||||
liqee_sell_token,
|
||||
incentive,
|
||||
liqor_sell_token,
|
||||
incentive,
|
||||
now_ts,
|
||||
sell_oracle_price,
|
||||
)?;
|
||||
let liqee_sell_post_balance = liqee_sell_token.native(sell_bank);
|
||||
if liqee_sell_post_balance < 0 {
|
||||
require!(
|
||||
tcs.allow_creating_borrows(),
|
||||
MangoError::TokenConditionalSwapCantPayIncentive
|
||||
);
|
||||
require!(
|
||||
!sell_bank.are_borrows_reduce_only(),
|
||||
MangoError::TokenInReduceOnlyMode
|
||||
);
|
||||
sell_bank.check_net_borrows(sell_oracle_price)?;
|
||||
}
|
||||
|
||||
health_cache
|
||||
.adjust_token_balance(sell_bank, liqee_sell_post_balance - liqee_sell_pre_balance)?;
|
||||
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: *group_pk,
|
||||
mango_account: liqee_key,
|
||||
token_index: sell_token_index,
|
||||
|
@ -103,7 +106,7 @@ pub fn token_conditional_swap_start(
|
|||
deposit_index: sell_bank.deposit_index.to_bits(),
|
||||
borrow_index: sell_bank.borrow_index.to_bits(),
|
||||
});
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: *group_pk,
|
||||
mango_account: liqor_key,
|
||||
token_index: sell_token_index,
|
||||
|
@ -111,7 +114,7 @@ pub fn token_conditional_swap_start(
|
|||
deposit_index: sell_bank.deposit_index.to_bits(),
|
||||
borrow_index: sell_bank.borrow_index.to_bits(),
|
||||
});
|
||||
emit!(TokenConditionalSwapStartLog {
|
||||
emit_stack(TokenConditionalSwapStartLog {
|
||||
mango_group: *group_pk,
|
||||
mango_account: liqee_key,
|
||||
caller: liqor_key,
|
||||
|
|
|
@ -5,10 +5,9 @@ use crate::accounts_ix::*;
|
|||
use crate::error::*;
|
||||
use crate::health::*;
|
||||
use crate::i80f48::ClampToInt;
|
||||
use crate::logs::TokenConditionalSwapCancelLog;
|
||||
use crate::logs::{
|
||||
LoanOriginationFeeInstruction, TokenBalanceLog, TokenConditionalSwapTriggerLogV3,
|
||||
WithdrawLoanLog,
|
||||
emit_stack, LoanOriginationFeeInstruction, TokenBalanceLog, TokenConditionalSwapCancelLog,
|
||||
TokenConditionalSwapTriggerLogV3, WithdrawLoanLog,
|
||||
};
|
||||
use crate::state::*;
|
||||
|
||||
|
@ -73,7 +72,7 @@ pub fn token_conditional_swap_trigger(
|
|||
liqee.token_decrement_dust_deactivate(sell_bank, now_ts, liqee_key)?;
|
||||
|
||||
msg!("TokenConditionalSwap is expired, removing");
|
||||
emit!(TokenConditionalSwapCancelLog {
|
||||
emit_stack(TokenConditionalSwapCancelLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.liqee.key(),
|
||||
id: token_conditional_swap_id,
|
||||
|
@ -87,7 +86,7 @@ pub fn token_conditional_swap_trigger(
|
|||
// changes when the tcs was created.
|
||||
liqee.ensure_token_position(buy_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")?;
|
||||
|
||||
let (buy_bank, buy_token_price, sell_bank_and_oracle_opt) =
|
||||
|
@ -118,8 +117,13 @@ pub fn token_conditional_swap_trigger(
|
|||
);
|
||||
|
||||
// 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)
|
||||
.context("compute liqor health")?;
|
||||
let liqor_health = compute_health(
|
||||
&liqor.borrow(),
|
||||
HealthType::Init,
|
||||
&account_retriever,
|
||||
now_ts,
|
||||
)
|
||||
.context("compute liqor health")?;
|
||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||
|
||||
Ok(())
|
||||
|
@ -290,9 +294,15 @@ fn action(
|
|||
|
||||
let (liqee_buy_token, liqee_buy_raw_index) = liqee.token_position_mut(tcs.buy_token_index)?;
|
||||
let (liqor_buy_token, liqor_buy_raw_index) = liqor.token_position_mut(tcs.buy_token_index)?;
|
||||
buy_bank.deposit(liqee_buy_token, buy_token_amount_i80f48, now_ts)?;
|
||||
let liqor_buy_withdraw =
|
||||
buy_bank.withdraw_with_fee(liqor_buy_token, buy_token_amount_i80f48, now_ts)?;
|
||||
let buy_transfer = buy_bank.checked_transfer_with_fee(
|
||||
liqor_buy_token,
|
||||
buy_token_amount_i80f48,
|
||||
liqee_buy_token,
|
||||
buy_token_amount_i80f48,
|
||||
now_ts,
|
||||
buy_token_price,
|
||||
)?;
|
||||
let liqor_buy_active = buy_transfer.source_is_active;
|
||||
|
||||
let post_liqee_buy_token = liqee_buy_token.native(&buy_bank);
|
||||
let post_liqor_buy_token = liqor_buy_token.native(&buy_bank);
|
||||
|
@ -303,28 +313,18 @@ fn action(
|
|||
liqee.token_position_mut(tcs.sell_token_index)?;
|
||||
let (liqor_sell_token, liqor_sell_raw_index) =
|
||||
liqor.token_position_mut(tcs.sell_token_index)?;
|
||||
let liqor_sell_active = sell_bank.deposit(
|
||||
let sell_transfer = sell_bank.checked_transfer_with_fee(
|
||||
liqee_sell_token,
|
||||
I80F48::from(sell_token_amount_from_liqee),
|
||||
liqor_sell_token,
|
||||
I80F48::from(sell_token_amount_to_liqor),
|
||||
now_ts,
|
||||
sell_token_price,
|
||||
)?;
|
||||
let liqee_sell_withdraw = sell_bank.withdraw_with_fee(
|
||||
liqee_sell_token,
|
||||
I80F48::from(sell_token_amount_from_liqee),
|
||||
now_ts,
|
||||
)?;
|
||||
let liqor_sell_active = sell_transfer.target_is_active;
|
||||
|
||||
sell_bank.collected_fees_native += I80F48::from(maker_fee + taker_fee);
|
||||
|
||||
// Check net borrows on both banks.
|
||||
//
|
||||
// While tcs triggering doesn't cause actual tokens to leave the platform, it can increase the amount
|
||||
// of borrows. For instance, if someone with USDC has a tcs to buy SOL and sell BTC, execution would
|
||||
// create BTC borrows (unless the executor had BTC borrows that get repaid by the execution, but
|
||||
// most executors will work on margin)
|
||||
buy_bank.check_net_borrows(buy_token_price)?;
|
||||
sell_bank.check_net_borrows(sell_token_price)?;
|
||||
|
||||
let post_liqee_sell_token = liqee_sell_token.native(&sell_bank);
|
||||
let post_liqor_sell_token = liqor_sell_token.native(&sell_bank);
|
||||
let liqee_sell_indexed_position = liqee_sell_token.indexed_position;
|
||||
|
@ -332,7 +332,7 @@ fn action(
|
|||
|
||||
// With a scanning account retriever, it's safe to deactivate inactive token positions immediately.
|
||||
// Liqee positions can only be deactivated if the tcs is closed (see below).
|
||||
if !liqor_buy_withdraw.position_is_active {
|
||||
if !liqor_buy_active {
|
||||
liqor.deactivate_token_position_and_log(liqor_buy_raw_index, liqor_key);
|
||||
}
|
||||
if !liqor_sell_active {
|
||||
|
@ -342,7 +342,7 @@ fn action(
|
|||
// Log info
|
||||
|
||||
// liqee buy token
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
mango_account: liqee_key,
|
||||
token_index: tcs.buy_token_index,
|
||||
|
@ -351,7 +351,7 @@ fn action(
|
|||
borrow_index: buy_bank.borrow_index.to_bits(),
|
||||
});
|
||||
// liqee sell token
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
mango_account: liqee_key,
|
||||
token_index: tcs.sell_token_index,
|
||||
|
@ -360,7 +360,7 @@ fn action(
|
|||
borrow_index: sell_bank.borrow_index.to_bits(),
|
||||
});
|
||||
// liqor buy token
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
mango_account: liqor_key,
|
||||
token_index: tcs.buy_token_index,
|
||||
|
@ -369,7 +369,7 @@ fn action(
|
|||
borrow_index: buy_bank.borrow_index.to_bits(),
|
||||
});
|
||||
// liqor sell token
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
mango_account: liqor_key,
|
||||
token_index: tcs.sell_token_index,
|
||||
|
@ -378,24 +378,24 @@ fn action(
|
|||
borrow_index: sell_bank.borrow_index.to_bits(),
|
||||
});
|
||||
|
||||
if liqor_buy_withdraw.has_loan() {
|
||||
emit!(WithdrawLoanLog {
|
||||
if buy_transfer.has_loan() {
|
||||
emit_stack(WithdrawLoanLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
mango_account: liqor_key,
|
||||
token_index: tcs.buy_token_index,
|
||||
loan_amount: liqor_buy_withdraw.loan_amount.to_bits(),
|
||||
loan_origination_fee: liqor_buy_withdraw.loan_origination_fee.to_bits(),
|
||||
loan_amount: buy_transfer.loan_amount.to_bits(),
|
||||
loan_origination_fee: buy_transfer.loan_origination_fee.to_bits(),
|
||||
instruction: LoanOriginationFeeInstruction::TokenConditionalSwapTrigger,
|
||||
price: Some(buy_token_price.to_bits()),
|
||||
});
|
||||
}
|
||||
if liqee_sell_withdraw.has_loan() {
|
||||
emit!(WithdrawLoanLog {
|
||||
if sell_transfer.has_loan() {
|
||||
emit_stack(WithdrawLoanLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
mango_account: liqee_key,
|
||||
token_index: tcs.sell_token_index,
|
||||
loan_amount: liqee_sell_withdraw.loan_amount.to_bits(),
|
||||
loan_origination_fee: liqee_sell_withdraw.loan_origination_fee.to_bits(),
|
||||
loan_amount: sell_transfer.loan_amount.to_bits(),
|
||||
loan_origination_fee: sell_transfer.loan_origination_fee.to_bits(),
|
||||
instruction: LoanOriginationFeeInstruction::TokenConditionalSwapTrigger,
|
||||
price: Some(sell_token_price.to_bits()),
|
||||
});
|
||||
|
@ -483,7 +483,7 @@ fn action(
|
|||
liqee.token_decrement_dust_deactivate(sell_bank, now_ts, liqee_key)?;
|
||||
}
|
||||
|
||||
emit!(TokenConditionalSwapTriggerLogV3 {
|
||||
emit_stack(TokenConditionalSwapTriggerLogV3 {
|
||||
mango_group: liqee.fixed.group,
|
||||
liqee: liqee_key,
|
||||
liqor: liqor_key,
|
||||
|
@ -797,7 +797,7 @@ mod tests {
|
|||
let retriever =
|
||||
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
|
||||
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(
|
||||
&mut self.liqor.borrow_mut(),
|
||||
|
|
|
@ -10,7 +10,7 @@ use crate::health::*;
|
|||
use crate::state::*;
|
||||
|
||||
use crate::accounts_ix::*;
|
||||
use crate::logs::{DepositLog, TokenBalanceLog};
|
||||
use crate::logs::*;
|
||||
|
||||
struct DepositCommon<'a, 'info> {
|
||||
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
|
||||
// to deposit to close borrows or do other fixes even if the oracle is bad.
|
||||
let unsafe_oracle_price = oracle_state_unchecked(
|
||||
let unsafe_oracle_state = oracle_state_unchecked(
|
||||
&AccountInfoRef::borrow(self.oracle.as_ref())?,
|
||||
bank.mint_decimals,
|
||||
)?
|
||||
.price;
|
||||
)?;
|
||||
let unsafe_oracle_price = unsafe_oracle_state.price;
|
||||
|
||||
// If increasing total deposits, check deposit limits
|
||||
if indexed_position > 0 {
|
||||
bank.check_deposit_and_oo_limit()?;
|
||||
}
|
||||
|
||||
// Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms)
|
||||
let amount_usd = (amount_i80f48 * unsafe_oracle_price).to_num::<i64>();
|
||||
account.fixed.net_deposits += amount_usd;
|
||||
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: self.group.key(),
|
||||
mango_account: self.account.key(),
|
||||
token_index,
|
||||
|
@ -114,11 +119,12 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
|
|||
// Health computation
|
||||
//
|
||||
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.
|
||||
// So it's ok to possibly skip token positions for bad oracles and compute a health
|
||||
// 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.
|
||||
// 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());
|
||||
}
|
||||
|
||||
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_account: self.account.key(),
|
||||
signer: self.token_authority.key(),
|
||||
|
@ -171,6 +190,11 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
|
|||
price: unsafe_oracle_price.to_bits(),
|
||||
});
|
||||
|
||||
unsafe {
|
||||
const POS_PTR: *mut usize = 0x300000000 as usize as *mut usize;
|
||||
msg!("heap {}", *POS_PTR);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use crate::error::MangoError;
|
|||
use crate::state::*;
|
||||
|
||||
use crate::accounts_ix::*;
|
||||
use crate::logs::TokenMetaDataLog;
|
||||
use crate::logs::{emit_stack, TokenMetaDataLog};
|
||||
use crate::util::fill_from_str;
|
||||
|
||||
#[allow(unused_variables)]
|
||||
|
@ -42,6 +42,15 @@ pub fn token_edit(
|
|||
token_conditional_swap_taker_fee_rate_opt: Option<f32>,
|
||||
token_conditional_swap_maker_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<()> {
|
||||
let group = ctx.accounts.group.load()?;
|
||||
|
||||
|
@ -339,6 +348,106 @@ pub fn token_edit(
|
|||
bank.flash_loan_swap_fee_rate = fee_rate;
|
||||
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
|
||||
|
@ -359,7 +468,7 @@ pub fn token_edit(
|
|||
let bank = ctx.remaining_accounts.first().unwrap().load_mut::<Bank>()?;
|
||||
bank.verify()?;
|
||||
|
||||
emit!(TokenMetaDataLog {
|
||||
emit_stack(TokenMetaDataLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mint: mint_info.mint.key(),
|
||||
token_index: bank.token_index,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::accounts_ix::*;
|
||||
use crate::error::*;
|
||||
use crate::health::*;
|
||||
use crate::logs::{TokenBalanceLog, TokenForceCloseBorrowsWithTokenLog};
|
||||
use crate::logs::{emit_stack, TokenBalanceLog, TokenForceCloseBorrowsWithTokenLog};
|
||||
use crate::state::*;
|
||||
use anchor_lang::prelude::*;
|
||||
use fixed::types::I80F48;
|
||||
|
@ -131,7 +131,7 @@ pub fn token_force_close_borrows_with_token(
|
|||
);
|
||||
|
||||
// liqee asset
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
mango_account: liqee_key,
|
||||
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(),
|
||||
});
|
||||
// liqee liab
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
mango_account: liqee_key,
|
||||
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(),
|
||||
});
|
||||
// liqor asset
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
mango_account: liqor_key,
|
||||
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(),
|
||||
});
|
||||
// liqor liab
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
mango_account: liqor_key,
|
||||
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(),
|
||||
});
|
||||
|
||||
emit!(TokenForceCloseBorrowsWithTokenLog {
|
||||
emit_stack(TokenForceCloseBorrowsWithTokenLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
liqee: liqee_key,
|
||||
liqor: liqor_key,
|
||||
|
@ -186,7 +186,8 @@ pub fn token_force_close_borrows_with_token(
|
|||
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")?;
|
||||
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
||||
liqee
|
||||
|
@ -211,12 +212,17 @@ pub fn token_force_close_borrows_with_token(
|
|||
// Check liqor's health
|
||||
// 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
|
||||
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &mut account_retriever)
|
||||
.context("compute liqor health")?;
|
||||
let liqor_health = compute_health(
|
||||
&liqor.borrow(),
|
||||
HealthType::Init,
|
||||
&mut account_retriever,
|
||||
now_ts,
|
||||
)
|
||||
.context("compute liqor health")?;
|
||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||
|
||||
// TODO log
|
||||
// emit!(TokenForceCloseBorrowWithToken
|
||||
// emit_stack(TokenForceCloseBorrowWithToken
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -9,7 +9,8 @@ use crate::state::*;
|
|||
|
||||
use crate::accounts_ix::*;
|
||||
use crate::logs::{
|
||||
LoanOriginationFeeInstruction, TokenBalanceLog, TokenLiqBankruptcyLog, WithdrawLoanLog,
|
||||
emit_stack, LoanOriginationFeeInstruction, TokenBalanceLog, TokenLiqBankruptcyLog,
|
||||
WithdrawLoanLog,
|
||||
};
|
||||
|
||||
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 now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
|
||||
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")?;
|
||||
liqee_health_cache.require_after_phase2_liquidation()?;
|
||||
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.
|
||||
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;
|
||||
if insurance_transfer > 0 {
|
||||
// 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)?;
|
||||
|
||||
// liqor quote
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.liqor.key(),
|
||||
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)?;
|
||||
|
||||
// liqor liab
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.liqor.key(),
|
||||
token_index: liab_token_index,
|
||||
|
@ -165,8 +165,12 @@ pub fn token_liq_bankruptcy(
|
|||
|
||||
// Check liqor's health
|
||||
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,
|
||||
)?;
|
||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||
}
|
||||
|
||||
|
@ -174,14 +178,14 @@ pub fn token_liq_bankruptcy(
|
|||
.loan_origination_fee
|
||||
.is_positive()
|
||||
{
|
||||
emit!(WithdrawLoanLog {
|
||||
emit_stack(WithdrawLoanLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.liqor.key(),
|
||||
token_index: liab_token_index,
|
||||
loan_amount: liqor_liab_withdraw_result.loan_amount.to_bits(),
|
||||
loan_origination_fee: liqor_liab_withdraw_result.loan_origination_fee.to_bits(),
|
||||
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
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.liqee.key(),
|
||||
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());
|
||||
}
|
||||
|
||||
emit!(TokenLiqBankruptcyLog {
|
||||
emit_stack(TokenLiqBankruptcyLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
liqee: ctx.accounts.liqee.key(),
|
||||
liqor: ctx.accounts.liqor.key(),
|
||||
|
@ -287,7 +291,7 @@ pub fn token_liq_bankruptcy(
|
|||
insurance_transfer: insurance_transfer_i80f48.to_bits(),
|
||||
socialized_loss: socialized_loss.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(())
|
||||
|
|
|
@ -5,7 +5,8 @@ use crate::accounts_ix::*;
|
|||
use crate::error::*;
|
||||
use crate::health::*;
|
||||
use crate::logs::{
|
||||
LoanOriginationFeeInstruction, TokenBalanceLog, TokenLiqWithTokenLog, WithdrawLoanLog,
|
||||
emit_stack, LoanOriginationFeeInstruction, TokenBalanceLog, TokenLiqWithTokenLog,
|
||||
WithdrawLoanLog,
|
||||
};
|
||||
use crate::state::*;
|
||||
|
||||
|
@ -20,6 +21,7 @@ pub fn token_liq_with_token(
|
|||
require!(asset_token_index != liab_token_index, MangoError::SomeError);
|
||||
let mut account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)
|
||||
.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());
|
||||
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()?;
|
||||
|
||||
// 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")?;
|
||||
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
||||
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 asset_token from liqee to liqor.
|
||||
//
|
||||
let now_ts = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
liquidation_action(
|
||||
&mut account_retriever,
|
||||
liab_token_index,
|
||||
|
@ -69,8 +70,13 @@ pub fn token_liq_with_token(
|
|||
|
||||
// Check liqor's health
|
||||
if !liqor.fixed.is_in_health_region() {
|
||||
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
|
||||
.context("compute liqor health")?;
|
||||
let liqor_health = compute_health(
|
||||
&liqor.borrow(),
|
||||
HealthType::Init,
|
||||
&account_retriever,
|
||||
now_ts,
|
||||
)
|
||||
.context("compute liqor health")?;
|
||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||
}
|
||||
|
||||
|
@ -252,7 +258,7 @@ pub(crate) fn liquidation_action(
|
|||
);
|
||||
|
||||
// liqee asset
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
mango_account: liqee_key,
|
||||
token_index: asset_token_index,
|
||||
|
@ -261,7 +267,7 @@ pub(crate) fn liquidation_action(
|
|||
borrow_index: asset_bank.borrow_index.to_bits(),
|
||||
});
|
||||
// liqee liab
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
mango_account: liqee_key,
|
||||
token_index: liab_token_index,
|
||||
|
@ -270,7 +276,7 @@ pub(crate) fn liquidation_action(
|
|||
borrow_index: liab_bank.borrow_index.to_bits(),
|
||||
});
|
||||
// liqor asset
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
mango_account: liqor_key,
|
||||
token_index: asset_token_index,
|
||||
|
@ -279,7 +285,7 @@ pub(crate) fn liquidation_action(
|
|||
borrow_index: asset_bank.borrow_index.to_bits(),
|
||||
});
|
||||
// liqor liab
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
mango_account: liqor_key,
|
||||
token_index: liab_token_index,
|
||||
|
@ -292,14 +298,14 @@ pub(crate) fn liquidation_action(
|
|||
.loan_origination_fee
|
||||
.is_positive()
|
||||
{
|
||||
emit!(WithdrawLoanLog {
|
||||
emit_stack(WithdrawLoanLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
mango_account: liqor_key,
|
||||
token_index: liab_token_index,
|
||||
loan_amount: liqor_liab_withdraw_result.loan_amount.to_bits(),
|
||||
loan_origination_fee: liqor_liab_withdraw_result.loan_origination_fee.to_bits(),
|
||||
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
|
||||
.maybe_recover_from_being_liquidated(liqee_liq_end_health);
|
||||
|
||||
emit!(TokenLiqWithTokenLog {
|
||||
emit_stack(TokenLiqWithTokenLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
liqee: liqee_key,
|
||||
liqor: liqor_key,
|
||||
|
@ -334,7 +340,7 @@ pub(crate) fn liquidation_action(
|
|||
asset_price: asset_oracle_price.to_bits(),
|
||||
liab_price: liab_oracle_price.to_bits(),
|
||||
bankruptcy: !liqee_health_cache.has_phase2_liquidatable()
|
||||
& liqee_liq_end_health.is_negative()
|
||||
& liqee_liq_end_health.is_negative(),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
@ -446,7 +452,7 @@ mod tests {
|
|||
let retriever =
|
||||
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> {
|
||||
|
@ -468,7 +474,7 @@ mod tests {
|
|||
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
|
||||
|
||||
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);
|
||||
|
||||
liquidation_action(
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::error::*;
|
|||
use crate::state::*;
|
||||
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());
|
||||
|
||||
|
@ -38,6 +38,10 @@ pub fn token_register(
|
|||
token_conditional_swap_taker_fee_rate: f32,
|
||||
token_conditional_swap_maker_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<()> {
|
||||
// Require token 0 to be in the insurance token
|
||||
if token_index == INSURANCE_TOKEN_INDEX {
|
||||
|
@ -107,11 +111,18 @@ pub fn token_register(
|
|||
fees_withdrawn: 0,
|
||||
token_conditional_swap_taker_fee_rate,
|
||||
token_conditional_swap_maker_fee_rate,
|
||||
flash_loan_swap_fee_rate,
|
||||
interest_target_utilization: 0.0, // unused in v0.20.0
|
||||
interest_curve_scaling: 0.0, // unused in v0.20.0
|
||||
deposits_in_serum: 0,
|
||||
reserved: [0; 2072],
|
||||
flash_loan_swap_fee_rate: flash_loan_swap_fee_rate,
|
||||
interest_target_utilization,
|
||||
interest_curve_scaling: interest_curve_scaling.into(),
|
||||
potential_serum_tokens: 0,
|
||||
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) =
|
||||
|
@ -129,7 +140,7 @@ pub fn token_register(
|
|||
*mint_info = MintInfo {
|
||||
group: ctx.accounts.group.key(),
|
||||
token_index,
|
||||
group_insurance_fund: 1,
|
||||
group_insurance_fund: if group_insurance_fund { 1 } else { 0 },
|
||||
padding1: Default::default(),
|
||||
mint: ctx.accounts.mint.key(),
|
||||
banks: Default::default(),
|
||||
|
@ -142,7 +153,7 @@ pub fn token_register(
|
|||
mint_info.banks[0] = ctx.accounts.bank.key();
|
||||
mint_info.vaults[0] = ctx.accounts.vault.key();
|
||||
|
||||
emit!(TokenMetaDataLog {
|
||||
emit_stack(TokenMetaDataLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mint: ctx.accounts.mint.key(),
|
||||
token_index,
|
||||
|
|
|
@ -7,7 +7,7 @@ use crate::instructions::INDEX_START;
|
|||
use crate::state::*;
|
||||
use crate::util::fill_from_str;
|
||||
|
||||
use crate::logs::TokenMetaDataLog;
|
||||
use crate::logs::{emit_stack, TokenMetaDataLog};
|
||||
|
||||
use crate::accounts_ix::*;
|
||||
|
||||
|
@ -45,8 +45,8 @@ pub fn token_register_trustless(
|
|||
vault: ctx.accounts.vault.key(),
|
||||
oracle: ctx.accounts.oracle.key(),
|
||||
oracle_config: OracleConfig {
|
||||
conf_filter: I80F48::from_num(0.10),
|
||||
max_staleness_slots: 10000,
|
||||
conf_filter: I80F48::from_num(1000.0), // effectively disabled
|
||||
max_staleness_slots: -1,
|
||||
reserved: [0; 72],
|
||||
},
|
||||
stable_price_model: StablePriceModel::default(),
|
||||
|
@ -67,11 +67,11 @@ pub fn token_register_trustless(
|
|||
collected_fees_native: I80F48::ZERO,
|
||||
loan_origination_fee_rate: I80F48::from_num(0.0020),
|
||||
loan_fee_rate: I80F48::from_num(0.005),
|
||||
maint_asset_weight: I80F48::from_num(0.75), // 4x leverage
|
||||
init_asset_weight: I80F48::from_num(0.5), // 2x leverage
|
||||
maint_liab_weight: I80F48::from_num(1.25), // 4x leverage
|
||||
init_liab_weight: I80F48::from_num(1.5), // 2x leverage
|
||||
liquidation_fee: I80F48::from_num(0.125),
|
||||
maint_asset_weight: I80F48::from_num(0),
|
||||
init_asset_weight: I80F48::from_num(0),
|
||||
maint_liab_weight: I80F48::from_num(1.4), // 2.5x
|
||||
init_liab_weight: I80F48::from_num(1.8), // 1.25x
|
||||
liquidation_fee: I80F48::from_num(0.2),
|
||||
dust: I80F48::ZERO,
|
||||
flash_loan_token_account_initial: u64::MAX,
|
||||
flash_loan_approved_amount: 0,
|
||||
|
@ -87,17 +87,24 @@ pub fn token_register_trustless(
|
|||
net_borrows_in_window: 0,
|
||||
borrow_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,
|
||||
padding: Default::default(),
|
||||
fees_withdrawn: 0,
|
||||
token_conditional_swap_taker_fee_rate: 0.0005,
|
||||
token_conditional_swap_maker_fee_rate: 0.0005,
|
||||
token_conditional_swap_taker_fee_rate: 0.0,
|
||||
token_conditional_swap_maker_fee_rate: 0.0,
|
||||
flash_loan_swap_fee_rate: 0.0,
|
||||
interest_target_utilization: 0.0, // unused in v0.20.0
|
||||
interest_curve_scaling: 0.0, // unused in v0.20.0
|
||||
deposits_in_serum: 0,
|
||||
reserved: [0; 2072],
|
||||
interest_target_utilization: 0.5,
|
||||
interest_curve_scaling: 4.0,
|
||||
potential_serum_tokens: 0,
|
||||
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) =
|
||||
|
@ -128,7 +135,7 @@ pub fn token_register_trustless(
|
|||
mint_info.banks[0] = ctx.accounts.bank.key();
|
||||
mint_info.vaults[0] = ctx.accounts.vault.key();
|
||||
|
||||
emit!(TokenMetaDataLog {
|
||||
emit_stack(TokenMetaDataLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mint: ctx.accounts.mint.key(),
|
||||
token_index,
|
||||
|
|
|
@ -2,7 +2,7 @@ use anchor_lang::prelude::*;
|
|||
|
||||
use crate::accounts_ix::*;
|
||||
use crate::error::MangoError;
|
||||
use crate::logs::{UpdateIndexLog, UpdateRateLog};
|
||||
use crate::logs::{emit_stack, UpdateIndexLog, UpdateRateLogV2};
|
||||
use crate::state::HOUR;
|
||||
use crate::{
|
||||
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());
|
||||
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(),
|
||||
token_index: mint_info.token_index,
|
||||
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.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
|
||||
{
|
||||
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);
|
||||
|
||||
// update each 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(),
|
||||
token_index: mint_info.token_index,
|
||||
rate0: rate0.to_bits(),
|
||||
util0: some_bank.util0.to_bits(),
|
||||
rate1: rate1.to_bits(),
|
||||
util1: some_bank.util1.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);
|
||||
|
||||
msg!("rate0 {}", rate0);
|
||||
msg!("rate1 {}", rate1);
|
||||
msg!("max_rate {}", max_rate);
|
||||
|
||||
// Apply the new parameters to all banks
|
||||
for ai in ctx.remaining_accounts.iter() {
|
||||
let mut bank = ai.load_mut::<Bank>()?;
|
||||
|
||||
bank.bank_rate_last_updated = now_ts;
|
||||
bank.interest_curve_scaling = scaling;
|
||||
bank.interest_target_utilization = target_util;
|
||||
bank.rate0 = rate0;
|
||||
bank.rate1 = rate1;
|
||||
bank.max_rate = max_rate;
|
||||
|
|
|
@ -7,13 +7,16 @@ use anchor_spl::token;
|
|||
use fixed::types::I80F48;
|
||||
|
||||
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<()> {
|
||||
require_msg!(amount > 0, "withdraw amount must be positive");
|
||||
|
||||
let group = ctx.accounts.group.load()?;
|
||||
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
|
||||
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 retriever =
|
||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let hc_result =
|
||||
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw health cache");
|
||||
let hc_result = new_health_cache(&account.borrow(), &retriever, now_ts)
|
||||
.context("pre-withdraw health cache");
|
||||
if hc_result.is_oracle_error() {
|
||||
// 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)
|
||||
|
@ -102,7 +105,7 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
|||
|
||||
let native_position_after = position.native(&bank);
|
||||
|
||||
emit!(TokenBalanceLog {
|
||||
emit_stack(TokenBalanceLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
token_index,
|
||||
|
@ -132,8 +135,9 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
|||
// Note that this must include the normal pre and post health checks.
|
||||
let retriever =
|
||||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let health_cache = new_health_cache_skipping_bad_oracles(&account.borrow(), &retriever)
|
||||
.context("special post-withdraw health-cache")?;
|
||||
let health_cache =
|
||||
new_health_cache_skipping_bad_oracles(&account.borrow(), &retriever, now_ts)
|
||||
.context("special post-withdraw health-cache")?;
|
||||
let post_init_health = health_cache.health(HealthType::Init);
|
||||
account.check_health_pre_checks(&health_cache, post_init_health)?;
|
||||
account.check_health_post_checks(I80F48::MAX, 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());
|
||||
}
|
||||
|
||||
emit!(WithdrawLog {
|
||||
emit_stack(WithdrawLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.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() {
|
||||
emit!(WithdrawLoanLog {
|
||||
emit_stack(WithdrawLoanLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
token_index,
|
||||
|
|
|
@ -150,6 +150,10 @@ pub mod mango_v4 {
|
|||
token_conditional_swap_taker_fee_rate: f32,
|
||||
token_conditional_swap_maker_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<()> {
|
||||
#[cfg(feature = "enable-gpl")]
|
||||
instructions::token_register(
|
||||
|
@ -177,6 +181,10 @@ pub mod mango_v4 {
|
|||
token_conditional_swap_taker_fee_rate,
|
||||
token_conditional_swap_maker_fee_rate,
|
||||
flash_loan_swap_fee_rate,
|
||||
interest_curve_scaling,
|
||||
interest_target_utilization,
|
||||
group_insurance_fund,
|
||||
deposit_limit,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -221,6 +229,15 @@ pub mod mango_v4 {
|
|||
token_conditional_swap_taker_fee_rate_opt: Option<f32>,
|
||||
token_conditional_swap_maker_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<()> {
|
||||
#[cfg(feature = "enable-gpl")]
|
||||
instructions::token_edit(
|
||||
|
@ -252,6 +269,15 @@ pub mod mango_v4 {
|
|||
token_conditional_swap_taker_fee_rate_opt,
|
||||
token_conditional_swap_maker_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(())
|
||||
}
|
||||
|
@ -540,9 +566,10 @@ pub mod mango_v4 {
|
|||
ctx: Context<Serum3RegisterMarket>,
|
||||
market_index: Serum3MarketIndex,
|
||||
name: String,
|
||||
oracle_price_band: f32,
|
||||
) -> Result<()> {
|
||||
#[cfg(feature = "enable-gpl")]
|
||||
instructions::serum3_register_market(ctx, market_index, name)?;
|
||||
instructions::serum3_register_market(ctx, market_index, name, oracle_price_band)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -551,9 +578,16 @@ pub mod mango_v4 {
|
|||
reduce_only_opt: Option<bool>,
|
||||
force_close_opt: Option<bool>,
|
||||
name_opt: Option<String>,
|
||||
oracle_price_band_opt: Option<f32>,
|
||||
) -> Result<()> {
|
||||
#[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(())
|
||||
}
|
||||
|
||||
|
@ -603,6 +637,36 @@ pub mod mango_v4 {
|
|||
order_type,
|
||||
client_order_id,
|
||||
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(())
|
||||
}
|
||||
|
|
|
@ -5,13 +5,29 @@ use crate::{
|
|||
use anchor_lang::prelude::*;
|
||||
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(
|
||||
mango_group: Pubkey,
|
||||
mango_account: Pubkey,
|
||||
pp: &PerpPosition,
|
||||
pm: &PerpMarket,
|
||||
) {
|
||||
emit!(PerpBalanceLog {
|
||||
emit_stack(PerpBalanceLog {
|
||||
mango_group,
|
||||
mango_account,
|
||||
market_index: pm.perp_market_index,
|
||||
|
@ -300,6 +316,20 @@ pub struct UpdateRateLog {
|
|||
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]
|
||||
pub struct TokenLiqWithTokenLog {
|
||||
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 ONE_BPS: I80F48 = I80F48::from_bits(28147497671);
|
||||
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)]
|
||||
#[derivative(Debug)]
|
||||
|
@ -151,19 +150,29 @@ pub struct Bank {
|
|||
|
||||
/// Target utilization: If actual utilization is higher, scale up interest.
|
||||
/// 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
|
||||
///
|
||||
/// 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
|
||||
// can be negative due to multibank, then it'd need to be balanced in the keeper
|
||||
pub deposits_in_serum: i64,
|
||||
/// Largest amount of tokens that might be added the the bank based on
|
||||
/// serum open order execution.
|
||||
pub potential_serum_tokens: u64,
|
||||
|
||||
#[derivative(Debug = "ignore")]
|
||||
pub reserved: [u8; 2072],
|
||||
pub maint_weight_shift_start: u64,
|
||||
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!(
|
||||
size_of::<Bank>(),
|
||||
|
@ -196,7 +205,11 @@ const_assert_eq!(
|
|||
+ 8
|
||||
+ 4 * 4
|
||||
+ 8 * 2
|
||||
+ 2072
|
||||
+ 8 * 2
|
||||
+ 16 * 3
|
||||
+ 32
|
||||
+ 8
|
||||
+ 1968
|
||||
);
|
||||
const_assert_eq!(size_of::<Bank>(), 3064);
|
||||
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 {
|
||||
pub fn from_existing_bank(
|
||||
existing_bank: &Bank,
|
||||
|
@ -231,7 +257,7 @@ impl Bank {
|
|||
flash_loan_approved_amount: 0,
|
||||
flash_loan_token_account_initial: u64::MAX,
|
||||
net_borrows_in_window: 0,
|
||||
deposits_in_serum: 0,
|
||||
potential_serum_tokens: 0,
|
||||
bump,
|
||||
bank_num,
|
||||
|
||||
|
@ -263,15 +289,15 @@ impl Bank {
|
|||
token_index: existing_bank.token_index,
|
||||
mint_decimals: existing_bank.mint_decimals,
|
||||
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,
|
||||
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,
|
||||
last_net_borrows_window_start_ts: existing_bank.last_net_borrows_window_start_ts,
|
||||
borrow_weight_scale_start_quote: f64::MAX,
|
||||
deposit_weight_scale_start_quote: f64::MAX,
|
||||
reduce_only: 0,
|
||||
force_close: 0,
|
||||
borrow_weight_scale_start_quote: existing_bank.borrow_weight_scale_start_quote,
|
||||
deposit_weight_scale_start_quote: existing_bank.deposit_weight_scale_start_quote,
|
||||
reduce_only: existing_bank.reduce_only,
|
||||
force_close: existing_bank.force_close,
|
||||
padding: [0; 6],
|
||||
token_conditional_swap_taker_fee_rate: existing_bank
|
||||
.token_conditional_swap_taker_fee_rate,
|
||||
|
@ -280,7 +306,14 @@ impl Bank {
|
|||
flash_loan_swap_fee_rate: existing_bank.flash_loan_swap_fee_rate,
|
||||
interest_target_utilization: existing_bank.interest_target_utilization,
|
||||
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.util1, 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_origination_fee_rate, 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_maker_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(())
|
||||
}
|
||||
|
||||
|
@ -337,6 +375,26 @@ impl Bank {
|
|||
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.
|
||||
/// Keep some in reserve to satisfy non-borrow withdraws.
|
||||
pub fn enforce_min_vault_to_deposits_ratio(&self, vault_ai: &AccountInfo) -> Result<()> {
|
||||
|
@ -557,7 +615,7 @@ impl Bank {
|
|||
require_gte!(native_amount, 0);
|
||||
let native_position = position.native(self);
|
||||
|
||||
if native_position.is_positive() {
|
||||
if !native_position.is_negative() {
|
||||
let new_native_position = native_position - native_amount;
|
||||
if !new_native_position.is_negative() {
|
||||
// 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.
|
||||
///
|
||||
/// 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(())
|
||||
}
|
||||
|
||||
pub fn remaining_deposits_until_limit(&self) -> I80F48 {
|
||||
if self.deposit_limit == 0 {
|
||||
return I80F48::MAX;
|
||||
}
|
||||
|
||||
// Assuming slightly higher deposits than true allows the returned value
|
||||
// to be deposit()ed safely into this bank without triggering limits.
|
||||
// (because deposit() will round up in favor of the user)
|
||||
let deposits = self.deposit_index * (self.indexed_deposits + I80F48::DELTA);
|
||||
|
||||
let serum = I80F48::from(self.potential_serum_tokens);
|
||||
let total = deposits + serum;
|
||||
|
||||
I80F48::from(self.deposit_limit) - total
|
||||
}
|
||||
|
||||
pub fn check_deposit_and_oo_limit(&self) -> Result<()> {
|
||||
if self.deposit_limit == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Intentionally does not use remaining_deposits_until_limit(): That function
|
||||
// returns slightly less than the true limit to make sure depositing that amount
|
||||
// will not cause a limit overrun.
|
||||
let deposits = self.native_deposits();
|
||||
let serum = I80F48::from(self.potential_serum_tokens);
|
||||
let total = deposits + serum;
|
||||
let remaining = I80F48::from(self.deposit_limit) - total;
|
||||
if remaining < 0 {
|
||||
return Err(error_msg_typed!(
|
||||
MangoError::BankDepositLimit,
|
||||
"deposit limit exceeded: remaining: {}, total: {}, limit: {}, deposits: {}, serum: {}",
|
||||
remaining,
|
||||
total,
|
||||
self.deposit_limit,
|
||||
deposits,
|
||||
serum,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_cumulative_interest(
|
||||
&self,
|
||||
position: &mut TokenPosition,
|
||||
|
@ -815,6 +974,7 @@ impl Bank {
|
|||
self.util1,
|
||||
self.rate1,
|
||||
self.max_rate,
|
||||
self.interest_curve_scaling,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -828,8 +988,9 @@ impl Bank {
|
|||
util1: I80F48,
|
||||
rate1: I80F48,
|
||||
max_rate: I80F48,
|
||||
scaling: f64,
|
||||
) -> I80F48 {
|
||||
if utilization <= util0 {
|
||||
let v = if utilization <= util0 {
|
||||
let slope = rate0 / util0;
|
||||
slope * utilization
|
||||
} else if utilization <= util1 {
|
||||
|
@ -840,6 +1001,13 @@ impl Bank {
|
|||
let extra_util = utilization - util1;
|
||||
let slope = (max_rate - rate1) / (I80F48::ONE - util1);
|
||||
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
|
||||
pub fn compute_rates(&self) -> (I80F48, I80F48, I80F48) {
|
||||
// interest rate legs 2 and 3 are seen as punitive legs, encouraging utilization to move towards optimal utilization
|
||||
// lets choose util0 as optimal utilization and 0 to utli0 as the leg where we want the utlization to preferably be
|
||||
let optimal_util = self.util0;
|
||||
pub fn update_interest_rate_scaling(&mut self) {
|
||||
// Interest increases above target_util, decreases below
|
||||
let target_util = self.interest_target_utilization as f64;
|
||||
|
||||
// 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
|
||||
// util factor is between -1 (avg util = 0) and +1 (avg util = 100%)
|
||||
let util_factor = if avg_util > optimal_util {
|
||||
(avg_util - optimal_util) / (I80F48::ONE - optimal_util)
|
||||
let util_factor = if avg_util > target_util {
|
||||
(avg_util - target_util) / (1.0 - target_util)
|
||||
} 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
|
||||
// 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)
|
||||
}
|
||||
self.interest_curve_scaling = (self.interest_curve_scaling * adjustment).max(1.0)
|
||||
}
|
||||
|
||||
pub fn oracle_price(
|
||||
|
@ -934,7 +1091,8 @@ impl Bank {
|
|||
if self.deposit_weight_scale_start_quote == f64::MAX {
|
||||
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>();
|
||||
if deposits_quote <= self.deposit_weight_scale_start_quote {
|
||||
self.init_asset_weight
|
||||
|
@ -963,6 +1121,16 @@ impl Bank {
|
|||
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]
|
||||
|
@ -987,9 +1155,108 @@ mod tests {
|
|||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
pub fn change() -> Result<()> {
|
||||
fn bank_change_runner(start: f64, change: i32, is_in_use: bool, use_withdraw: bool) {
|
||||
println!(
|
||||
"testing: in use: {is_in_use}, start: {start}, change: {change}, use_withdraw: {use_withdraw}",
|
||||
);
|
||||
|
||||
let epsilon = I80F48::from_bits(1);
|
||||
|
||||
//
|
||||
// SETUP
|
||||
//
|
||||
|
||||
let mut bank = Bank::zeroed();
|
||||
bank.net_borrow_limit_window_size_ts = 1; // dummy
|
||||
bank.net_borrow_limit_per_window_quote = i64::MAX; // max since we don't want this to interfere
|
||||
bank.deposit_index = I80F48::from_num(100.0);
|
||||
bank.borrow_index = I80F48::from_num(10.0);
|
||||
bank.loan_origination_fee_rate = I80F48::from_num(0.1);
|
||||
let indexed = |v: I80F48, b: &Bank| {
|
||||
if v > 0 {
|
||||
let i = v / b.deposit_index;
|
||||
if i * b.deposit_index < v {
|
||||
i + I80F48::DELTA
|
||||
} else {
|
||||
i
|
||||
}
|
||||
} else {
|
||||
v / b.borrow_index
|
||||
}
|
||||
};
|
||||
|
||||
let mut account = TokenPosition {
|
||||
indexed_position: I80F48::ZERO,
|
||||
token_index: 0,
|
||||
in_use_count: u16::from(is_in_use),
|
||||
cumulative_deposit_interest: 0.0,
|
||||
cumulative_borrow_interest: 0.0,
|
||||
previous_index: I80F48::ZERO,
|
||||
padding: Default::default(),
|
||||
reserved: [0; 128],
|
||||
};
|
||||
|
||||
account.indexed_position = indexed(I80F48::from_num(start), &bank);
|
||||
if start >= 0.0 {
|
||||
bank.indexed_deposits = account.indexed_position;
|
||||
} else {
|
||||
bank.indexed_borrows = -account.indexed_position;
|
||||
}
|
||||
|
||||
// get the rounded start value
|
||||
let start_native = account.native(&bank);
|
||||
|
||||
//
|
||||
// TEST
|
||||
//
|
||||
|
||||
let change = I80F48::from(change);
|
||||
let dummy_now_ts = 1 as u64;
|
||||
let dummy_price = I80F48::ZERO;
|
||||
let is_active = if use_withdraw {
|
||||
bank.withdraw_with_fee(&mut account, change, dummy_now_ts)
|
||||
.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 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_eq!(bank.dust, expected_native);
|
||||
expected_native = I80F48::ZERO;
|
||||
} else {
|
||||
assert!(is_active);
|
||||
assert_eq!(bank.dust, I80F48::ZERO);
|
||||
}
|
||||
if change < 0 && expected_native < 0 {
|
||||
let new_borrow = -(expected_native - min(start_native, I80F48::ZERO));
|
||||
expected_native -= new_borrow * bank.loan_origination_fee_rate;
|
||||
}
|
||||
let expected_indexed = indexed(expected_native, &bank);
|
||||
|
||||
// at most one epsilon error in the resulting indexed value
|
||||
assert!((account.indexed_position - expected_indexed).abs() <= epsilon);
|
||||
|
||||
if account.indexed_position.is_positive() {
|
||||
assert_eq!(bank.indexed_deposits, account.indexed_position);
|
||||
assert_eq!(bank.indexed_borrows, I80F48::ZERO);
|
||||
} else {
|
||||
assert_eq!(bank.indexed_deposits, I80F48::ZERO);
|
||||
assert_eq!(bank.indexed_borrows, -account.indexed_position);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn bank_change() -> Result<()> {
|
||||
let cases = [
|
||||
(-10.1, 1),
|
||||
(-10.1, 10),
|
||||
|
@ -1013,95 +1280,167 @@ mod tests {
|
|||
(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 {
|
||||
println!(
|
||||
"testing: in use: {}, start: {}, change: {}",
|
||||
is_in_use, start, change
|
||||
);
|
||||
|
||||
//
|
||||
// SETUP
|
||||
//
|
||||
|
||||
let mut bank = Bank::zeroed();
|
||||
bank.net_borrow_limit_window_size_ts = 1; // dummy
|
||||
bank.net_borrow_limit_per_window_quote = i64::MAX; // max since we don't want this to interfere
|
||||
bank.deposit_index = I80F48::from_num(100.0);
|
||||
bank.borrow_index = I80F48::from_num(10.0);
|
||||
bank.loan_origination_fee_rate = I80F48::from_num(0.1);
|
||||
let indexed = |v: I80F48, b: &Bank| {
|
||||
if v > 0 {
|
||||
v / b.deposit_index
|
||||
} else {
|
||||
v / b.borrow_index
|
||||
}
|
||||
};
|
||||
|
||||
let mut account = TokenPosition {
|
||||
indexed_position: I80F48::ZERO,
|
||||
token_index: 0,
|
||||
in_use_count: u16::from(is_in_use),
|
||||
cumulative_deposit_interest: 0.0,
|
||||
cumulative_borrow_interest: 0.0,
|
||||
previous_index: I80F48::ZERO,
|
||||
padding: Default::default(),
|
||||
reserved: [0; 128],
|
||||
};
|
||||
|
||||
account.indexed_position = indexed(I80F48::from_num(start), &bank);
|
||||
if start >= 0.0 {
|
||||
bank.indexed_deposits = account.indexed_position;
|
||||
} else {
|
||||
bank.indexed_borrows = -account.indexed_position;
|
||||
}
|
||||
|
||||
// get the rounded start value
|
||||
let start_native = account.native(&bank);
|
||||
|
||||
//
|
||||
// TEST
|
||||
//
|
||||
|
||||
let change = I80F48::from(change);
|
||||
let dummy_now_ts = 1 as u64;
|
||||
let dummy_price = I80F48::ZERO;
|
||||
let is_active = bank
|
||||
.change_with_fee(&mut account, change, dummy_now_ts)?
|
||||
.position_is_active;
|
||||
|
||||
let mut expected_native = start_native + change;
|
||||
if expected_native >= 0.0 && expected_native < 1.0 && !is_in_use {
|
||||
assert!(!is_active);
|
||||
assert_eq!(bank.dust, expected_native);
|
||||
expected_native = I80F48::ZERO;
|
||||
} else {
|
||||
assert!(is_active);
|
||||
assert_eq!(bank.dust, I80F48::ZERO);
|
||||
}
|
||||
if change < 0 && expected_native < 0 {
|
||||
let new_borrow = -(expected_native - min(start_native, I80F48::ZERO));
|
||||
expected_native -= new_borrow * bank.loan_origination_fee_rate;
|
||||
}
|
||||
let expected_indexed = indexed(expected_native, &bank);
|
||||
|
||||
// at most one epsilon error in the resulting indexed value
|
||||
assert!((account.indexed_position - expected_indexed).abs() <= epsilon);
|
||||
|
||||
if account.indexed_position.is_positive() {
|
||||
assert_eq!(bank.indexed_deposits, account.indexed_position);
|
||||
assert_eq!(bank.indexed_borrows, I80F48::ZERO);
|
||||
} else {
|
||||
assert_eq!(bank.indexed_deposits, I80F48::ZERO);
|
||||
assert_eq!(bank.indexed_borrows, -account.indexed_position);
|
||||
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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bank_transfer() {
|
||||
//
|
||||
// SETUP
|
||||
//
|
||||
|
||||
let mut bank_proto = Bank::zeroed();
|
||||
bank_proto.net_borrow_limit_window_size_ts = 1; // dummy
|
||||
bank_proto.net_borrow_limit_per_window_quote = i64::MAX; // max since we don't want this to interfere
|
||||
bank_proto.deposit_index = I80F48::from(1_234_567);
|
||||
bank_proto.borrow_index = I80F48::from(1_234_567);
|
||||
bank_proto.loan_origination_fee_rate = I80F48::from_num(0.1);
|
||||
|
||||
let account_proto = TokenPosition {
|
||||
indexed_position: I80F48::ZERO,
|
||||
token_index: 0,
|
||||
in_use_count: 1,
|
||||
cumulative_deposit_interest: 0.0,
|
||||
cumulative_borrow_interest: 0.0,
|
||||
previous_index: I80F48::ZERO,
|
||||
padding: Default::default(),
|
||||
reserved: [0; 128],
|
||||
};
|
||||
|
||||
//
|
||||
// TESTS
|
||||
//
|
||||
|
||||
// simple transfer
|
||||
{
|
||||
let mut bank = bank_proto.clone();
|
||||
let mut a1 = account_proto.clone();
|
||||
let mut a2 = account_proto.clone();
|
||||
|
||||
let amount = I80F48::from(100);
|
||||
bank.deposit(&mut a1, amount, 0).unwrap();
|
||||
let damount = a1.native(&bank);
|
||||
let r = bank
|
||||
.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE)
|
||||
.unwrap();
|
||||
assert_eq!(a2.native(&bank), damount);
|
||||
assert!(r.source_is_active);
|
||||
assert!(r.target_is_active);
|
||||
}
|
||||
|
||||
// borrow limits
|
||||
{
|
||||
let mut bank = bank_proto.clone();
|
||||
bank.net_borrow_limit_per_window_quote = 100;
|
||||
bank.loan_origination_fee_rate = I80F48::ZERO;
|
||||
let mut a1 = account_proto.clone();
|
||||
let mut a2 = account_proto.clone();
|
||||
|
||||
{
|
||||
let mut b = bank.clone();
|
||||
let amount = I80F48::from(101);
|
||||
assert!(b
|
||||
.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
{
|
||||
let mut b = bank.clone();
|
||||
let amount = I80F48::from(100);
|
||||
b.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
{
|
||||
let mut b = bank.clone();
|
||||
let amount = b.remaining_net_borrows_quote(I80F48::ONE);
|
||||
b.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// deposit limits
|
||||
{
|
||||
let mut bank = bank_proto.clone();
|
||||
bank.deposit_limit = 100;
|
||||
let mut a1 = account_proto.clone();
|
||||
let mut a2 = account_proto.clone();
|
||||
|
||||
{
|
||||
let mut b = bank.clone();
|
||||
let amount = I80F48::from(101);
|
||||
assert!(b
|
||||
.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
{
|
||||
// still bad because deposit() adds DELTA more than requested
|
||||
let mut b = bank.clone();
|
||||
let amount = I80F48::from(100);
|
||||
assert!(b
|
||||
.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
{
|
||||
let mut b = bank.clone();
|
||||
let amount = I80F48::from_num(99.999);
|
||||
b.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
{
|
||||
let mut b = bank.clone();
|
||||
let amount = b.remaining_deposits_until_limit();
|
||||
b.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// reducing transfer while limits exceeded
|
||||
{
|
||||
let mut bank = bank_proto.clone();
|
||||
bank.loan_origination_fee_rate = I80F48::ZERO;
|
||||
|
||||
let amount = I80F48::from(100);
|
||||
let mut a1 = account_proto.clone();
|
||||
bank.deposit(&mut a1, amount, 0).unwrap();
|
||||
let mut a2 = account_proto.clone();
|
||||
bank.withdraw_with_fee(&mut a2, amount, 0).unwrap();
|
||||
|
||||
bank.net_borrow_limit_per_window_quote = 100;
|
||||
bank.net_borrows_in_window = 200;
|
||||
bank.deposit_limit = 100;
|
||||
bank.potential_serum_tokens = 200;
|
||||
|
||||
let half = I80F48::from(50);
|
||||
bank.checked_transfer_with_fee(&mut a1, half, &mut a2, half, 0, I80F48::ONE)
|
||||
.unwrap();
|
||||
bank.checked_transfer_with_fee(&mut a1, half, &mut a2, half, 0, I80F48::ONE)
|
||||
.unwrap();
|
||||
assert!(bank
|
||||
.checked_transfer_with_fee(&mut a1, half, &mut a2, half, 0, I80F48::ONE)
|
||||
.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_new_avg_utilization() {
|
||||
let mut bank = Bank::zeroed();
|
||||
|
@ -1185,4 +1524,48 @@ mod tests {
|
|||
|
||||
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,
|
||||
TokenConditionalSwapCreatePremiumAuction = 69,
|
||||
TokenConditionalSwapCreateLinearAuction = 70,
|
||||
Serum3PlaceOrderV2 = 71,
|
||||
// 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::health::{HealthCache, HealthType};
|
||||
use crate::logs::{DeactivatePerpPositionLog, DeactivateTokenPositionLog};
|
||||
use crate::logs::{emit_stack, DeactivatePerpPositionLog, DeactivateTokenPositionLog};
|
||||
use crate::util;
|
||||
|
||||
use super::BookSideOrderTree;
|
||||
|
@ -1014,7 +1014,7 @@ impl<
|
|||
let mango_group = self.fixed().group;
|
||||
let token_position = self.token_position_mut_by_raw_index(raw_index);
|
||||
assert!(token_position.in_use_count == 0);
|
||||
emit!(DeactivateTokenPositionLog {
|
||||
emit_stack(DeactivateTokenPositionLog {
|
||||
mango_group,
|
||||
mango_account: mango_account_pubkey,
|
||||
token_index: token_position.token_index,
|
||||
|
@ -1168,7 +1168,7 @@ impl<
|
|||
let mango_group = self.fixed().group;
|
||||
let perp_position = self.perp_position_mut(perp_market_index)?;
|
||||
|
||||
emit!(DeactivatePerpPositionLog {
|
||||
emit_stack(DeactivatePerpPositionLog {
|
||||
mango_group,
|
||||
mango_account: mango_account_pubkey,
|
||||
market_index: perp_market_index,
|
||||
|
|
|
@ -145,20 +145,28 @@ pub struct Serum3Orders {
|
|||
pub highest_placed_bid_inv: 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)
|
||||
/// and they need to be deducted from there when they flow back into the bank
|
||||
/// as real tokens.
|
||||
pub base_deposits_reserved: u64,
|
||||
pub quote_deposits_reserved: u64,
|
||||
/// The bank still considers these amounts user deposits (see Bank::potential_serum_tokens)
|
||||
/// and that value needs to be updated in conjunction with these numbers.
|
||||
///
|
||||
/// 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)
|
||||
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")]
|
||||
pub reserved: [u8; 32],
|
||||
pub reserved: [u8; 16],
|
||||
}
|
||||
const_assert_eq!(
|
||||
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>() % 8, 0);
|
||||
|
@ -185,9 +193,11 @@ impl Default for Serum3Orders {
|
|||
quote_borrows_without_fee: 0,
|
||||
highest_placed_bid_inv: 0.0,
|
||||
lowest_placed_ask: 0.0,
|
||||
base_deposits_reserved: 0,
|
||||
quote_deposits_reserved: 0,
|
||||
reserved: [0; 32],
|
||||
potential_base_tokens: 0,
|
||||
potential_quote_tokens: 0,
|
||||
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::state::MangoAccountRefMut;
|
||||
use crate::{
|
||||
error::*,
|
||||
state::{orderbook::bookside::*, EventQueue, PerpMarket},
|
||||
};
|
||||
use crate::error::*;
|
||||
use crate::logs::{emit_stack, FilledPerpOrderLog, PerpTakerTradeLog};
|
||||
use crate::state::{orderbook::bookside::*, EventQueue, MangoAccountRefMut, PerpMarket};
|
||||
use anchor_lang::prelude::*;
|
||||
use bytemuck::cast;
|
||||
use fixed::types::I80F48;
|
||||
|
@ -205,7 +202,7 @@ impl<'a> Orderbook<'a> {
|
|||
event_queue.push_back(cast(fill)).unwrap();
|
||||
limit -= 1;
|
||||
|
||||
emit!(FilledPerpOrderLog {
|
||||
emit_stack(FilledPerpOrderLog {
|
||||
mango_group: market.group.key(),
|
||||
perp_market_index: market.perp_market_index,
|
||||
seq_num,
|
||||
|
@ -226,7 +223,7 @@ impl<'a> Orderbook<'a> {
|
|||
mango_account,
|
||||
total_quote_lots_taken - decremented_quote_lots,
|
||||
)?;
|
||||
emit!(PerpTakerTradeLog {
|
||||
emit_stack(PerpTakerTradeLog {
|
||||
mango_group: market.group.key(),
|
||||
mango_account: *mango_account_pk,
|
||||
perp_market_index: market.perp_market_index,
|
||||
|
|
|
@ -8,7 +8,7 @@ use static_assertions::const_assert_eq;
|
|||
|
||||
use crate::accounts_zerocopy::KeyedAccountReader;
|
||||
use crate::error::MangoError;
|
||||
use crate::logs::PerpUpdateFundingLogV2;
|
||||
use crate::logs::{emit_stack, PerpUpdateFundingLogV2};
|
||||
use crate::state::orderbook::Side;
|
||||
use crate::state::{oracle, TokenIndex};
|
||||
use crate::util;
|
||||
|
@ -343,7 +343,7 @@ impl PerpMarket {
|
|||
self.stable_price_model
|
||||
.update(now_ts, oracle_price.to_num());
|
||||
|
||||
emit!(PerpUpdateFundingLogV2 {
|
||||
emit_stack(PerpUpdateFundingLogV2 {
|
||||
mango_group: self.group,
|
||||
market_index: self.perp_market_index,
|
||||
long_funding: self.long_funding.to_bits(),
|
||||
|
|
|
@ -26,7 +26,13 @@ pub struct Serum3Market {
|
|||
|
||||
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,
|
||||
|
||||
|
@ -34,7 +40,7 @@ pub struct Serum3Market {
|
|||
}
|
||||
const_assert_eq!(
|
||||
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>() % 8, 0);
|
||||
|
@ -53,6 +59,14 @@ impl Serum3Market {
|
|||
pub fn is_force_close(&self) -> bool {
|
||||
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)]
|
||||
|
|
|
@ -414,3 +414,270 @@ async fn test_account_size_migration() -> Result<(), TransportError> {
|
|||
|
||||
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(())
|
||||
}
|
||||
|
||||
#[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
|
||||
#[tokio::test]
|
||||
async fn test_health_compute_serum() -> Result<(), TransportError> {
|
||||
|
|
|
@ -613,3 +613,117 @@ async fn test_flash_loan_creates_ata_accounts() -> Result<(), BanksClientError>
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_margin_trade_deposit_limit() -> Result<(), BanksClientError> {
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(100_000);
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
let mints = &context.mints[0..2];
|
||||
let payer_mint0_account = context.users[1].token_accounts[0];
|
||||
|
||||
//
|
||||
// SETUP: Create a group, account, register a token (mint0)
|
||||
//
|
||||
|
||||
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints: mints.to_vec(),
|
||||
..GroupWithTokensConfig::default()
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
let bank = tokens[0].bank;
|
||||
|
||||
//
|
||||
// SETUP: deposit limit
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
TokenEdit {
|
||||
group,
|
||||
admin,
|
||||
mint: tokens[0].mint.pubkey,
|
||||
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
|
||||
//
|
||||
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;
|
||||
|
||||
let native0 = account_position(solana, account, base_token.bank).await;
|
||||
let native1 = account_position(solana, account, quote_token.bank).await;
|
||||
assert_eq!(native0, 1000);
|
||||
assert_eq!(native1, 900);
|
||||
assert_eq!(native1, 910);
|
||||
|
||||
let account_data = get_mango_account(solana, account).await;
|
||||
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();
|
||||
assert_eq!(serum_orders.base_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.quote_deposits_reserved, 100);
|
||||
assert_eq!(serum_orders.potential_base_tokens, 100);
|
||||
assert_eq!(serum_orders.potential_quote_tokens, 90);
|
||||
|
||||
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;
|
||||
assert_eq!(quote_bank.deposits_in_serum, 100);
|
||||
assert_eq!(quote_bank.potential_serum_tokens, 90);
|
||||
|
||||
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();
|
||||
assert_eq!(serum_orders.base_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.quote_deposits_reserved, 0);
|
||||
assert_eq!(serum_orders.potential_base_tokens, 0);
|
||||
assert_eq!(serum_orders.potential_quote_tokens, 0);
|
||||
|
||||
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;
|
||||
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
|
||||
context
|
||||
|
@ -1150,89 +1150,72 @@ async fn test_serum_track_bid_ask() -> Result<(), TransportError> {
|
|||
// TEST: highest bid/lowest ask updating
|
||||
//
|
||||
|
||||
assert_eq!(
|
||||
order_placer
|
||||
.mango_serum_orders()
|
||||
.await
|
||||
.highest_placed_bid_inv,
|
||||
0.0
|
||||
);
|
||||
assert_eq!(
|
||||
order_placer.mango_serum_orders().await.lowest_placed_ask,
|
||||
0.0
|
||||
);
|
||||
let srm = order_placer.mango_serum_orders().await;
|
||||
assert_eq!(srm.highest_placed_bid_inv, 0.0);
|
||||
assert_eq!(srm.lowest_placed_bid_inv, 0.0);
|
||||
assert_eq!(srm.highest_placed_ask, 0.0);
|
||||
assert_eq!(srm.lowest_placed_ask, 0.0);
|
||||
|
||||
order_placer.bid_maker(10.0, 100).await.unwrap();
|
||||
assert_eq!(
|
||||
order_placer
|
||||
.mango_serum_orders()
|
||||
.await
|
||||
.highest_placed_bid_inv,
|
||||
1.0 / 10.0
|
||||
);
|
||||
|
||||
let srm = order_placer.mango_serum_orders().await;
|
||||
assert_eq!(srm.highest_placed_bid_inv, 1.0 / 10.0);
|
||||
assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 10.0);
|
||||
assert_eq!(srm.highest_placed_ask, 0.0);
|
||||
assert_eq!(srm.lowest_placed_ask, 0.0);
|
||||
|
||||
order_placer.bid_maker(9.0, 100).await.unwrap();
|
||||
assert_eq!(
|
||||
order_placer
|
||||
.mango_serum_orders()
|
||||
.await
|
||||
.highest_placed_bid_inv,
|
||||
1.0 / 10.0
|
||||
);
|
||||
|
||||
let srm = order_placer.mango_serum_orders().await;
|
||||
assert_eq!(srm.highest_placed_bid_inv, 1.0 / 10.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.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!(
|
||||
order_placer.mango_serum_orders().await.lowest_placed_ask,
|
||||
0.0
|
||||
);
|
||||
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, 0.0);
|
||||
assert_eq!(srm.lowest_placed_ask, 0.0);
|
||||
|
||||
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!(
|
||||
order_placer
|
||||
.mango_serum_orders()
|
||||
.await
|
||||
.highest_placed_bid_inv,
|
||||
1.0 / 11.0
|
||||
);
|
||||
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, 20.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
|
||||
//
|
||||
|
||||
order_placer.cancel_all().await;
|
||||
assert_eq!(
|
||||
order_placer.mango_serum_orders().await.lowest_placed_ask,
|
||||
19.0
|
||||
);
|
||||
assert_eq!(
|
||||
order_placer
|
||||
.mango_serum_orders()
|
||||
.await
|
||||
.highest_placed_bid_inv,
|
||||
1.0 / 11.0
|
||||
);
|
||||
|
||||
// no immediate change
|
||||
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);
|
||||
|
||||
// Process events such that the OutEvent deactivates the closed order on open_orders
|
||||
context
|
||||
|
@ -1242,36 +1225,36 @@ async fn test_serum_track_bid_ask() -> Result<(), TransportError> {
|
|||
|
||||
// takes new value for bid, resets ask
|
||||
order_placer.bid_maker(1.0, 100).await.unwrap();
|
||||
assert_eq!(
|
||||
order_placer.mango_serum_orders().await.lowest_placed_ask,
|
||||
0.0
|
||||
);
|
||||
assert_eq!(
|
||||
order_placer
|
||||
.mango_serum_orders()
|
||||
.await
|
||||
.highest_placed_bid_inv,
|
||||
1.0
|
||||
);
|
||||
|
||||
let srm = order_placer.mango_serum_orders().await;
|
||||
assert_eq!(srm.highest_placed_bid_inv, 1.0);
|
||||
assert_eq!(srm.lowest_placed_bid_inv, 1.0);
|
||||
assert_eq!(srm.highest_placed_ask, 0.0);
|
||||
assert_eq!(srm.lowest_placed_ask, 0.0);
|
||||
|
||||
//
|
||||
// TEST: can reset even when there's still an order on the other side
|
||||
//
|
||||
let (oid, _) = order_placer.ask(10.0, 100).await.unwrap();
|
||||
assert_eq!(
|
||||
order_placer.mango_serum_orders().await.lowest_placed_ask,
|
||||
10.0
|
||||
);
|
||||
|
||||
let srm = order_placer.mango_serum_orders().await;
|
||||
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;
|
||||
context
|
||||
.serum
|
||||
.consume_spot_events(&serum_market_cookie, &[order_placer.open_orders])
|
||||
.await;
|
||||
order_placer.ask(9.0, 100).await.unwrap();
|
||||
assert_eq!(
|
||||
order_placer.mango_serum_orders().await.lowest_placed_ask,
|
||||
9.0
|
||||
);
|
||||
|
||||
let srm = order_placer.mango_serum_orders().await;
|
||||
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(())
|
||||
}
|
||||
|
@ -1305,10 +1288,10 @@ async fn test_serum_track_reserved_deposits() -> Result<(), TransportError> {
|
|||
let base_bank = solana.get_account::<Bank>(base_bank).await;
|
||||
let quote_bank = solana.get_account::<Bank>(quote_bank).await;
|
||||
(
|
||||
orders.base_deposits_reserved,
|
||||
base_bank.deposits_in_serum,
|
||||
orders.quote_deposits_reserved,
|
||||
quote_bank.deposits_in_serum,
|
||||
orders.potential_base_tokens,
|
||||
base_bank.potential_serum_tokens,
|
||||
orders.potential_quote_tokens,
|
||||
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.ask(1.2, 2000).await.unwrap();
|
||||
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
|
||||
// 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],
|
||||
)
|
||||
.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;
|
||||
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();
|
||||
context
|
||||
|
@ -1345,16 +1336,19 @@ async fn test_serum_track_reserved_deposits() -> Result<(), TransportError> {
|
|||
&[order_placer.open_orders, order_placer2.open_orders],
|
||||
)
|
||||
.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;
|
||||
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
|
||||
//
|
||||
|
||||
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(())
|
||||
}
|
||||
|
@ -1481,6 +1475,239 @@ async fn test_serum_compute() -> Result<(), TransportError> {
|
|||
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 {
|
||||
group_with_tokens: GroupWithTokens,
|
||||
serum_market_cookie: SpotMarketCookie,
|
||||
|
@ -1491,6 +1718,14 @@ struct CommonSetup {
|
|||
}
|
||||
|
||||
async fn common_setup(context: &TestContext, deposit_amount: u64) -> CommonSetup {
|
||||
common_setup2(context, deposit_amount, 10000000).await
|
||||
}
|
||||
|
||||
async fn common_setup2(
|
||||
context: &TestContext,
|
||||
deposit_amount: u64,
|
||||
vault_funding: u64,
|
||||
) -> CommonSetup {
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
|
@ -1565,17 +1800,19 @@ async fn common_setup(context: &TestContext, deposit_amount: u64) -> CommonSetup
|
|||
)
|
||||
.await;
|
||||
// to have enough funds in the vaults
|
||||
create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
3,
|
||||
&context.users[1],
|
||||
mints,
|
||||
10000000,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
if vault_funding > 0 {
|
||||
create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
3,
|
||||
&context.users[1],
|
||||
mints,
|
||||
10000000,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let open_orders = send_tx(
|
||||
solana,
|
||||
|
|
|
@ -1013,3 +1013,145 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_token_conditional_swap_deposit_limit() -> Result<(), TransportError> {
|
||||
pub use utils::assert_equal_f64_f64 as assert_equal_f_f;
|
||||
|
||||
let context = TestContext::new().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
let mints = &context.mints[0..2];
|
||||
|
||||
//
|
||||
// SETUP: Create a group, account, tokens
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints: mints.to_vec(),
|
||||
..mango_setup::GroupWithTokensConfig::default()
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
let quote_token = &tokens[0];
|
||||
let base_token = &tokens[1];
|
||||
|
||||
// total deposits on quote and base is 2x this value
|
||||
let deposit_amount = 1_000f64;
|
||||
let account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
0,
|
||||
&context.users[1],
|
||||
&mints[..1],
|
||||
deposit_amount as u64,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
let liqor = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
1,
|
||||
&context.users[1],
|
||||
mints,
|
||||
deposit_amount as u64,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
99,
|
||||
&context.users[1],
|
||||
&mints[1..],
|
||||
deposit_amount as u64,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
//
|
||||
// SETUP: A base deposit limit
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
TokenEdit {
|
||||
group,
|
||||
admin,
|
||||
mint: base_token.mint.pubkey,
|
||||
options: mango_v4::instruction::TokenEdit {
|
||||
deposit_limit_opt: Some(2500),
|
||||
..token_edit_instruction_default()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// SETUP: Sell base token from "account" creating borrows that are deposited into liqor,
|
||||
// increasing the total deposits
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
TokenConditionalSwapCreateInstruction {
|
||||
account,
|
||||
owner,
|
||||
buy_mint: quote_token.mint.pubkey,
|
||||
sell_mint: base_token.mint.pubkey,
|
||||
max_buy: 900,
|
||||
max_sell: 900,
|
||||
price_lower_limit: 0.0,
|
||||
price_upper_limit: 10.0,
|
||||
price_premium_rate: 0.01,
|
||||
allow_creating_deposits: true,
|
||||
allow_creating_borrows: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// TEST: Large execution fails, a bit smaller is ok
|
||||
//
|
||||
|
||||
send_tx_expect_error!(
|
||||
solana,
|
||||
TokenConditionalSwapTriggerInstruction {
|
||||
liqee: account,
|
||||
liqor,
|
||||
liqor_owner: owner,
|
||||
index: 0,
|
||||
max_buy_token_to_liqee: 100000,
|
||||
max_sell_token_to_liqor: 501,
|
||||
min_buy_token: 1,
|
||||
min_taker_price: 0.0,
|
||||
},
|
||||
MangoError::BankDepositLimit,
|
||||
);
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenConditionalSwapTriggerInstruction {
|
||||
liqee: account,
|
||||
liqor,
|
||||
liqor_owner: owner,
|
||||
index: 0,
|
||||
max_buy_token_to_liqee: 100000,
|
||||
max_sell_token_to_liqor: 499,
|
||||
min_buy_token: 1,
|
||||
min_taker_price: 0.0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -69,6 +69,27 @@ pub async fn send_tx_get_metadata<CI: ClientInstruction>(
|
|||
.await
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! send_tx_expect_error {
|
||||
($solana:expr, $ix:expr, $err:expr $(,)?) => {
|
||||
let result = send_tx($solana, $ix).await;
|
||||
let expected_err: u32 = $err.into();
|
||||
match result {
|
||||
Ok(_) => assert!(false, "no error returned"),
|
||||
Err(TransportError::TransactionError(
|
||||
solana_sdk::transaction::TransactionError::InstructionError(
|
||||
_,
|
||||
solana_program::instruction::InstructionError::Custom(err_num),
|
||||
),
|
||||
)) => {
|
||||
assert_eq!(err_num, expected_err, "wrong error code");
|
||||
}
|
||||
_ => assert!(false, "not a mango error"),
|
||||
}
|
||||
};
|
||||
}
|
||||
pub use send_tx_expect_error;
|
||||
|
||||
/// Build a transaction from multiple instructions
|
||||
pub struct ClientTransaction {
|
||||
solana: Arc<SolanaCookie>,
|
||||
|
@ -111,6 +132,28 @@ impl<'a> ClientTransaction {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_expect_error(
|
||||
&self,
|
||||
error: mango_v4::error::MangoError,
|
||||
) -> std::result::Result<(), BanksClientError> {
|
||||
let tx_result = self
|
||||
.solana
|
||||
.process_transaction(&self.instructions, Some(&self.signers))
|
||||
.await?;
|
||||
match tx_result.result {
|
||||
Ok(_) => assert!(false, "no error returned"),
|
||||
Err(solana_sdk::transaction::TransactionError::InstructionError(
|
||||
_,
|
||||
solana_program::instruction::InstructionError::Custom(err_num),
|
||||
)) => {
|
||||
let expected_err: u32 = error.into();
|
||||
assert_eq!(err_num, expected_err, "wrong error code");
|
||||
}
|
||||
_ => assert!(false, "not a mango error"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Tx error still returns success
|
||||
pub async fn send_get_metadata(
|
||||
&self,
|
||||
|
@ -386,6 +429,17 @@ pub async fn account_init_health(solana: &SolanaCookie, account: Pubkey) -> 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
|
||||
// matches the init health of the account.
|
||||
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_maker_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(
|
||||
|
@ -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_maker_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 {
|
||||
market_index: self.market_index,
|
||||
name: "UUU/usdc".to_string(),
|
||||
oracle_price_band: f32::MAX,
|
||||
};
|
||||
|
||||
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 group: Pubkey,
|
||||
pub admin: TestKeypair,
|
||||
|
@ -2472,7 +2580,7 @@ pub struct Serum3PlaceOrderInstruction {
|
|||
#[async_trait::async_trait(?Send)]
|
||||
impl ClientInstruction for Serum3PlaceOrderInstruction {
|
||||
type Accounts = mango_v4::accounts::Serum3PlaceOrder;
|
||||
type Instruction = mango_v4::instruction::Serum3PlaceOrder;
|
||||
type Instruction = mango_v4::instruction::Serum3PlaceOrderV2;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
|
@ -2526,7 +2634,7 @@ impl ClientInstruction for Serum3PlaceOrderInstruction {
|
|||
)
|
||||
.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,
|
||||
None,
|
||||
|
@ -2535,11 +2643,17 @@ impl ClientInstruction for Serum3PlaceOrderInstruction {
|
|||
)
|
||||
.await;
|
||||
|
||||
let payer_info = &match self.side {
|
||||
Serum3Side::Bid => "e_info,
|
||||
Serum3Side::Ask => &base_info,
|
||||
let (payer_info, receiver_info) = &match self.side {
|
||||
Serum3Side::Bid => ("e_info, &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 {
|
||||
group: account.fixed.group,
|
||||
account: self.account,
|
||||
|
|
|
@ -87,15 +87,12 @@ pub fn assert_mango_error<T>(
|
|||
) {
|
||||
match result {
|
||||
Ok(_) => assert!(false, "No error returned"),
|
||||
Err(TransportError::TransactionError(tx_err)) => match tx_err {
|
||||
TransactionError::InstructionError(_, err) => match err {
|
||||
InstructionError::Custom(err_num) => {
|
||||
assert_eq!(*err_num, expected_error, "{}", comment);
|
||||
}
|
||||
_ => assert!(false, "Not a mango error"),
|
||||
},
|
||||
_ => assert!(false, "Not a mango error"),
|
||||
},
|
||||
Err(TransportError::TransactionError(TransactionError::InstructionError(
|
||||
_,
|
||||
InstructionError::Custom(err_num),
|
||||
))) => {
|
||||
assert_eq!(*err_num, expected_error, "{}", comment);
|
||||
}
|
||||
_ => assert!(false, "Not a mango error"),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,7 @@ export interface BankForHealth {
|
|||
scaledInitLiabWeight(price: I80F48): I80F48;
|
||||
nativeDeposits(): I80F48;
|
||||
nativeBorrows(): I80F48;
|
||||
maintWeights(): [I80F48, I80F48];
|
||||
|
||||
depositWeightScaleStartQuote: number;
|
||||
borrowWeightScaleStartQuote: number;
|
||||
|
@ -75,6 +76,9 @@ export class Bank implements BankForHealth {
|
|||
public maintLiabWeight: I80F48;
|
||||
public liquidationFee: I80F48;
|
||||
public dust: I80F48;
|
||||
public maintWeightShiftDurationInv: I80F48;
|
||||
public maintWeightShiftAssetTarget: I80F48;
|
||||
public maintWeightShiftLiabTarget: I80F48;
|
||||
|
||||
static from(
|
||||
publicKey: PublicKey,
|
||||
|
@ -128,7 +132,13 @@ export class Bank implements BankForHealth {
|
|||
flashLoanSwapFeeRate: number;
|
||||
interestTargetUtilization: number;
|
||||
interestCurveScaling: number;
|
||||
depositsInSerum: BN;
|
||||
potentialSerumTokens: BN;
|
||||
maintWeightShiftStart: BN;
|
||||
maintWeightShiftEnd: BN;
|
||||
maintWeightShiftDurationInv: I80F48Dto;
|
||||
maintWeightShiftAssetTarget: I80F48Dto;
|
||||
maintWeightShiftLiabTarget: I80F48Dto;
|
||||
depositLimit: BN;
|
||||
},
|
||||
): Bank {
|
||||
return new Bank(
|
||||
|
@ -182,7 +192,13 @@ export class Bank implements BankForHealth {
|
|||
obj.flashLoanSwapFeeRate,
|
||||
obj.interestTargetUtilization,
|
||||
obj.interestCurveScaling,
|
||||
// obj.depositsInSerum,
|
||||
obj.potentialSerumTokens,
|
||||
obj.maintWeightShiftStart,
|
||||
obj.maintWeightShiftEnd,
|
||||
obj.maintWeightShiftDurationInv,
|
||||
obj.maintWeightShiftAssetTarget,
|
||||
obj.maintWeightShiftLiabTarget,
|
||||
obj.depositLimit,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -236,7 +252,14 @@ export class Bank implements BankForHealth {
|
|||
public tokenConditionalSwapMakerFeeRate: number,
|
||||
public flashLoanSwapFeeRate: 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.oracleConfig = {
|
||||
|
@ -263,6 +286,9 @@ export class Bank implements BankForHealth {
|
|||
this.initLiabWeight = I80F48.from(initLiabWeight);
|
||||
this.liquidationFee = I80F48.from(liquidationFee);
|
||||
this.dust = I80F48.from(dust);
|
||||
this.maintWeightShiftDurationInv = I80F48.from(maintWeightShiftDurationInv);
|
||||
this.maintWeightShiftAssetTarget = I80F48.from(maintWeightShiftAssetTarget);
|
||||
this.maintWeightShiftLiabTarget = I80F48.from(maintWeightShiftLiabTarget);
|
||||
this._price = undefined;
|
||||
this._uiPrice = 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 {
|
||||
return this.price.min(I80F48.fromNumber(this.stablePriceModel.stablePrice));
|
||||
}
|
||||
|
@ -457,19 +509,22 @@ export class Bank implements BankForHealth {
|
|||
}
|
||||
|
||||
const utilization = totalBorrows.div(totalDeposits);
|
||||
const scaling = I80F48.fromNumber(
|
||||
this.interestCurveScaling == 0.0 ? 1.0 : this.interestCurveScaling,
|
||||
);
|
||||
if (utilization.lt(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)) {
|
||||
const extraUtil = utilization.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 {
|
||||
const extraUtil = utilization.sub(this.util1);
|
||||
const slope = this.maxRate
|
||||
.sub(this.rate1)
|
||||
.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),
|
||||
nativeBorrows: () => I80F48.fromNumber(borrows),
|
||||
maintWeights: () => [
|
||||
I80F48.fromNumber(1 - maintWeight),
|
||||
I80F48.fromNumber(1 + maintWeight),
|
||||
],
|
||||
borrowWeightScaleStartQuote: borrowWeightScaleStartQuote,
|
||||
depositWeightScaleStartQuote: depositWeightScaleStartQuote,
|
||||
};
|
||||
|
|
|
@ -2,13 +2,7 @@ import { BN } from '@coral-xyz/anchor';
|
|||
import { OpenOrders } from '@project-serum/serum';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import {
|
||||
I80F48,
|
||||
I80F48Dto,
|
||||
MAX_I80F48,
|
||||
ONE_I80F48,
|
||||
ZERO_I80F48,
|
||||
} from '../numbers/I80F48';
|
||||
import { I80F48, MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
|
||||
import {
|
||||
toNativeI80F48ForQuote,
|
||||
toUiDecimals,
|
||||
|
@ -158,14 +152,6 @@ export class HealthCache {
|
|||
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): {
|
||||
tokenMaxReserved: TokenMaxReserved[];
|
||||
serum3Reserved: Serum3Reserved[];
|
||||
|
@ -1437,23 +1423,6 @@ export class TokenInfo {
|
|||
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 {
|
||||
const p = new Prices(
|
||||
bank.price,
|
||||
|
@ -1462,12 +1431,15 @@ export class TokenInfo {
|
|||
// Use the liab price for computing weight scaling, because it's pessimistic and
|
||||
// causes the most unfavorable scaling.
|
||||
const liabPrice = p.liab(HealthType.init);
|
||||
|
||||
const [maintAssetWeight, maintLiabWeight] = bank.maintWeights();
|
||||
|
||||
return new TokenInfo(
|
||||
bank.tokenIndex,
|
||||
bank.maintAssetWeight,
|
||||
maintAssetWeight,
|
||||
bank.initAssetWeight,
|
||||
bank.scaledInitAssetWeight(liabPrice),
|
||||
bank.maintLiabWeight,
|
||||
maintLiabWeight,
|
||||
bank.initLiabWeight,
|
||||
bank.scaledInitLiabWeight(liabPrice),
|
||||
p,
|
||||
|
@ -1564,18 +1536,6 @@ export class Serum3Info {
|
|||
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(
|
||||
serum3Market: Serum3Market,
|
||||
baseEntryIndex: number,
|
||||
|
@ -1756,29 +1716,6 @@ export class PerpInfo {
|
|||
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(
|
||||
perpMarket: PerpMarket,
|
||||
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.tokenConditionalSwapMakerFeeRate,
|
||||
params.flashLoanSwapFeeRate,
|
||||
params.interestCurveScaling,
|
||||
params.interestTargetUtilization,
|
||||
params.groupInsuranceFund,
|
||||
params.depositLimit,
|
||||
)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
|
@ -537,6 +541,15 @@ export class MangoClient {
|
|||
params.tokenConditionalSwapTakerFeeRate,
|
||||
params.tokenConditionalSwapMakerFeeRate,
|
||||
params.flashLoanSwapFeeRate,
|
||||
params.interestCurveScaling,
|
||||
params.interestTargetUtilization,
|
||||
params.maintWeightShiftStart,
|
||||
params.maintWeightShiftEnd,
|
||||
params.maintWeightShiftAssetTarget,
|
||||
params.maintWeightShiftLiabTarget,
|
||||
params.maintWeightShiftAbort ?? false,
|
||||
false, // setFallbackOracle, unused
|
||||
params.depositLimit,
|
||||
)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
|
@ -1604,9 +1617,10 @@ export class MangoClient {
|
|||
quoteBank: Bank,
|
||||
marketIndex: number,
|
||||
name: string,
|
||||
oraclePriceBand: number,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
const ix = await this.program.methods
|
||||
.serum3RegisterMarket(marketIndex, name)
|
||||
.serum3RegisterMarket(marketIndex, name, oraclePriceBand)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
|
@ -1626,11 +1640,12 @@ export class MangoClient {
|
|||
reduceOnly: boolean | null,
|
||||
forceClose: boolean | null,
|
||||
name: string | null,
|
||||
oraclePriceBand: number | null,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
const serum3Market =
|
||||
group.serum3MarketsMapByMarketIndex.get(serum3MarketIndex);
|
||||
const ix = await this.program.methods
|
||||
.serum3EditMarket(reduceOnly, forceClose, name)
|
||||
.serum3EditMarket(reduceOnly, forceClose, name, oraclePriceBand)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
|
@ -1885,6 +1900,32 @@ export class MangoClient {
|
|||
orderType: Serum3OrderType,
|
||||
clientOrderId: 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[]> {
|
||||
const ixs: TransactionInstruction[] = [];
|
||||
const serum3Market = group.serum3MarketsMapByExternal.get(
|
||||
|
@ -1998,6 +2039,143 @@ export class MangoClient {
|
|||
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(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
|
@ -2010,7 +2188,7 @@ export class MangoClient {
|
|||
clientOrderId: number,
|
||||
limit: number,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
const placeOrderIxs = await this.serum3PlaceOrderIx(
|
||||
const placeOrderIxs = await this.serum3PlaceOrderV2Ix(
|
||||
group,
|
||||
mangoAccount,
|
||||
externalMarketPk,
|
||||
|
@ -4875,7 +5053,7 @@ export class MangoClient {
|
|||
orderId,
|
||||
),
|
||||
this.serum3SettleFundsV2Ix(group, mangoAccount, externalMarketPk),
|
||||
this.serum3PlaceOrderIx(
|
||||
this.serum3PlaceOrderV2Ix(
|
||||
group,
|
||||
mangoAccount,
|
||||
externalMarketPk,
|
||||
|
|
|
@ -25,6 +25,9 @@ export interface TokenRegisterParams {
|
|||
tokenConditionalSwapTakerFeeRate: number;
|
||||
tokenConditionalSwapMakerFeeRate: number;
|
||||
flashLoanSwapFeeRate: number;
|
||||
interestCurveScaling: number;
|
||||
interestTargetUtilization: number;
|
||||
depositLimit: BN;
|
||||
}
|
||||
|
||||
export const DefaultTokenRegisterParams: TokenRegisterParams = {
|
||||
|
@ -35,10 +38,10 @@ export const DefaultTokenRegisterParams: TokenRegisterParams = {
|
|||
groupInsuranceFund: false,
|
||||
interestRateParams: {
|
||||
util0: 0.5,
|
||||
rate0: 0.072,
|
||||
rate0: 0.018,
|
||||
util1: 0.8,
|
||||
rate1: 0.2,
|
||||
maxRate: 2,
|
||||
rate1: 0.05,
|
||||
maxRate: 0.5,
|
||||
adjustmentFactor: 0.004,
|
||||
},
|
||||
loanFeeRate: 0.0005,
|
||||
|
@ -60,6 +63,9 @@ export const DefaultTokenRegisterParams: TokenRegisterParams = {
|
|||
tokenConditionalSwapTakerFeeRate: 0.0005,
|
||||
tokenConditionalSwapMakerFeeRate: 0.0005,
|
||||
flashLoanSwapFeeRate: 0.0005,
|
||||
interestCurveScaling: 4.0,
|
||||
interestTargetUtilization: 0.5,
|
||||
depositLimit: new BN(0),
|
||||
};
|
||||
|
||||
export interface TokenEditParams {
|
||||
|
@ -90,6 +96,14 @@ export interface TokenEditParams {
|
|||
tokenConditionalSwapTakerFeeRate: number | null;
|
||||
tokenConditionalSwapMakerFeeRate: 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 = {
|
||||
|
@ -120,6 +134,14 @@ export const NullTokenEditParams: TokenEditParams = {
|
|||
tokenConditionalSwapTakerFeeRate: null,
|
||||
tokenConditionalSwapMakerFeeRate: null,
|
||||
flashLoanSwapFeeRate: null,
|
||||
interestCurveScaling: null,
|
||||
interestTargetUtilization: null,
|
||||
maintWeightShiftStart: null,
|
||||
maintWeightShiftEnd: null,
|
||||
maintWeightShiftAssetTarget: null,
|
||||
maintWeightShiftLiabTarget: null,
|
||||
maintWeightShiftAbort: null,
|
||||
depositLimit: null,
|
||||
};
|
||||
|
||||
export interface PerpEditParams {
|
||||
|
@ -264,6 +286,7 @@ export interface IxGateParams {
|
|||
TokenConditionalSwapStart: boolean;
|
||||
TokenConditionalSwapCreatePremiumAuction: boolean;
|
||||
TokenConditionalSwapCreateLinearAuction: boolean;
|
||||
Serum3PlaceOrderV2: boolean;
|
||||
}
|
||||
|
||||
// Default with all ixs enabled, use with buildIxGate
|
||||
|
@ -342,6 +365,7 @@ export const TrueIxGateParams: IxGateParams = {
|
|||
TokenConditionalSwapStart: true,
|
||||
TokenConditionalSwapCreatePremiumAuction: true,
|
||||
TokenConditionalSwapCreateLinearAuction: true,
|
||||
Serum3PlaceOrderV2: true,
|
||||
};
|
||||
|
||||
// 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, 'TokenConditionalSwapCreatePremiumAuction', 69);
|
||||
toggleIx(ixGate, p, 'TokenConditionalSwapCreateLinearAuction', 70);
|
||||
toggleIx(ixGate, p, 'Serum3PlaceOrderV2', 71);
|
||||
|
||||
return ixGate;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue