Merge branch 'deploy-v0.21.0' into deploy

This commit is contained in:
microwavedcola1 2023-12-13 09:04:44 +01:00
commit e75d279b15
70 changed files with 4107 additions and 869 deletions

View File

@ -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

2
Cargo.lock generated
View File

@ -3301,7 +3301,7 @@ dependencies = [
[[package]]
name = "mango-v4"
version = "0.20.0"
version = "0.21.0"
dependencies = [
"anchor-lang",
"anchor-spl",

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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")
}

View File

@ -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"
}
]
}

View File

@ -1,6 +1,6 @@
[package]
name = "mango-v4"
version = "0.20.0"
version = "0.21.0"
description = "Created with Anchor"
edition = "2021"

View File

@ -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(

View File

@ -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>,

View File

@ -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 {

View File

@ -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()
},
],

View File

@ -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)
));
}

View File

@ -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(),

View File

@ -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,

View File

@ -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

View File

@ -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)?;

View File

@ -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;

View File

@ -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 => {

View File

@ -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,

View File

@ -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,

View File

@ -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(),

View File

@ -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> {

View File

@ -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()?;

View File

@ -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);
}

View File

@ -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 {

View File

@ -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);

View File

@ -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(),

View File

@ -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,

View File

@ -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,

View File

@ -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(),

View File

@ -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,

View File

@ -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;

View File

@ -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,

View File

@ -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()),
});
}

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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(),

View File

@ -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(())
}
}

View File

@ -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,

View File

@ -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(())
}

View File

@ -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(())

View File

@ -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(

View File

@ -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,

View File

@ -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,

View File

@ -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;

View File

@ -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,

View File

@ -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(())
}

View File

@ -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,

View File

@ -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(())
}
}

View File

@ -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.
}

View File

@ -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,

View File

@ -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],
}
}
}

View File

@ -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,

View File

@ -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(),

View File

@ -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)]

View File

@ -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(())
}

View File

@ -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> {

View File

@ -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(())
}

View File

@ -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,
&quote_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,
&quote_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,

View File

@ -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(())
}

View File

@ -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 => &quote_info,
Serum3Side::Ask => &base_info,
let (payer_info, receiver_info) = &match self.side {
Serum3Side::Bid => (&quote_info, &base_info),
Serum3Side::Ask => (&base_info, &quote_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,

View File

@ -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"),
}
}

View File

@ -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);
}
}

View File

@ -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,
};

View File

@ -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;
}

View File

@ -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,

View File

@ -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