Merge remote-tracking branch 'origin/release/program-v0.18' into deploy

This commit is contained in:
Christian Kamm 2023-07-17 16:28:59 +02:00
commit fef5fd97da
100 changed files with 8494 additions and 1251 deletions

View File

@ -4,7 +4,31 @@ Update this for each program release and mainnet deployment.
## not on mainnet
### v0.17.1, 2023-7-
### v0.18.0, 2023-7-
- Introduce limit and stop loss orders for arbitrary spot pairs (#604, #634)
Allow users to request that a swap between two spot tokens should be executed
once the price crosses a threshold. Independent of OpenBook markets.
- Improve behavior when listing tokens or markets with upcoming oracles (#620)
When we listed RNDR before the oracle started publishing a price, there
was an issue where the stable price got initialized to 0. Now, the stable
price is only initialized the first time a valid oracle value is read.
- Deprecate Serum3SettleFunds (#606)
Use the Serum3SettleFundsV2 instruction introduced in v0.8.0.
- Perp FillEventLog: Include amount of closed pnl (#624)
- Pyth: Fix reading most recent valid price (#631)
## mainnet
### v0.17.1, 2023-7-6
Deployment: Jul 6, 2023 at 20:26:34 Central European Summer Time, https://explorer.solana.com/tx/4kiVtR1G3xNh8bTP4FetfG7rjPjLThFjrQNzMMs2TfQHnw7Ezp6JX4rboQbGrJsfZDd6zaMuEa1ZTxahRwPPb9JR
- Remove extra Pyth oracle status check added in v0.17.0
@ -15,8 +39,6 @@ Update this for each program release and mainnet deployment.
This staleness limit is much more strict than the ones configured on the
oracles currently used by Mango and caused occasional transaction failures.
## mainnet
### v0.17.0, 2023-7-3
Deployment: Jul 3, 2023 at 09:46:14 Central European Summer Time, https://explorer.solana.com/tx/4G6b1uihopkHqp968sq3RYacYHn5ND8mMmeNd1RfswTCmiqeappTN2747JTvswVXxs7oqgfU6M3VKPGVRFPGJYuL

104
Cargo.lock generated
View File

@ -1698,19 +1698,6 @@ dependencies = [
"syn 1.0.105",
]
[[package]]
name = "env_logger"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3"
dependencies = [
"atty",
"humantime",
"log 0.4.17",
"regex",
"termcolor",
]
[[package]]
name = "env_logger"
version = "0.9.3"
@ -2192,6 +2179,15 @@ dependencies = [
"libc",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
dependencies = [
"serde",
]
[[package]]
name = "histogram"
version = "0.6.9"
@ -2928,7 +2924,7 @@ dependencies = [
[[package]]
name = "mango-v4"
version = "0.17.1"
version = "0.18.0"
dependencies = [
"anchor-lang",
"anchor-spl",
@ -2940,7 +2936,7 @@ dependencies = [
"bytemuck",
"default-env",
"derivative",
"env_logger 0.9.3",
"env_logger",
"fixed",
"itertools",
"lazy_static",
@ -2974,10 +2970,9 @@ dependencies = [
"anyhow",
"clap 3.2.23",
"dotenv",
"env_logger 0.8.4",
"fixed",
"futures 0.3.25",
"log 0.4.17",
"itertools",
"mango-v4",
"mango-v4-client",
"pyth-sdk-solana",
@ -2985,6 +2980,7 @@ dependencies = [
"solana-client",
"solana-sdk",
"tokio",
"tracing",
]
[[package]]
@ -2998,6 +2994,7 @@ dependencies = [
"async-channel",
"async-once-cell",
"async-trait",
"atty",
"base64 0.13.1",
"bincode",
"fixed",
@ -3005,7 +3002,6 @@ dependencies = [
"itertools",
"jsonrpc-core 18.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core-client",
"log 0.4.17",
"mango-feeds-connector",
"mango-v4",
"pyth-sdk-solana",
@ -3023,6 +3019,8 @@ dependencies = [
"thiserror",
"tokio",
"tokio-stream",
"tracing",
"tracing-subscriber",
]
[[package]]
@ -3035,12 +3033,10 @@ dependencies = [
"anyhow",
"clap 3.2.23",
"dotenv",
"env_logger 0.8.4",
"fixed",
"futures 0.3.25",
"itertools",
"lazy_static",
"log 0.4.17",
"mango-v4",
"mango-v4-client",
"prometheus",
@ -3049,6 +3045,7 @@ dependencies = [
"solana-client",
"solana-sdk",
"tokio",
"tracing",
"warp",
]
@ -3076,7 +3073,6 @@ dependencies = [
"jemallocator",
"jsonrpc-core 18.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core-client",
"log 0.4.17",
"mango-v4",
"mango-v4-client",
"once_cell",
@ -3095,6 +3091,7 @@ dependencies = [
"tokio",
"tokio-stream",
"tokio-tungstenite 0.16.1",
"tracing",
]
[[package]]
@ -3122,7 +3119,6 @@ dependencies = [
"jemallocator",
"jsonrpc-core 18.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core-client",
"log 0.4.17",
"mango-v4",
"mango-v4-client",
"once_cell",
@ -3142,6 +3138,16 @@ dependencies = [
"tokio",
"tokio-stream",
"tokio-tungstenite 0.16.1",
"tracing",
]
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata",
]
[[package]]
@ -3415,6 +3421,16 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi 0.3.9",
]
[[package]]
name = "num"
version = "0.2.1"
@ -3716,6 +3732,12 @@ dependencies = [
"syn 1.0.105",
]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "parking_lot"
version = "0.9.0"
@ -4106,21 +4128,22 @@ dependencies = [
[[package]]
name = "pyth-sdk"
version = "0.1.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "446ff07d7ef3bd98214f9b4fe6a611a69e36b5aad74b18cdbad5150193c1f204"
checksum = "00bf2540203ca3c7a5712fdb8b5897534b7f6a0b6e7b0923ff00466c5f9efcb3"
dependencies = [
"borsh",
"borsh-derive",
"hex",
"schemars",
"serde",
]
[[package]]
name = "pyth-sdk-solana"
version = "0.1.0"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27a648739aa69cab94edd900a0d7ca37d8a789e9c88741b23deec11fab418d16"
checksum = "e1fcd2dcd063ea85004667cf5cb07f30de7387f17249df988a295e764a47b9f5"
dependencies = [
"borsh",
"borsh-derive",
@ -4509,6 +4532,15 @@ dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.28"
@ -5670,7 +5702,7 @@ version = "1.14.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b502866be84a799633c0744e1d72b819a256337149e9fb6c7eee4db84ec63f5"
dependencies = [
"env_logger 0.9.3",
"env_logger",
"lazy_static",
"log 0.4.17",
]
@ -7227,6 +7259,17 @@ dependencies = [
"tracing",
]
[[package]]
name = "tracing-log"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
dependencies = [
"lazy_static",
"log 0.4.17",
"tracing-core",
]
[[package]]
name = "tracing-opentelemetry"
version = "0.17.4"
@ -7246,9 +7289,16 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec 1.10.0",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]

View File

@ -10,7 +10,7 @@ anchor-client = "0.27.0"
anchor-lang = "0.27.0"
anchor-spl = "0.27.0"
fixed = { path = "./3rdparty/fixed", version = "1.11.0" }
pyth-sdk-solana = "0.1.0"
pyth-sdk-solana = "0.7.0"
serum_dex = { git = "https://github.com/openbook-dex/program.git" }
solana-address-lookup-table-program = "~1.14.9"
solana-account-decoder = "~1.14.9"

Binary file not shown.

View File

@ -16,10 +16,8 @@ anchor-spl = { workspace = true }
anyhow = "1.0"
clap = { version = "3.1.8", features = ["derive", "env"] }
dotenv = "0.15.0"
env_logger = "0.8.4"
fixed = { workspace = true, features = ["serde", "borsh"] }
futures = "0.3.21"
log = "0.4.0"
mango-v4 = { path = "../../programs/mango-v4", features = ["client"] }
mango-v4-client = { path = "../../lib/client" }
pyth-sdk-solana = { workspace = true }
@ -27,3 +25,5 @@ serum_dex = { workspace = true, default-features=false,features = ["no-entrypoin
solana-client = { workspace = true }
solana-sdk = { workspace = true }
tokio = { version = "1.14.1", features = ["rt-multi-thread", "time", "macros", "sync"] }
itertools = "0.10.3"
tracing = "0.1"

View File

@ -7,6 +7,8 @@ use solana_sdk::pubkey::Pubkey;
use std::str::FromStr;
use std::sync::Arc;
mod test_oracles;
#[derive(Parser, Debug, Clone)]
#[clap()]
struct Cli {
@ -108,6 +110,14 @@ enum Command {
#[clap(short, long, default_value = "0")]
num: u32,
},
/// Regularly fetches all oracles and prints their prices
TestOracles {
#[clap(short, long)]
group: String,
#[clap(flatten)]
rpc: Rpc,
},
}
impl Rpc {
@ -127,9 +137,7 @@ impl Rpc {
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
env_logger::init_from_env(
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
);
mango_v4_client::tracing_subscriber_init();
dotenv::dotenv().ok();
let cli = Cli::parse();
@ -215,6 +223,11 @@ async fn main() -> Result<(), anyhow::Error> {
.0;
println!("{}", address);
}
Command::TestOracles { group, rpc } => {
let client = rpc.client(None)?;
let group = pubkey_from_cli(&group);
test_oracles::run(&client, group).await?;
}
};
Ok(())

View File

@ -0,0 +1,74 @@
use itertools::Itertools;
use mango_v4::accounts_zerocopy::KeyedAccount;
use mango_v4_client::{Client, MangoGroupContext};
use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::pubkey::Pubkey;
use tracing::*;
pub async fn run(client: &Client, group: Pubkey) -> anyhow::Result<()> {
let rpc_async = client.rpc_async();
let context = MangoGroupContext::new_from_rpc(&rpc_async, group).await?;
let oracles = context
.tokens
.values()
.map(|t| t.mint_info.oracle)
.chain(context.perp_markets.values().map(|p| p.market.oracle))
.unique()
.collect_vec();
let mut interval = tokio::time::interval(std::time::Duration::from_secs(5));
loop {
interval.tick().await;
let response = rpc_async
.get_multiple_accounts_with_commitment(&oracles, CommitmentConfig::processed())
.await;
if response.is_err() {
warn!("could not fetch oracles");
continue;
}
let response = response.unwrap();
let slot = response.context.slot;
let accounts = response.value;
for (pubkey, account_opt) in oracles.iter().zip(accounts.into_iter()) {
if account_opt.is_none() {
warn!("no oracle data for {pubkey}");
continue;
}
let keyed_account = KeyedAccount {
key: *pubkey,
account: account_opt.unwrap(),
};
let tc_opt = context
.tokens
.values()
.find(|t| t.mint_info.oracle == *pubkey);
let pc_opt = context
.perp_markets
.values()
.find(|p| p.market.oracle == *pubkey);
let mut price = None;
if let Some(tc) = tc_opt {
match tc.bank.oracle_price(&keyed_account, Some(slot)) {
Ok(p) => price = Some(p),
Err(e) => {
error!("could not read bank oracle {}: {e:?}", keyed_account.key);
}
}
}
if let Some(pc) = pc_opt {
match pc.market.oracle_price(&keyed_account, Some(slot)) {
Ok(p) => price = Some(p),
Err(e) => {
error!("could not read perp oracle {}: {e:?}", keyed_account.key);
}
}
}
if let Some(p) = price {
info!("{pubkey},{p}");
}
}
}
}

View File

@ -16,11 +16,9 @@ anchor-spl = { workspace = true }
anyhow = "1.0"
clap = { version = "3.1.8", features = ["derive", "env"] }
dotenv = "0.15.0"
env_logger = "0.8.4"
fixed = { workspace = true, features = ["serde", "borsh"] }
futures = "0.3.21"
itertools = "0.10.3"
log = "0.4.0"
mango-v4 = { path = "../../programs/mango-v4", features = ["client"] }
mango-v4-client = { path = "../../lib/client" }
pyth-sdk-solana = { workspace = true }
@ -31,3 +29,4 @@ tokio = { version = "1.14.1", features = ["rt-multi-thread", "time", "macros", "
prometheus = "0.13.3"
warp = "0.3.3"
lazy_static = "1.4.0"
tracing = "0.1"

View File

@ -12,6 +12,7 @@ use solana_sdk::{
pubkey::Pubkey,
};
use tokio::time;
use tracing::*;
use warp::Filter;
lazy_static::lazy_static! {
@ -229,21 +230,18 @@ pub async fn loop_update_index_and_rate(
if let Err(e) = sig_result {
METRIC_UPDATE_TOKENS_FAILURE.inc();
log::info!(
info!(
"metricName=UpdateTokensV4Failure tokens={} durationMs={} error={}",
token_names,
confirmation_time,
e
token_names, confirmation_time, e
);
log::error!("{:?}", e)
error!("{:?}", e)
} else {
METRIC_UPDATE_TOKENS_SUCCESS.inc();
log::info!(
info!(
"metricName=UpdateTokensV4Success tokens={} durationMs={}",
token_names,
confirmation_time,
token_names, confirmation_time,
);
log::info!("{:?}", sig_result);
info!("{:?}", sig_result);
}
}
}
@ -304,7 +302,7 @@ pub async fn loop_consume_events(
Ok(Some(x)) => x,
Ok(None) => continue,
Err(err) => {
log::error!("preparing consume_events ams: {err:?}");
error!("preparing consume_events ams: {err:?}");
continue;
}
};
@ -347,23 +345,23 @@ pub async fn loop_consume_events(
if let Err(e) = sig_result {
METRIC_CONSUME_EVENTS_FAILURE.inc();
log::info!(
info!(
"metricName=ConsumeEventsV4Failure market={} durationMs={} consumed={} error={}",
perp_market.name(),
confirmation_time,
num_of_events,
e.to_string()
);
log::error!("{:?}", e)
error!("{:?}", e)
} else {
METRIC_CONSUME_EVENTS_SUCCESS.inc();
log::info!(
info!(
"metricName=ConsumeEventsV4Success market={} durationMs={} consumed={}",
perp_market.name(),
confirmation_time,
num_of_events,
);
log::info!("{:?}", sig_result);
info!("{:?}", sig_result);
}
}
}
@ -402,21 +400,21 @@ pub async fn loop_update_funding(
if let Err(e) = sig_result {
METRIC_UPDATE_FUNDING_FAILURE.inc();
log::error!(
error!(
"metricName=UpdateFundingV4Error market={} durationMs={} error={}",
perp_market.name(),
confirmation_time,
e.to_string()
);
log::error!("{:?}", e)
error!("{:?}", e)
} else {
METRIC_UPDATE_FUNDING_SUCCESS.inc();
log::info!(
info!(
"metricName=UpdateFundingV4Success market={} durationMs={}",
perp_market.name(),
confirmation_time,
);
log::info!("{:?}", sig_result);
info!("{:?}", sig_result);
}
}
}

View File

@ -11,6 +11,7 @@ use mango_v4_client::{keypair_from_cli, Client, MangoClient, TransactionBuilderC
use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::pubkey::Pubkey;
use tokio::time;
use tracing::*;
// TODO
// - may be nice to have one-shot cranking as well as the interval cranking
@ -73,9 +74,7 @@ enum Command {
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
env_logger::init_from_env(
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
);
mango_v4_client::tracing_subscriber_init();
let args = if let Ok(cli_dotenv) = CliDotenv::try_parse() {
dotenv::from_path(cli_dotenv.dotenv)?;
@ -121,7 +120,7 @@ async fn main() -> Result<(), anyhow::Error> {
interval.tick().await;
let client = mango_client.clone();
tokio::task::spawn_blocking(move || {
log::info!(
info!(
"Arc<MangoClient>::strong_count() {}",
Arc::<MangoClient>::strong_count(&client)
)

View File

@ -6,7 +6,11 @@ use std::{
use fixed::types::I80F48;
use futures::Future;
use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
use mango_v4::{
accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side},
state::TokenIndex,
};
use tracing::*;
use tokio::time;
@ -20,47 +24,34 @@ pub async fn runner(
ensure_oo(&mango_client).await?;
let mut price_arcs = HashMap::new();
for market_name in mango_client.context.serum3_market_indexes_by_name.keys() {
for s3_market in mango_client.context.serum3_markets.values() {
let base_token_index = s3_market.market.base_token_index;
let price = mango_client
.get_oracle_price(
market_name
.split('/')
.collect::<Vec<&str>>()
.first()
.unwrap(),
)
.bank_oracle_price(base_token_index)
.await
.unwrap();
price_arcs.insert(
market_name.to_owned(),
Arc::new(RwLock::new(
I80F48::from_num(price.price) / I80F48::from_num(10u64.pow(-price.expo as u32)),
)),
);
price_arcs.insert(base_token_index, Arc::new(RwLock::new(price)));
}
let handles1 = mango_client
.context
.serum3_market_indexes_by_name
.keys()
.map(|market_name| {
loop_blocking_price_update(
mango_client.clone(),
market_name.to_owned(),
price_arcs.get(market_name).unwrap().clone(),
)
let handles1 = price_arcs
.iter()
.map(|(base_token_index, price)| {
loop_blocking_price_update(mango_client.clone(), *base_token_index, price.clone())
})
.collect::<Vec<_>>();
let handles2 = mango_client
.context
.serum3_market_indexes_by_name
.keys()
.map(|market_name| {
.serum3_markets
.values()
.map(|s3_market| {
loop_blocking_orders(
mango_client.clone(),
market_name.to_owned(),
price_arcs.get(market_name).unwrap().clone(),
s3_market.market.name().to_string(),
price_arcs
.get(&s3_market.market.base_token_index)
.unwrap()
.clone(),
)
})
.collect::<Vec<_>>();
@ -100,7 +91,7 @@ async fn ensure_deposit(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::E
Some(token_account) => {
let native = token_account.native(&bank);
let ui = token_account.ui(&bank);
log::info!("Current balance {} {}", ui, bank.name());
info!("Current balance {} {}", ui, bank.name());
if native < I80F48::ZERO {
desired_balance - native
@ -115,7 +106,7 @@ async fn ensure_deposit(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::E
continue;
}
log::info!("Depositing {} {}", deposit_native, bank.name());
info!("Depositing {} {}", deposit_native, bank.name());
mango_client
.token_deposit(bank.mint, desired_balance.to_num(), false)
.await?;
@ -126,19 +117,18 @@ async fn ensure_deposit(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::E
pub async fn loop_blocking_price_update(
mango_client: Arc<MangoClient>,
market_name: String,
token_index: TokenIndex,
price: Arc<RwLock<I80F48>>,
) {
let mut interval = time::interval(Duration::from_secs(1));
let token_name = market_name.split('/').collect::<Vec<&str>>()[0];
let token_name = &mango_client.context.token(token_index).name;
loop {
interval.tick().await;
let fresh_price = mango_client.get_oracle_price(token_name).await.unwrap();
log::info!("{} Updated price is {:?}", token_name, fresh_price.price);
let fresh_price = mango_client.bank_oracle_price(token_index).await.unwrap();
info!("{} Updated price is {:?}", token_name, fresh_price);
if let Ok(mut price) = price.write() {
*price = I80F48::from_num(fresh_price.price)
/ I80F48::from_num(10u64.pow(-fresh_price.expo as u32));
*price = fresh_price;
}
}
}
@ -155,7 +145,10 @@ pub async fn loop_blocking_orders(
.serum3_cancel_all_orders(&market_name)
.await
.unwrap();
log::info!("Cancelled orders - {:?} for {}", orders, market_name);
info!("Cancelled orders - {:?} for {}", orders, market_name);
let market_index = mango_client.context.serum3_market_index(&market_name);
let s3 = mango_client.context.serum3(market_index);
loop {
interval.tick().await;
@ -164,25 +157,21 @@ pub async fn loop_blocking_orders(
let market_name = market_name.clone();
let price = price.clone();
let res = (|| async move {
let res: anyhow::Result<()> = (|| async move {
client.serum3_settle_funds(&market_name).await?;
let fresh_price = match price.read() {
Ok(price) => *price,
Err(_) => {
anyhow::bail!("Price RwLock PoisonError!");
}
};
let fresh_price = price.read().unwrap().to_num::<f64>();
let bid_price = fresh_price * 1.1;
let fresh_price = fresh_price.to_num::<f64>();
let bid_price_lots = bid_price * s3.coin_lot_size as f64 / s3.pc_lot_size as f64;
let bid_price = fresh_price + fresh_price * 0.1;
let res = client
.serum3_place_order(
&market_name,
Serum3Side::Bid,
bid_price,
0.0001,
bid_price_lots.round() as u64,
1,
u64::MAX,
Serum3SelfTradeBehavior::DecrementTake,
Serum3OrderType::ImmediateOrCancel,
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64,
@ -190,18 +179,21 @@ pub async fn loop_blocking_orders(
)
.await;
if let Err(e) = res {
log::error!("Error while placing taker bid {:#?}", e)
error!("Error while placing taker bid {:#?}", e)
} else {
log::info!("Placed bid at {} for {}", bid_price, market_name)
info!("Placed bid at {} for {}", bid_price, market_name)
}
let ask_price = fresh_price - fresh_price * 0.1;
let ask_price = fresh_price * 0.9;
let ask_price_lots = ask_price * s3.coin_lot_size as f64 / s3.pc_lot_size as f64;
let res = client
.serum3_place_order(
&market_name,
Serum3Side::Ask,
ask_price,
0.0001,
ask_price_lots.round() as u64,
1,
u64::MAX,
Serum3SelfTradeBehavior::DecrementTake,
Serum3OrderType::ImmediateOrCancel,
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64,
@ -209,9 +201,9 @@ pub async fn loop_blocking_orders(
)
.await;
if let Err(e) = res {
log::error!("Error while placing taker ask {:#?}", e)
error!("Error while placing taker ask {:#?}", e)
} else {
log::info!("Placed ask at {} for {}", ask_price, market_name)
info!("Placed ask at {} for {}", ask_price, market_name)
}
Ok(())
@ -219,7 +211,7 @@ pub async fn loop_blocking_orders(
.await;
if let Err(err) = res {
log::error!("{:?}", err);
error!("{:?}", err);
}
}
}

View File

@ -30,7 +30,6 @@ itertools = "0.10.3"
jemallocator = "0.3.2"
jsonrpc-core = "18.0.0"
jsonrpc-core-client = { version = "18.0.0", features = ["ws", "http", "tls"] }
log = "0.4"
mango-v4 = { path = "../../programs/mango-v4", features = ["client"] }
mango-v4-client = { path = "../../lib/client" }
once_cell = "1.12.0"
@ -49,3 +48,4 @@ solana-sdk = { workspace = true }
tokio = { version = "1", features = ["full"] }
tokio-stream = { version = "0.1.9"}
tokio-tungstenite = "0.16.1"
tracing = "0.1"

View File

@ -1,3 +1,29 @@
## Disclaimer
The following open source code contains an an example that documents possible interaction with the smart contract for the purpose of performing liquidations. Please note that the use of this code is at your own risk and responsibility.
1. No Warranty: The code is provided "as is," without any warranty or guarantee of any kind, express or implied. The developers and contributors of this code do not make any representations or warranties regarding its accuracy, reliability, or functionality. The use of this code is solely at your own risk.
2. Limitation of Liability: In no event shall the developers and contributors of this code be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services, loss of use, data, or profits, or business interruption) arising in any way out of the use, inability to use, or the results of the use of this code, even if advised of the possibility of such damages.
3. Compliance with Laws: It is your responsibility to ensure that the use of this code complies with all applicable laws, regulations, and policies. The developers and contributors of this code shall not be held responsible for any illegal or unauthorized use of the code.
4. User Accountability: You are solely responsible for any actions performed using this code. The developers and contributors of this code shall not be held liable for any misuse, harm, or damages caused by the bot or its actions.
5. Security Considerations: While efforts have been made to ensure the security of this code, the developers and contributors do not guarantee its absolute security. It is recommended that you take appropriate measures to secure the code and any associated systems from potential vulnerabilities or threats.
6. Third-Party Dependencies: This code may rely on third-party libraries, frameworks, or APIs. The developers and contributors of this code are not responsible for the functionality, availability, or security of any third-party components.
By using this open source code, you acknowledge and agree to the above disclaimer. If you do not agree with any part of the disclaimer, refrain from using the code.
## License
See https://github.com/blockworks-foundation/mango-v4/blob/dev/LICENSE
---
Two branches are relevant here:
- `devnet`: bleeding edge, may be unstable, could be incompatible with deployed program

View File

@ -2,21 +2,22 @@ use std::collections::HashSet;
use std::time::Duration;
use itertools::Itertools;
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
use mango_v4::health::{HealthCache, HealthType};
use mango_v4::state::{
Bank, MangoAccountValue, PerpMarketIndex, Side, TokenIndex, QUOTE_TOKEN_INDEX,
};
use mango_v4_client::{chain_data, health_cache, AccountFetcher, JupiterSwapMode, MangoClient};
use mango_v4::state::{MangoAccountValue, PerpMarketIndex, Side, TokenIndex, QUOTE_TOKEN_INDEX};
use mango_v4_client::{chain_data, health_cache, JupiterSwapMode, MangoClient};
use solana_sdk::signature::Signature;
use futures::{stream, StreamExt, TryStreamExt};
use rand::seq::SliceRandom;
use tracing::*;
use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
use crate::util;
pub struct Config {
pub min_health_ratio: f64,
pub refresh_timeout: Duration,
pub mock_jupiter: bool,
}
pub async fn jupiter_market_can_buy(
@ -119,7 +120,7 @@ impl<'a> LiquidateHelper<'a> {
}
// Cancel all orders on a random serum market
let serum_orders = serum_force_cancels.choose(&mut rand::thread_rng()).unwrap();
let sig = self
let txsig = self
.client
.serum3_liq_force_cancel_orders(
(self.pubkey, &self.liqee),
@ -127,14 +128,12 @@ impl<'a> LiquidateHelper<'a> {
&serum_orders.open_orders,
)
.await?;
log::info!(
"Force cancelled serum orders on account {}, market index {}, maint_health was {}, tx sig {:?}",
self.pubkey,
serum_orders.market_index,
self.maint_health,
sig
info!(
market_index = serum_orders.market_index,
%txsig,
"Force cancelled serum orders",
);
Ok(Some(sig))
Ok(Some(txsig))
}
async fn perp_close_orders(&self) -> anyhow::Result<Option<Signature>> {
@ -149,18 +148,16 @@ impl<'a> LiquidateHelper<'a> {
// Cancel all orders on a random perp market
let perp_market_index = *perp_force_cancels.choose(&mut rand::thread_rng()).unwrap();
let sig = self
let txsig = self
.client
.perp_liq_force_cancel_orders((self.pubkey, &self.liqee), perp_market_index)
.await?;
log::info!(
"Force cancelled perp orders on account {}, market index {}, maint_health was {}, tx sig {:?}",
self.pubkey,
info!(
perp_market_index,
self.maint_health,
sig
%txsig,
"Force cancelled perp orders",
);
Ok(Some(sig))
Ok(Some(txsig))
}
async fn perp_liq_base_or_positive_pnl(&self) -> anyhow::Result<Option<Signature>> {
@ -173,15 +170,7 @@ impl<'a> LiquidateHelper<'a> {
{
return Ok(None);
}
let perp = self.client.context.perp(pp.market_index);
let oracle = self
.account_fetcher
.fetch_raw_account(&perp.market.oracle)
.await?;
let price = perp.market.oracle_price(
&KeyedAccountSharedData::new(perp.market.oracle, oracle.into()),
None,
)?;
let price = self.client.perp_oracle_price(pp.market_index).await?;
Ok(Some((
pp.market_index,
base_lots,
@ -263,9 +252,13 @@ impl<'a> LiquidateHelper<'a> {
(max_base_transfer, max_pnl_transfer.floor().to_num::<u64>())
};
log::info!("computed max_base_transfer: {max_base_transfer_abs}, max_pnl_transfer: {max_pnl_transfer}");
trace!(
max_base_transfer_abs,
max_pnl_transfer,
"computed transfer maximums"
);
let sig = self
let txsig = self
.client
.perp_liq_base_or_positive_pnl(
(self.pubkey, &self.liqee),
@ -274,14 +267,12 @@ impl<'a> LiquidateHelper<'a> {
max_pnl_transfer,
)
.await?;
log::info!(
"Liquidated base position for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}",
self.pubkey,
info!(
perp_market_index,
self.maint_health,
sig
%txsig,
"Liquidated base position for perp market",
);
Ok(Some(sig))
Ok(Some(txsig))
}
async fn perp_liq_negative_pnl_or_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
@ -306,7 +297,7 @@ impl<'a> LiquidateHelper<'a> {
}
let (perp_market_index, _) = perp_negative_pnl.first().unwrap();
let sig = self
let txsig = self
.client
.perp_liq_negative_pnl_or_bankruptcy(
(self.pubkey, &self.liqee),
@ -315,32 +306,21 @@ impl<'a> LiquidateHelper<'a> {
u64::MAX,
)
.await?;
log::info!(
"Liquidated negative perp pnl on account {}, market index {}, maint_health was {}, tx sig {:?}",
self.pubkey,
info!(
perp_market_index,
self.maint_health,
sig
%txsig,
"Liquidated negative perp pnl",
);
Ok(Some(sig))
Ok(Some(txsig))
}
async fn tokens(&self) -> anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>> {
let tokens_maybe: anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>> =
stream::iter(self.liqee.active_token_positions())
.then(|token_position| async {
let token = self.client.context.token(token_position.token_index);
let bank = self
.account_fetcher
.fetch::<Bank>(&token.mint_info.first_bank())?;
let oracle = self
.account_fetcher
.fetch_raw_account(&token.mint_info.oracle)
.await?;
let price = bank.oracle_price(
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
None,
)?;
let token_index = token_position.token_index;
let price = self.client.bank_oracle_price(token_index).await?;
let bank = self.client.first_bank(token_index).await?;
Ok((
token_position.token_index,
price,
@ -359,40 +339,28 @@ impl<'a> LiquidateHelper<'a> {
source: TokenIndex,
target: TokenIndex,
) -> anyhow::Result<I80F48> {
let mut liqor = self
let liqor = self
.account_fetcher
.fetch_fresh_mango_account(&self.client.mango_account_address)
.await
.context("getting liquidator account")?;
// Ensure the tokens are activated, so they appear in the health cache and
// max_swap_source() will work.
liqor.ensure_token_position(source)?;
liqor.ensure_token_position(target)?;
let source_price = self.client.bank_oracle_price(source).await?;
let target_price = self.client.bank_oracle_price(target).await?;
let health_cache = health_cache::new(&self.client.context, self.account_fetcher, &liqor)
.await
.expect("always ok");
let source_bank = self.client.first_bank(source).await?;
let target_bank = self.client.first_bank(target).await?;
let source_price = health_cache.token_info(source).unwrap().prices.oracle;
let target_price = health_cache.token_info(target).unwrap().prices.oracle;
// TODO: This is where we could multiply in the liquidation fee factors
let oracle_swap_price = source_price / target_price;
let price = source_price / target_price;
let amount = health_cache
.max_swap_source_for_health_ratio(
&liqor,
&source_bank,
source_price,
&target_bank,
oracle_swap_price,
self.liqor_min_health_ratio,
)
.context("getting max_swap_source")?;
Ok(amount)
util::max_swap_source(
self.client,
self.account_fetcher,
&liqor,
source,
target,
price,
self.liqor_min_health_ratio,
)
.await
}
async fn token_liq(&self) -> anyhow::Result<Option<Signature>> {
@ -445,7 +413,7 @@ impl<'a> LiquidateHelper<'a> {
// TODO: log liqor's assets in UI form
// TODO: log liquee's liab_needed, need to refactor program code to be able to be accessed from client side
//
let sig = self
let txsig = self
.client
.token_liq_with_token(
(self.pubkey, &self.liqee),
@ -455,13 +423,13 @@ impl<'a> LiquidateHelper<'a> {
)
.await
.context("sending liq_token_with_token")?;
log::info!(
"Liquidated token with token for {}, maint_health was {}, tx sig {:?}",
self.pubkey,
self.maint_health,
sig
info!(
asset_token_index,
liab_token_index,
%txsig,
"Liquidated token with token",
);
Ok(Some(sig))
Ok(Some(txsig))
}
async fn token_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
@ -499,7 +467,7 @@ impl<'a> LiquidateHelper<'a> {
.max_token_liab_transfer(liab_token_index, quote_token_index)
.await?;
let sig = self
let txsig = self
.client
.token_liq_bankruptcy(
(self.pubkey, &self.liqee),
@ -508,15 +476,15 @@ impl<'a> LiquidateHelper<'a> {
)
.await
.context("sending liq_token_bankruptcy")?;
log::info!(
"Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}",
self.pubkey,
self.maint_health,
sig
info!(
liab_token_index,
%txsig,
"Liquidated token bankruptcy",
);
Ok(Some(sig))
Ok(Some(txsig))
}
#[instrument(skip(self), fields(pubkey = %*self.pubkey, maint = %self.maint_health))]
async fn send_liq_tx(&self) -> anyhow::Result<Option<Signature>> {
// TODO: Should we make an attempt to settle positive PNL first?
// The problem with it is that small market movements can continuously create
@ -559,11 +527,7 @@ impl<'a> LiquidateHelper<'a> {
}
if self.health_cache.has_perp_open_fills() {
log::info!(
"Account {} has open perp fills, maint_health {}, waiting...",
self.pubkey,
self.maint_health
);
info!("there are open perp fills, waiting...",);
return Ok(None);
}
@ -617,11 +581,10 @@ pub async fn maybe_liquidate_account(
return Ok(false);
}
log::trace!(
"possible candidate: {}, with owner: {}, maint health: {}",
pubkey,
account.fixed.owner,
maint_health,
trace!(
%pubkey,
%maint_health,
"possible candidate",
);
// Fetch a fresh account and re-compute
@ -670,7 +633,7 @@ pub async fn maybe_liquidate_account(
)
.await
{
log::info!("could not refresh after liquidation: {}", e);
info!("could not refresh after liquidation: {}", e);
}
}

View File

@ -1,10 +1,10 @@
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::{Arc, RwLock};
use std::time::Duration;
use std::time::{Duration, Instant};
use anchor_client::Cluster;
use clap::Parser;
use log::*;
use mango_v4::state::{PerpMarketIndex, TokenIndex};
use mango_v4_client::{
account_update_stream, chain_data, keypair_from_cli, snapshot_source, websocket_source,
@ -15,11 +15,13 @@ use mango_v4_client::{
use itertools::Itertools;
use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::pubkey::Pubkey;
use std::collections::HashSet;
use tracing::*;
pub mod liquidate;
pub mod metrics;
pub mod rebalance;
pub mod token_swap_info;
pub mod trigger_tcs;
pub mod util;
use crate::util::{is_mango_account, is_mango_bank, is_mint_info, is_perp_market};
@ -39,16 +41,20 @@ struct CliDotenv {
remaining_args: Vec<std::ffi::OsString>,
}
// Prefer "--rebalance false" over "--no-rebalance" because it works
// better with REBALANCE=false env values.
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
enum BoolArg {
True,
False,
}
#[derive(Parser)]
#[clap()]
struct Cli {
#[clap(short, long, env)]
rpc_url: String,
// TODO: different serum markets could use different serum programs, should come from registered markets
#[clap(long, env)]
serum_program: Pubkey,
#[clap(long, env)]
liqor_mango_account: Pubkey,
@ -70,12 +76,25 @@ struct Cli {
#[clap(long, env, default_value = "50")]
min_health_ratio: f64,
/// if rebalancing is enabled
///
/// typically only disabled for tests where swaps are unavailable
#[clap(long, env, value_enum, default_value = "true")]
rebalance: BoolArg,
/// max slippage to request on swaps to rebalance spot tokens
#[clap(long, env, default_value = "100")]
rebalance_slippage_bps: u64,
/// prioritize each transaction with this many microlamports/cu
#[clap(long, env, default_value = "0")]
prioritization_micro_lamports: u64,
/// use a jupiter mock instead of actual queries
///
/// This is required for devnet testing.
#[clap(long, env, value_enum, default_value = "false")]
mock_jupiter: BoolArg,
}
pub fn encode_address(addr: &Pubkey) -> String {
@ -84,6 +103,8 @@ pub fn encode_address(addr: &Pubkey) -> String {
#[tokio::main]
async fn main() -> anyhow::Result<()> {
mango_v4_client::tracing_subscriber_init();
let args = if let Ok(cli_dotenv) = CliDotenv::try_parse() {
dotenv::from_path(cli_dotenv.dotenv)?;
cli_dotenv.remaining_args
@ -135,13 +156,21 @@ async fn main() -> anyhow::Result<()> {
.unique()
.collect::<Vec<Pubkey>>();
let serum_programs = group_context
.serum3_markets
.values()
.map(|s3| s3.market.serum_program)
.unique()
.collect_vec();
// TODO: Currently the websocket source only supports a single serum program address!
assert_eq!(serum_programs.len(), 1);
//
// feed setup
//
// FUTURE: decouple feed setup and liquidator business logic
// feed should send updates to a channel which liquidator can consume
solana_logger::setup_with_default("info");
info!("startup");
let metrics = metrics::start();
@ -154,7 +183,7 @@ async fn main() -> anyhow::Result<()> {
websocket_source::start(
websocket_source::Config {
rpc_ws_url: ws_url.clone(),
serum_program: cli.serum_program,
serum_program: *serum_programs.first().unwrap(),
open_orders_authority: mango_group,
},
mango_oracles.clone(),
@ -199,14 +228,35 @@ async fn main() -> anyhow::Result<()> {
)?)
};
let token_swap_info_config = token_swap_info::Config {
quote_index: 0, // USDC
quote_amount: 1_000_000_000, // TODO: config, $1000, should be >= tcs_config.max_trigger_quote_amount
mock_jupiter: cli.mock_jupiter == BoolArg::True,
};
let token_swap_info_updater = Arc::new(token_swap_info::TokenSwapInfoUpdater::new(
mango_client.clone(),
token_swap_info_config,
));
let liq_config = liquidate::Config {
min_health_ratio: cli.min_health_ratio,
mock_jupiter: cli.mock_jupiter == BoolArg::True,
// TODO: config
refresh_timeout: Duration::from_secs(30),
};
let tcs_config = trigger_tcs::Config {
min_health_ratio: cli.min_health_ratio,
max_trigger_quote_amount: 1_000_000_000, // TODO: config, $1000
mock_jupiter: cli.mock_jupiter == BoolArg::True,
// TODO: config
refresh_timeout: Duration::from_secs(30),
};
let mut rebalance_interval = tokio::time::interval(Duration::from_secs(5));
let rebalance_config = rebalance::Config {
enabled: cli.rebalance == BoolArg::True,
slippage_bps: cli.rebalance_slippage_bps,
// TODO: config
borrow_settle_excess: 1.05,
@ -220,16 +270,26 @@ async fn main() -> anyhow::Result<()> {
config: rebalance_config,
});
let mut liquidation = LiquidationState {
let mut liquidation = Box::new(LiquidationState {
mango_client,
account_fetcher,
liquidation_config: liq_config,
trigger_tcs_config: tcs_config,
rebalancer: rebalancer.clone(),
accounts_with_errors: Default::default(),
error_skip_threshold: 5,
error_skip_duration: std::time::Duration::from_secs(120),
error_reset_duration: std::time::Duration::from_secs(360),
};
token_swap_info: token_swap_info_updater.clone(),
liq_errors: ErrorTracking {
skip_threshold: 5,
skip_duration: std::time::Duration::from_secs(120),
reset_duration: std::time::Duration::from_secs(360),
..ErrorTracking::default()
},
tcs_errors: ErrorTracking {
skip_threshold: 2,
skip_duration: std::time::Duration::from_secs(120),
reset_duration: std::time::Duration::from_secs(360),
..ErrorTracking::default()
},
});
let (liquidation_trigger_sender, liquidation_trigger_receiver) =
async_channel::bounded::<()>(1);
@ -265,7 +325,7 @@ async fn main() -> anyhow::Result<()> {
let mut state = shared_state.write().unwrap();
if is_mango_account(&account_write.account, &mango_group).is_some() {
// e.g. to render debug logs RUST_LOG="liquidator=debug"
log::debug!(
debug!(
"change to mango account {}...",
&account_write.pubkey.to_string()[0..3]
);
@ -281,15 +341,15 @@ async fn main() -> anyhow::Result<()> {
} else {
let mut must_check_all = false;
if is_mango_bank(&account_write.account, &mango_group).is_some() {
log::debug!("change to bank {}", &account_write.pubkey);
debug!("change to bank {}", &account_write.pubkey);
must_check_all = true;
}
if is_perp_market(&account_write.account, &mango_group).is_some() {
log::debug!("change to perp market {}", &account_write.pubkey);
debug!("change to perp market {}", &account_write.pubkey);
must_check_all = true;
}
if oracles.contains(&account_write.pubkey) {
log::debug!("change to oracle {}", &account_write.pubkey);
debug!("change to oracle {}", &account_write.pubkey);
must_check_all = true;
}
if must_check_all {
@ -328,6 +388,9 @@ async fn main() -> anyhow::Result<()> {
}
});
// Could be refactored to only start the below jobs when the first snapshot is done.
// But need to take care to abort if the above job aborts beforehand.
let rebalance_job = tokio::spawn({
let shared_state = shared_state.clone();
async move {
@ -337,7 +400,7 @@ async fn main() -> anyhow::Result<()> {
continue;
}
if let Err(err) = rebalancer.zero_all_non_quote().await {
log::error!("failed to rebalance liqor: {:?}", err);
error!("failed to rebalance liqor: {:?}", err);
// Workaround: We really need a sequence enforcer in the liquidator since we don't want to
// accidentally send a similar tx again when we incorrectly believe an earlier one got forked
@ -349,6 +412,7 @@ async fn main() -> anyhow::Result<()> {
});
let liquidation_job = tokio::spawn({
let shared_state = shared_state.clone();
async move {
loop {
liquidation_trigger_receiver.recv().await.unwrap();
@ -368,22 +432,69 @@ async fn main() -> anyhow::Result<()> {
state.health_check_accounts = vec![];
}
liquidation
let liquidated = liquidation
.maybe_liquidate_one_and_rebalance(account_addresses.iter())
.await
.unwrap();
if !liquidated {
liquidation
.maybe_take_token_conditional_swap(account_addresses.iter())
.await
.unwrap();
}
}
}
});
let token_swap_info_job = tokio::spawn({
// TODO: configurable interval
let mut interval = tokio::time::interval(Duration::from_secs(60));
let mut min_delay = tokio::time::interval(Duration::from_secs(1));
let shared_state = shared_state.clone();
async move {
loop {
if !shared_state.read().unwrap().one_snapshot_done {
continue;
}
let token_indexes = token_swap_info_updater
.mango_client()
.context
.token_indexes_by_name
.values()
.copied()
.collect_vec();
for token_index in token_indexes {
match token_swap_info_updater.update_one(token_index).await {
Ok(()) => {}
Err(err) => {
warn!(
"failed to update token swap info for token {token_index}: {:?}",
err
);
}
}
min_delay.tick().await;
}
token_swap_info_updater.log_all();
interval.tick().await;
}
}
});
use futures::StreamExt;
let mut jobs: futures::stream::FuturesUnordered<_> =
vec![data_job, rebalance_job, liquidation_job]
.into_iter()
.collect();
let mut jobs: futures::stream::FuturesUnordered<_> = vec![
data_job,
rebalance_job,
liquidation_job,
token_swap_info_job,
]
.into_iter()
.collect();
jobs.next().await;
log::error!("a critical job aborted, exiting");
error!("a critical job aborted, exiting");
Ok(())
}
@ -403,27 +514,68 @@ struct SharedState {
health_check_all: bool,
}
struct ErrorTracking {
#[derive(Clone)]
struct AccountErrorState {
count: u64,
last_at: std::time::Instant,
}
#[derive(Default)]
struct ErrorTracking {
accounts: HashMap<Pubkey, AccountErrorState>,
skip_threshold: u64,
skip_duration: std::time::Duration,
reset_duration: std::time::Duration,
}
impl ErrorTracking {
pub fn had_too_many_errors(&self, pubkey: &Pubkey, now: Instant) -> Option<AccountErrorState> {
if let Some(error_entry) = self.accounts.get(pubkey) {
if error_entry.count >= self.skip_threshold
&& now.duration_since(error_entry.last_at) < self.skip_duration
{
Some(error_entry.clone())
} else {
None
}
} else {
None
}
}
pub fn record_error(&mut self, pubkey: &Pubkey, now: Instant) {
let error_entry = self.accounts.entry(*pubkey).or_insert(AccountErrorState {
count: 0,
last_at: now,
});
if now.duration_since(error_entry.last_at) > self.reset_duration {
error_entry.count = 0;
}
error_entry.count += 1;
error_entry.last_at = now;
}
pub fn clear_errors(&mut self, pubkey: &Pubkey) {
self.accounts.remove(pubkey);
}
}
struct LiquidationState {
mango_client: Arc<MangoClient>,
account_fetcher: Arc<chain_data::AccountFetcher>,
rebalancer: Arc<rebalance::Rebalancer>,
token_swap_info: Arc<token_swap_info::TokenSwapInfoUpdater>,
liquidation_config: liquidate::Config,
accounts_with_errors: HashMap<Pubkey, ErrorTracking>,
error_skip_threshold: u64,
error_skip_duration: std::time::Duration,
error_reset_duration: std::time::Duration,
trigger_tcs_config: trigger_tcs::Config,
liq_errors: ErrorTracking,
tcs_errors: ErrorTracking,
}
impl LiquidationState {
async fn maybe_liquidate_one_and_rebalance<'b>(
&mut self,
accounts_iter: impl Iterator<Item = &'b Pubkey>,
) -> anyhow::Result<()> {
) -> anyhow::Result<bool> {
use rand::seq::SliceRandom;
let mut accounts = accounts_iter.collect::<Vec<&Pubkey>>();
@ -444,29 +596,26 @@ impl LiquidationState {
}
}
if !liquidated_one {
return Ok(());
return Ok(false);
}
if let Err(err) = self.rebalancer.zero_all_non_quote().await {
log::error!("failed to rebalance liqor: {:?}", err);
error!("failed to rebalance liqor: {:?}", err);
}
Ok(())
Ok(true)
}
async fn maybe_liquidate_and_log_error(&mut self, pubkey: &Pubkey) -> anyhow::Result<bool> {
let now = std::time::Instant::now();
let error_tracking = &mut self.liq_errors;
// Skip a pubkey if there've been too many errors recently
if let Some(error_entry) = self.accounts_with_errors.get(pubkey) {
if error_entry.count >= self.error_skip_threshold
&& now.duration_since(error_entry.last_at) < self.error_skip_duration
{
log::trace!(
"skip checking account {pubkey}, had {} errors recently",
error_entry.count
);
return Ok(false);
}
if let Some(error_entry) = error_tracking.had_too_many_errors(pubkey, now) {
trace!(
"skip checking account {pubkey}, had {} errors recently",
error_entry.count
);
return Ok(false);
}
let result = liquidate::maybe_liquidate_account(
@ -479,21 +628,10 @@ impl LiquidationState {
if let Err(err) = result.as_ref() {
// Keep track of pubkeys that had errors
let error_entry = self
.accounts_with_errors
.entry(*pubkey)
.or_insert(ErrorTracking {
count: 0,
last_at: now,
});
if now.duration_since(error_entry.last_at) > self.error_reset_duration {
error_entry.count = 0;
}
error_entry.count += 1;
error_entry.last_at = now;
error_tracking.record_error(pubkey, now);
// Not all errors need to be raised to the user's attention.
let mut log_level = log::Level::Error;
let mut is_error = true;
// Simulation errors due to liqee precondition failures on the liquidation instructions
// will commonly happen if our liquidator is late or if there are chain forks.
@ -502,14 +640,108 @@ impl LiquidationState {
if logs.iter().any(|line| {
line.contains("HealthMustBeNegative") || line.contains("IsNotBankrupt")
}) {
log_level = log::Level::Trace;
is_error = false;
}
}
_ => {}
};
log::log!(log_level, "liquidating account {}: {:?}", pubkey, err);
if is_error {
error!("liquidating account {}: {:?}", pubkey, err);
} else {
trace!("liquidating account {}: {:?}", pubkey, err);
}
} else {
self.accounts_with_errors.remove(pubkey);
error_tracking.clear_errors(pubkey);
}
result
}
async fn maybe_take_token_conditional_swap<'b>(
&mut self,
accounts_iter: impl Iterator<Item = &'b Pubkey>,
) -> anyhow::Result<()> {
use rand::seq::SliceRandom;
let mut accounts = accounts_iter.collect::<Vec<&Pubkey>>();
{
let mut rng = rand::thread_rng();
accounts.shuffle(&mut rng);
}
let mut took_one = false;
for pubkey in accounts {
if self
.maybe_take_conditional_swap_and_log_error(pubkey)
.await
.unwrap_or(false)
{
took_one = true;
break;
}
}
if !took_one {
return Ok(());
}
if let Err(err) = self.rebalancer.zero_all_non_quote().await {
error!("failed to rebalance liqor: {:?}", err);
}
Ok(())
}
async fn maybe_take_conditional_swap_and_log_error(
&mut self,
pubkey: &Pubkey,
) -> anyhow::Result<bool> {
let now = std::time::Instant::now();
let error_tracking = &mut self.tcs_errors;
// Skip a pubkey if there've been too many errors recently
if let Some(error_entry) = error_tracking.had_too_many_errors(pubkey, now) {
trace!(
"skip checking for tcs on account {pubkey}, had {} errors recently",
error_entry.count
);
return Ok(false);
}
let result = trigger_tcs::maybe_execute_token_conditional_swap(
&self.mango_client,
&self.account_fetcher,
&self.token_swap_info,
pubkey,
&self.trigger_tcs_config,
)
.await;
if let Err(err) = result.as_ref() {
// Keep track of pubkeys that had errors
error_tracking.record_error(pubkey, now);
// Not all errors need to be raised to the user's attention.
let mut is_error = true;
// Simulation errors due to liqee precondition failures
// will commonly happen if our liquidator is late or if there are chain forks.
match err.downcast_ref::<MangoClientError>() {
Some(MangoClientError::SendTransactionPreflightFailure { logs, .. }) => {
if logs
.iter()
.any(|line| line.contains("TokenConditionalSwapPriceNotInRange"))
{
is_error = false;
}
}
_ => {}
};
if is_error {
error!("token conditional swap on account {}: {:?}", pubkey, err);
} else {
trace!("token conditional swap on account {}: {:?}", pubkey, err);
}
} else {
error_tracking.clear_errors(pubkey);
}
result

View File

@ -2,6 +2,7 @@ use {
std::collections::HashMap,
std::sync::{atomic, Arc, Mutex, RwLock},
tokio::time,
tracing::*,
};
#[derive(Debug)]
@ -151,7 +152,7 @@ pub fn start() -> Metrics {
0
};
let diff = new_value.wrapping_sub(previous_value) as i64;
log::info!("metric: {}: {} ({:+})", name, new_value, diff);
info!("metric: {}: {} ({:+})", name, new_value, diff);
}
Value::I64(v) => {
let new_value = v.load(atomic::Ordering::Acquire);
@ -164,7 +165,7 @@ pub fn start() -> Metrics {
0
};
let diff = new_value - previous_value;
log::info!("metric: {}: {} ({:+})", name, new_value, diff);
info!("metric: {}: {} ({:+})", name, new_value, diff);
}
Value::String(v) => {
let new_value = v.lock().unwrap();
@ -178,13 +179,11 @@ pub fn start() -> Metrics {
"".into()
};
if *new_value == previous_value {
log::info!("metric: {}: {} (unchanged)", name, &*new_value);
info!("metric: {}: {} (unchanged)", name, &*new_value);
} else {
log::info!(
info!(
"metric: {}: {} (before: {})",
name,
&*new_value,
previous_value
name, &*new_value, previous_value
);
}
}

View File

@ -1,11 +1,12 @@
use itertools::Itertools;
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
use mango_v4::state::{
Bank, BookSide, PlaceOrderType, Side, TokenIndex, TokenPosition, QUOTE_TOKEN_INDEX,
Bank, BookSide, MangoAccountValue, PerpPosition, PlaceOrderType, Side, TokenIndex,
TokenPosition, QUOTE_TOKEN_INDEX,
};
use mango_v4_client::{
chain_data, jupiter::QueryRoute, perp_pnl, AnyhowWrap, JupiterSwapMode, MangoClient,
TokenContext, TransactionBuilder,
PerpMarketContext, TokenContext, TransactionBuilder,
};
use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
@ -14,9 +15,11 @@ use solana_sdk::signature::Signature;
use std::str::FromStr;
use std::sync::Arc;
use std::{collections::HashMap, time::Duration};
use tracing::*;
#[derive(Clone)]
pub struct Config {
pub enabled: bool,
/// Maximum slippage allowed in Jupiter
pub slippage_bps: u64,
/// When closing borrows, the rebalancer can't close token positions exactly.
@ -84,7 +87,14 @@ pub struct Rebalancer {
impl Rebalancer {
pub async fn zero_all_non_quote(&self) -> anyhow::Result<()> {
log::trace!("checking for rebalance: {}", self.mango_account_address);
if !self.config.enabled {
return Ok(());
}
trace!(
pubkey = %self.mango_account_address,
"checking for rebalance"
);
self.rebalance_perps().await?;
self.rebalance_tokens().await?;
@ -106,7 +116,7 @@ impl Rebalancer {
{
// If we don't get fresh data, maybe the tx landed on a fork?
// Rebalance is technically still ok.
log::info!("could not refresh account data: {}", e);
info!("could not refresh account data: {}", e);
return Ok(false);
}
Ok(true)
@ -259,15 +269,16 @@ impl Rebalancer {
if builder.transaction_size_ok()? {
return Ok((builder, full.clone()));
}
log::trace!(
"full route from {} to {} does not fit in a tx, market_info.label {}",
full.input_mint,
full.output_mint,
full.route
trace!(
market_info_label = full
.route
.market_infos
.first()
.map(|v| v.label.clone())
.unwrap_or_else(|| "no market_info".into())
.unwrap_or_else(|| "no market_info".into()),
%full.input_mint,
%full.output_mint,
"full route does not fit in a tx",
);
if alternatives.is_empty() {
@ -313,7 +324,7 @@ impl Rebalancer {
})
.try_collect();
let tokens = tokens?;
log::trace!("account tokens: {:?}", tokens);
trace!(?tokens, "account tokens");
for (token_index, token_state) in tokens {
let token = self.mango_client.context.token(token_index);
@ -351,13 +362,13 @@ impl Rebalancer {
.context
.token_by_mint(&route.input_mint)
.unwrap();
log::info!(
"bought {} {} for {} {} in tx {}",
info!(
%txsig,
"bought {} {} for {} {}",
token.native_to_ui(I80F48::from_str(&route.route.out_amount).unwrap()),
token.name,
in_token.native_to_ui(I80F48::from_str(&route.route.in_amount).unwrap()),
in_token.name,
txsig,
);
if !self.refresh_mango_account_after_tx(txsig).await? {
return Ok(());
@ -382,13 +393,13 @@ impl Rebalancer {
.context
.token_by_mint(&route.output_mint)
.unwrap();
log::info!(
"sold {} {} for {} {} in tx {}",
info!(
%txsig,
"sold {} {} for {} {}",
token.native_to_ui(I80F48::from_str(&route.route.in_amount).unwrap()),
token.name,
out_token.native_to_ui(I80F48::from_str(&route.route.out_amount).unwrap()),
out_token.name,
txsig,
);
if !self.refresh_mango_account_after_tx(txsig).await? {
return Ok(());
@ -411,11 +422,11 @@ impl Rebalancer {
.mango_client
.token_withdraw(token_mint, u64::MAX, allow_borrow)
.await?;
log::info!(
"withdrew {} {} to liqor wallet in {}",
info!(
%txsig,
"withdrew {} {} to liqor wallet",
token.native_to_ui(amount),
token.name,
txsig
);
if !self.refresh_mango_account_after_tx(txsig).await? {
return Ok(());
@ -432,174 +443,170 @@ impl Rebalancer {
Ok(())
}
async fn rebalance_perps(&self) -> anyhow::Result<()> {
#[instrument(
skip_all,
fields(
perp_market_name = perp.market.name(),
base_lots = perp_position.base_position_lots(),
effective_lots = perp_position.effective_base_position_lots(),
quote_native = %perp_position.quote_position_native()
)
)]
async fn rebalance_perp(
&self,
account: &MangoAccountValue,
perp: &PerpMarketContext,
perp_position: &PerpPosition,
) -> anyhow::Result<bool> {
let now_ts: u64 = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_secs()
.try_into()?;
let account = self
.account_fetcher
.fetch_mango_account(&self.mango_account_address)?;
let base_lots = perp_position.base_position_lots();
let effective_lots = perp_position.effective_base_position_lots();
let quote_native = perp_position.quote_position_native();
if effective_lots != 0 {
// send an ioc order to reduce the base position
let oracle_account_data = self.account_fetcher.fetch_raw(&perp.market.oracle)?;
let oracle_account =
KeyedAccountSharedData::new(perp.market.oracle, oracle_account_data);
let oracle_price = perp.market.oracle_price(&oracle_account, None)?;
let oracle_price_lots = perp.market.native_price_to_lot(oracle_price);
let (side, order_price, oo_lots) = if effective_lots > 0 {
(
Side::Ask,
oracle_price * (I80F48::ONE - perp.market.base_liquidation_fee),
perp_position.asks_base_lots,
)
} else {
(
Side::Bid,
oracle_price * (I80F48::ONE + perp.market.base_liquidation_fee),
perp_position.bids_base_lots,
)
};
let price_lots = perp.market.native_price_to_lot(order_price);
let max_base_lots = effective_lots.abs() - oo_lots;
if max_base_lots <= 0 {
warn!(?side, oo_lots, "cannot place reduce-only order",);
return Ok(true);
}
// Check the orderbook before sending the ioc order to see if we could
// even match anything. That way we don't need to pay the tx fee and
// ioc penalty fee unnecessarily.
let opposite_side_key = match side.invert_side() {
Side::Bid => perp.market.bids,
Side::Ask => perp.market.asks,
};
let bookside = Box::new(self.account_fetcher.fetch::<BookSide>(&opposite_side_key)?);
if bookside.quantity_at_price(price_lots, now_ts, oracle_price_lots) <= 0 {
warn!(
other_side = ?side.invert_side(),
%order_price,
%oracle_price,
"no liquidity",
);
return Ok(true);
}
let txsig = self
.mango_client
.perp_place_order(
perp_position.market_index,
side,
price_lots,
max_base_lots,
i64::MAX,
0,
PlaceOrderType::ImmediateOrCancel,
true, // reduce only
0,
10,
mango_v4::state::SelfTradeBehavior::DecrementTake,
)
.await?;
info!(
%txsig,
%order_price,
"attempt to ioc reduce perp base position"
);
if !self.refresh_mango_account_after_tx(txsig).await? {
return Ok(false);
}
} else if base_lots == 0 && quote_native != 0 {
// settle pnl
let direction = if quote_native > 0 {
perp_pnl::Direction::MaxNegative
} else {
perp_pnl::Direction::MaxPositive
};
let counters = perp_pnl::fetch_top(
&self.mango_client.context,
self.account_fetcher.as_ref(),
perp_position.market_index,
direction,
2,
)
.await?;
if counters.is_empty() {
// If we can't settle some positive PNL because we're lacking a suitable counterparty,
// then liquidation should continue, even though this step produced no transaction
info!("could not settle perp pnl on perp market: no counterparty",);
return Ok(true);
}
let (counter_key, counter_acc, _counter_pnl) = counters.first().unwrap();
let (account_a, account_b) = if quote_native > 0 {
(
(&self.mango_account_address, account),
(counter_key, counter_acc),
)
} else {
(
(counter_key, counter_acc),
(&self.mango_account_address, account),
)
};
let txsig = self
.mango_client
.perp_settle_pnl(perp_position.market_index, account_a, account_b)
.await?;
info!(%txsig, "settled perp pnl");
if !self.refresh_mango_account_after_tx(txsig).await? {
return Ok(false);
}
} else if base_lots == 0 && quote_native == 0 {
// close perp position
let txsig = self
.mango_client
.perp_deactivate_position(perp_position.market_index)
.await?;
info!(
%txsig, "closed perp position"
);
if !self.refresh_mango_account_after_tx(txsig).await? {
return Ok(false);
}
} else {
// maybe we're still waiting for consume_events
info!("cannot deactivate perp position, waiting for consume events?");
}
Ok(true)
}
async fn rebalance_perps(&self) -> anyhow::Result<()> {
let account = Box::new(
self.account_fetcher
.fetch_mango_account(&self.mango_account_address)?,
);
for perp_position in account.active_perp_positions() {
let perp = self.mango_client.context.perp(perp_position.market_index);
let base_lots = perp_position.base_position_lots();
let effective_lots = perp_position.effective_base_position_lots();
let quote_native = perp_position.quote_position_native();
log::info!(
"active perp position on {}, base lots: {}, effective lots: {}, quote native: {}",
perp.market.name(),
base_lots,
effective_lots,
quote_native,
);
if effective_lots != 0 {
// send an ioc order to reduce the base position
let oracle_account_data = self.account_fetcher.fetch_raw(&perp.market.oracle)?;
let oracle_account =
KeyedAccountSharedData::new(perp.market.oracle, oracle_account_data);
let oracle_price = perp.market.oracle_price(&oracle_account, None)?;
let oracle_price_lots = perp.market.native_price_to_lot(oracle_price);
let (side, order_price, oo_lots) = if effective_lots > 0 {
(
Side::Ask,
oracle_price * (I80F48::ONE - perp.market.base_liquidation_fee),
perp_position.asks_base_lots,
)
} else {
(
Side::Bid,
oracle_price * (I80F48::ONE + perp.market.base_liquidation_fee),
perp_position.bids_base_lots,
)
};
let price_lots = perp.market.native_price_to_lot(order_price);
let max_base_lots = effective_lots.abs() - oo_lots;
if max_base_lots <= 0 {
log::warn!(
"cannot place reduce-only order on {} {:?}, base pos: {}, in open orders: {}",
perp.market.name(),
side,
effective_lots,
oo_lots,
);
continue;
}
// Check the orderbook before sending the ioc order to see if we could
// even match anything. That way we don't need to pay the tx fee and
// ioc penalty fee unnecessarily.
let opposite_side_key = match side.invert_side() {
Side::Bid => perp.market.bids,
Side::Ask => perp.market.asks,
};
let bookside = self.account_fetcher.fetch::<BookSide>(&opposite_side_key)?;
if bookside.quantity_at_price(price_lots, now_ts, oracle_price_lots) <= 0 {
log::warn!(
"no liquidity on {} {:?} at price {}, oracle price {}",
perp.market.name(),
side.invert_side(),
order_price,
oracle_price,
);
continue;
}
let txsig = self
.mango_client
.perp_place_order(
perp_position.market_index,
side,
price_lots,
max_base_lots,
i64::MAX,
0,
PlaceOrderType::ImmediateOrCancel,
true, // reduce only
0,
10,
mango_v4::state::SelfTradeBehavior::DecrementTake,
)
.await?;
log::info!(
"attempt to ioc reduce perp base position of {} {} at price {} in {}",
perp_position.base_position_native(&perp.market),
perp.market.name(),
order_price,
txsig
);
if !self.refresh_mango_account_after_tx(txsig).await? {
return Ok(());
}
} else if base_lots == 0 && quote_native != 0 {
// settle pnl
let direction = if quote_native > 0 {
perp_pnl::Direction::MaxNegative
} else {
perp_pnl::Direction::MaxPositive
};
let counters = perp_pnl::fetch_top(
&self.mango_client.context,
self.account_fetcher.as_ref(),
perp_position.market_index,
direction,
2,
)
.await?;
if counters.is_empty() {
// If we can't settle some positive PNL because we're lacking a suitable counterparty,
// then liquidation should continue, even though this step produced no transaction
log::info!(
"could not settle perp pnl on perp market {}: no counterparty",
perp.market.name()
);
continue;
}
let (counter_key, counter_acc, _counter_pnl) = counters.first().unwrap();
let (account_a, account_b) = if quote_native > 0 {
(
(&self.mango_account_address, &account),
(counter_key, counter_acc),
)
} else {
(
(counter_key, counter_acc),
(&self.mango_account_address, &account),
)
};
let txsig = self
.mango_client
.perp_settle_pnl(perp_position.market_index, account_a, account_b)
.await?;
log::info!("settled perp {} pnl, tx sig {}", perp.market.name(), txsig);
if !self.refresh_mango_account_after_tx(txsig).await? {
return Ok(());
}
} else if base_lots == 0 && quote_native == 0 {
// close perp position
let txsig = self
.mango_client
.perp_deactivate_position(perp_position.market_index)
.await?;
log::info!(
"closed perp position on {} in {}",
perp.market.name(),
txsig
);
if !self.refresh_mango_account_after_tx(txsig).await? {
return Ok(());
}
} else {
// maybe we're still waiting for consume_events
log::info!(
"cannot deactivate perp {} position, base lots {}, effective lots {}, quote {}",
perp.market.name(),
perp_position.base_position_lots(),
effective_lots,
perp_position.quote_position_native()
);
if !self.rebalance_perp(&account, perp, perp_position).await? {
return Ok(());
}
}

View File

@ -0,0 +1,162 @@
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use itertools::Itertools;
use tracing::*;
use mango_v4::state::TokenIndex;
use mango_v4_client::jupiter::QueryRoute;
use mango_v4_client::{JupiterSwapMode, MangoClient};
use crate::util;
pub struct Config {
pub quote_index: TokenIndex,
pub quote_amount: u64,
pub mock_jupiter: bool,
}
#[derive(Clone, Default)]
pub struct TokenSwapInfo {
/// multiplier to the oracle price for executing a buy, so 1.5 would mean buying 50% over oracle price
pub buy_over_oracle: f64,
/// multiplier to the oracle price for executing a sell,
/// but with the price inverted, so values > 1 mean a worse deal than oracle price
pub sell_over_oracle: f64,
}
/// Track the buy/sell slippage for tokens
///
/// Needed to evaluate whether a token conditional swap premium might be good enough
/// without having to query each time.
pub struct TokenSwapInfoUpdater {
mango_client: Arc<MangoClient>,
swap_infos: RwLock<HashMap<TokenIndex, TokenSwapInfo>>,
config: Config,
}
impl TokenSwapInfoUpdater {
pub fn new(mango_client: Arc<MangoClient>, config: Config) -> Self {
Self {
mango_client,
swap_infos: RwLock::new(HashMap::new()),
config,
}
}
pub fn mango_client(&self) -> &Arc<MangoClient> {
&self.mango_client
}
fn update(&self, token_index: TokenIndex, slippage: TokenSwapInfo) {
let mut lock = self.swap_infos.write().unwrap();
let entry = lock.entry(token_index).or_default();
*entry = slippage;
}
pub fn swap_info(&self, token_index: TokenIndex) -> Option<TokenSwapInfo> {
let lock = self.swap_infos.read().unwrap();
lock.get(&token_index).cloned()
}
/// oracle price is how many "in" tokens to pay for one "out" token
fn price_over_oracle(oracle_price: f64, route: QueryRoute) -> anyhow::Result<f64> {
let in_amount = route.in_amount.parse::<f64>()?;
let out_amount = route.out_amount.parse::<f64>()?;
let actual_price = in_amount / out_amount;
Ok(actual_price / oracle_price)
}
pub async fn update_one(&self, token_index: TokenIndex) -> anyhow::Result<()> {
// since we're only quoting, the slippage does not matter
let slippage = 100;
let quote_index = self.config.quote_index;
if token_index == quote_index {
self.update(quote_index, TokenSwapInfo::default());
return Ok(());
}
let token_mint = self.mango_client.context.mint_info(token_index).mint;
let quote_mint = self.mango_client.context.mint_info(quote_index).mint;
// these prices are in USD, which doesn't exist on chain
let token_price = self
.mango_client
.bank_oracle_price(token_index)
.await?
.to_num::<f64>();
let quote_price = self
.mango_client
.bank_oracle_price(quote_index)
.await?
.to_num::<f64>();
// prices for the pair
let quote_per_token_price = token_price / quote_price;
let token_per_quote_price = quote_price / token_price;
let token_amount = (self.config.quote_amount as f64 * token_per_quote_price) as u64;
let sell_route = util::jupiter_route(
&self.mango_client,
token_mint,
quote_mint,
token_amount,
slippage,
JupiterSwapMode::ExactIn,
false,
self.config.mock_jupiter,
)
.await?;
let buy_route = util::jupiter_route(
&self.mango_client,
quote_mint,
token_mint,
self.config.quote_amount,
slippage,
JupiterSwapMode::ExactIn,
false,
self.config.mock_jupiter,
)
.await?;
let buy_over_oracle = Self::price_over_oracle(quote_per_token_price, buy_route)?;
let sell_over_oracle = Self::price_over_oracle(token_per_quote_price, sell_route)?;
self.update(
token_index,
TokenSwapInfo {
buy_over_oracle,
sell_over_oracle,
},
);
Ok(())
}
pub fn log_all(&self) {
let mut tokens = self
.mango_client
.context
.token_indexes_by_name
.clone()
.into_iter()
.collect_vec();
tokens.sort_by(|a, b| a.0.cmp(&b.0));
let infos = self.swap_infos.read().unwrap();
let mut msg = String::new();
for (token, token_index) in tokens {
let info = infos
.get(&token_index)
.map(|info| {
format!(
"buy {}, sell {}",
info.buy_over_oracle, info.sell_over_oracle
)
})
.unwrap_or_else(|| "no data".into());
msg.push_str(&format!("token {token}, {info}"));
}
trace!("swap infos:{}", msg);
}
}

View File

@ -0,0 +1,306 @@
use std::time::Duration;
use mango_v4::state::{MangoAccountValue, TokenConditionalSwap};
use mango_v4_client::{chain_data, health_cache, JupiterSwapMode, MangoClient};
use rand::seq::SliceRandom;
use tracing::*;
use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
use crate::{token_swap_info, util};
pub struct Config {
pub min_health_ratio: f64,
pub max_trigger_quote_amount: u64,
pub refresh_timeout: Duration,
pub mock_jupiter: bool,
}
async fn tcs_is_in_price_range(
mango_client: &MangoClient,
tcs: &TokenConditionalSwap,
) -> anyhow::Result<bool> {
let buy_token_price = mango_client.bank_oracle_price(tcs.buy_token_index).await?;
let sell_token_price = mango_client.bank_oracle_price(tcs.sell_token_index).await?;
let base_price = (buy_token_price / sell_token_price).to_num();
if !tcs.price_in_range(base_price) {
return Ok(false);
}
return Ok(true);
}
fn tcs_has_plausible_premium(
tcs: &TokenConditionalSwap,
token_swap_info: &token_swap_info::TokenSwapInfoUpdater,
) -> anyhow::Result<bool> {
// The premium the taker receives needs to take taker fees into account
let premium = tcs.taker_price(tcs.premium_price(1.0)) as f64;
// Never take tcs where the fee exceeds the premium and the triggerer exchanges
// tokens at below oracle price.
if premium < 1.0 {
return Ok(false);
}
let buy_info = token_swap_info
.swap_info(tcs.buy_token_index)
.ok_or_else(|| anyhow::anyhow!("no swap info for token {}", tcs.buy_token_index))?;
let sell_info = token_swap_info
.swap_info(tcs.sell_token_index)
.ok_or_else(|| anyhow::anyhow!("no swap info for token {}", tcs.sell_token_index))?;
// If this is 1.0 then the exchange can (probably) happen at oracle price.
// 1.5 would mean we need to pay 50% more than oracle etc.
let cost = buy_info.buy_over_oracle * sell_info.sell_over_oracle;
Ok(cost <= premium)
}
async fn tcs_is_interesting(
mango_client: &MangoClient,
tcs: &TokenConditionalSwap,
token_swap_info: &token_swap_info::TokenSwapInfoUpdater,
now_ts: u64,
) -> anyhow::Result<bool> {
Ok(!tcs.is_expired(now_ts)
&& tcs_is_in_price_range(mango_client, tcs).await?
&& tcs_has_plausible_premium(tcs, token_swap_info)?)
}
#[allow(clippy::too_many_arguments)]
async fn maybe_execute_token_conditional_swap_inner(
mango_client: &MangoClient,
account_fetcher: &chain_data::AccountFetcher,
token_swap_info: &token_swap_info::TokenSwapInfoUpdater,
pubkey: &Pubkey,
liqee_old: &MangoAccountValue,
tcs_id: u64,
config: &Config,
now_ts: u64,
) -> anyhow::Result<bool> {
let health_cache = health_cache::new(&mango_client.context, account_fetcher, &liqee_old)
.await
.context("creating health cache 1")?;
if health_cache.is_liquidatable() {
return Ok(false);
}
// get a fresh account and re-check the tcs and health
let liqee = account_fetcher.fetch_fresh_mango_account(pubkey).await?;
let (_, tcs) = liqee.token_conditional_swap_by_id(tcs_id)?;
if !tcs_is_interesting(mango_client, tcs, token_swap_info, now_ts).await? {
return Ok(false);
}
let health_cache = health_cache::new(&mango_client.context, account_fetcher, &liqee)
.await
.context("creating health cache 1")?;
if health_cache.is_liquidatable() {
return Ok(false);
}
execute_token_conditional_swap(mango_client, account_fetcher, pubkey, config, &liqee, tcs).await
}
#[allow(clippy::too_many_arguments)]
#[instrument(skip_all, fields(%pubkey, tcs_id = tcs.id))]
async fn execute_token_conditional_swap(
mango_client: &MangoClient,
account_fetcher: &chain_data::AccountFetcher,
pubkey: &Pubkey,
config: &Config,
liqee: &MangoAccountValue,
tcs: &TokenConditionalSwap,
) -> anyhow::Result<bool> {
let liqor_min_health_ratio = I80F48::from_num(config.min_health_ratio);
// Compute the max viable swap (for liqor and liqee) and min it
let buy_token_price = mango_client.bank_oracle_price(tcs.buy_token_index).await?;
let sell_token_price = mango_client.bank_oracle_price(tcs.sell_token_index).await?;
let base_price = buy_token_price / sell_token_price;
let premium_price = tcs.premium_price(base_price.to_num());
let maker_price = I80F48::from_num(tcs.maker_price(premium_price));
let taker_price = I80F48::from_num(tcs.taker_price(premium_price));
let max_take_quote = I80F48::from(config.max_trigger_quote_amount);
// The background here is that the program considers bringing the liqee health ratio
// below 1% as "the tcs was completely fulfilled" and then closes the tcs.
// Choosing a value too close to 0 is problematic, since then small oracle fluctuations
// could bring the final health below 0 and make the triggering invalid!
let liqee_target_health_ratio = I80F48::from_num(0.5);
let max_sell_token_to_liqor = util::max_swap_source(
mango_client,
account_fetcher,
&liqee,
tcs.sell_token_index,
tcs.buy_token_index,
I80F48::ONE / maker_price,
liqee_target_health_ratio,
)
.await?
.min(max_take_quote / sell_token_price)
.floor()
.to_num::<u64>()
.min(tcs.remaining_sell());
let max_buy_token_to_liqee = util::max_swap_source(
mango_client,
account_fetcher,
&mango_client.mango_account().await?,
tcs.buy_token_index,
tcs.sell_token_index,
taker_price,
liqor_min_health_ratio,
)
.await?
.min(max_take_quote / buy_token_price)
.floor()
.to_num::<u64>()
.min(tcs.remaining_buy());
if max_sell_token_to_liqor == 0 || max_buy_token_to_liqee == 0 {
return Ok(false);
}
// Final check of the reverse trade on jupiter
{
let buy_mint = mango_client.context.mint_info(tcs.buy_token_index).mint;
let sell_mint = mango_client.context.mint_info(tcs.sell_token_index).mint;
let swap_mode = JupiterSwapMode::ExactIn;
// The slippage does not matter since we're not going to execute it
let slippage = 100;
let input_amount = max_sell_token_to_liqor.min(
(I80F48::from(max_buy_token_to_liqee) * taker_price)
.floor()
.to_num(),
);
let route = util::jupiter_route(
mango_client,
sell_mint,
buy_mint,
input_amount,
slippage,
swap_mode,
false,
config.mock_jupiter,
)
.await?;
let sell_amount = route.in_amount.parse::<f64>()?;
let buy_amount = route.out_amount.parse::<f64>()?;
let swap_price = sell_amount / buy_amount;
if swap_price > taker_price.to_num::<f64>() {
trace!(
max_buy = max_buy_token_to_liqee,
max_sell = max_sell_token_to_liqor,
jupiter_swap_price = %swap_price,
tcs_taker_price = %taker_price,
"skipping token conditional swap because of prices",
);
return Ok(false);
}
}
trace!(
max_buy = max_buy_token_to_liqee,
max_sell = max_sell_token_to_liqor,
"executing token conditional swap",
);
let txsig = mango_client
.token_conditional_swap_trigger(
(pubkey, &liqee),
tcs.id,
max_buy_token_to_liqee,
max_sell_token_to_liqor,
)
.await?;
info!(
%txsig,
"Executed token conditional swap",
);
let slot = account_fetcher.transaction_max_slot(&[txsig]).await?;
if let Err(e) = account_fetcher
.refresh_accounts_via_rpc_until_slot(
&[*pubkey, mango_client.mango_account_address],
slot,
config.refresh_timeout,
)
.await
{
info!(%txsig, "could not refresh after tcs execution: {}", e);
}
Ok(true)
}
#[allow(clippy::too_many_arguments)]
#[instrument(skip_all, fields(%pubkey, tcs_id))]
pub async fn remove_expired_token_conditional_swap(
mango_client: &MangoClient,
pubkey: &Pubkey,
liqee: &MangoAccountValue,
tcs_id: u64,
) -> anyhow::Result<bool> {
let txsig = mango_client
.token_conditional_swap_trigger((pubkey, &liqee), tcs_id, 0, 0)
.await?;
info!(
%txsig,
"Removed expired token conditional swap",
);
Ok(true)
}
#[allow(clippy::too_many_arguments)]
pub async fn maybe_execute_token_conditional_swap(
mango_client: &MangoClient,
account_fetcher: &chain_data::AccountFetcher,
token_swap_info: &token_swap_info::TokenSwapInfoUpdater,
pubkey: &Pubkey,
config: &Config,
) -> anyhow::Result<bool> {
let now_ts: u64 = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_secs()
.try_into()?;
let liqee = account_fetcher.fetch_mango_account(pubkey)?;
// Find an interesting triggerable conditional swap
let mut tcs_shuffled = liqee.active_token_conditional_swaps().collect::<Vec<&_>>();
{
let mut rng = rand::thread_rng();
tcs_shuffled.shuffle(&mut rng);
}
for tcs in tcs_shuffled.iter() {
if tcs_is_interesting(mango_client, tcs, token_swap_info, now_ts).await? {
return maybe_execute_token_conditional_swap_inner(
mango_client,
account_fetcher,
token_swap_info,
pubkey,
&liqee,
tcs.id,
config,
now_ts,
)
.await;
}
}
for tcs in tcs_shuffled {
if tcs.is_expired(now_ts) {
return remove_expired_token_conditional_swap(mango_client, pubkey, &liqee, tcs.id)
.await;
}
}
Ok(false)
}

View File

@ -31,7 +31,6 @@ itertools = "0.10.3"
jemallocator = "0.3.2"
jsonrpc-core = "18.0.0"
jsonrpc-core-client = { version = "18.0.0", features = ["ws", "http", "tls"] }
log = "0.4"
mango-v4 = { path = "../../programs/mango-v4", features = ["client"] }
mango-v4-client = { path = "../../lib/client" }
once_cell = "1.12.0"
@ -51,3 +50,4 @@ solana-sdk = { workspace = true }
tokio = { version = "1", features = ["full"] }
tokio-stream = { version = "0.1.9"}
tokio-tungstenite = "0.16.1"
tracing = "0.1"

View File

@ -4,12 +4,12 @@ use std::time::Duration;
use anchor_client::Cluster;
use clap::Parser;
use log::*;
use mango_v4::state::{PerpMarketIndex, TokenIndex};
use mango_v4_client::{
account_update_stream, chain_data, keypair_from_cli, snapshot_source, websocket_source,
AsyncChannelSendUnlessFull, Client, MangoClient, MangoGroupContext, TransactionBuilderConfig,
};
use tracing::*;
use itertools::Itertools;
use solana_sdk::commitment_config::CommitmentConfig;
@ -75,6 +75,8 @@ pub fn encode_address(addr: &Pubkey) -> String {
#[tokio::main]
async fn main() -> anyhow::Result<()> {
mango_v4_client::tracing_subscriber_init();
let args = if let Ok(cli_dotenv) = CliDotenv::try_parse() {
dotenv::from_path(cli_dotenv.dotenv)?;
cli_dotenv.remaining_args
@ -234,7 +236,7 @@ async fn main() -> anyhow::Result<()> {
let mut state = shared_state.write().unwrap();
if is_mango_account(&account_write.account, &mango_group).is_some() {
// e.g. to render debug logs RUST_LOG="liquidator=debug"
log::debug!(
debug!(
"change to mango account {}...",
&account_write.pubkey.to_string()[0..3]
);
@ -250,15 +252,15 @@ async fn main() -> anyhow::Result<()> {
} else {
let mut must_check_all = false;
if is_mango_bank(&account_write.account, &mango_group).is_some() {
log::debug!("change to bank {}", &account_write.pubkey);
debug!("change to bank {}", &account_write.pubkey);
must_check_all = true;
}
if is_perp_market(&account_write.account, &mango_group).is_some() {
log::debug!("change to perp market {}", &account_write.pubkey);
debug!("change to perp market {}", &account_write.pubkey);
must_check_all = true;
}
if oracles.contains(&account_write.pubkey) {
log::debug!("change to oracle {}", &account_write.pubkey);
debug!("change to oracle {}", &account_write.pubkey);
must_check_all = true;
}
if must_check_all {
@ -324,7 +326,7 @@ async fn main() -> anyhow::Result<()> {
vec![data_job, settle_job].into_iter().collect();
jobs.next().await;
log::error!("a critical job aborted, exiting");
error!("a critical job aborted, exiting");
Ok(())
}

View File

@ -2,6 +2,7 @@ use {
std::collections::HashMap,
std::sync::{atomic, Arc, Mutex, RwLock},
tokio::time,
tracing::*,
};
#[derive(Debug)]
@ -151,7 +152,7 @@ pub fn start() -> Metrics {
0
};
let diff = new_value.wrapping_sub(previous_value) as i64;
log::info!("metric: {}: {} ({:+})", name, new_value, diff);
info!("metric: {}: {} ({:+})", name, new_value, diff);
}
Value::I64(v) => {
let new_value = v.load(atomic::Ordering::Acquire);
@ -164,7 +165,7 @@ pub fn start() -> Metrics {
0
};
let diff = new_value - previous_value;
log::info!("metric: {}: {} ({:+})", name, new_value, diff);
info!("metric: {}: {} ({:+})", name, new_value, diff);
}
Value::String(v) => {
let new_value = v.lock().unwrap();
@ -178,13 +179,11 @@ pub fn start() -> Metrics {
"".into()
};
if *new_value == previous_value {
log::info!("metric: {}: {} (unchanged)", name, &*new_value);
info!("metric: {}: {} (unchanged)", name, &*new_value);
} else {
log::info!(
info!(
"metric: {}: {} (before: {})",
name,
&*new_value,
previous_value
name, &*new_value, previous_value
);
}
}

View File

@ -15,6 +15,7 @@ use solana_sdk::signature::Signature;
use solana_sdk::signer::Signer;
use solana_sdk::transaction::VersionedTransaction;
use tracing::*;
use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
pub struct Config {
@ -44,7 +45,7 @@ fn perp_markets_and_prices(
|v: anyhow::Result<(PerpMarketIndex, (PerpMarket, I80F48))>| match v {
Ok(v) => Some(v),
Err(err) => {
log::error!("error while retriving perp market and price: {:?}", err);
error!("error while retriving perp market and price: {:?}", err);
None
}
},
@ -279,12 +280,12 @@ impl<'a> SettleBatchProcessor<'a> {
.map_err(|e| prettify_solana_client_error(e));
if let Err(err) = send_result {
log::info!("error while sending settle batch: {}", err);
info!("error while sending settle batch: {}", err);
return Ok(None);
}
let txsig = send_result.unwrap();
log::info!("sent settle tx: {txsig}");
info!("sent settle tx: {txsig}");
Ok(Some(txsig))
}

View File

@ -16,6 +16,7 @@ anyhow = "1.0"
async-channel = "1.6"
async-once-cell = { version = "0.4.2", features = ["unpin"] }
async-trait = "0.1.52"
atty = "0.2"
fixed = { workspace = true, features = ["serde", "borsh"] }
futures = "0.3.25"
itertools = "0.10.3"
@ -33,7 +34,6 @@ solana-address-lookup-table-program = { workspace = true }
mango-feeds-connector = "0.1.1"
spl-associated-token-account = "1.0.3"
thiserror = "1.0.31"
log = "0.4"
reqwest = "0.11.11"
tokio = { version = "1", features = ["full"] }
tokio-stream = { version = "0.1.9"}
@ -41,3 +41,5 @@ serde = "1.0.141"
serde_json = "1.0.82"
base64 = "0.13.0"
bincode = "1.3.3"
tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

View File

@ -1,8 +1,8 @@
use solana_client::rpc_response::{Response, RpcKeyedAccount};
use solana_sdk::{account::AccountSharedData, pubkey::Pubkey};
use log::*;
use std::{str::FromStr, sync::Arc};
use tracing::*;
use crate::chain_data;

View File

@ -3,7 +3,7 @@ use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use anchor_client::{ClientError, Cluster};
use anchor_client::Cluster;
use anchor_lang::__private::bytemuck;
use anchor_lang::prelude::System;
@ -33,7 +33,7 @@ use solana_sdk::signer::keypair;
use solana_sdk::transaction::TransactionError;
use crate::account_fetcher::*;
use crate::context::{MangoGroupContext, Serum3MarketContext, TokenContext};
use crate::context::MangoGroupContext;
use crate::gpa::{fetch_anchor_account, fetch_mango_accounts};
use crate::jupiter;
@ -188,8 +188,8 @@ impl MangoClient {
) -> anyhow::Result<(Pubkey, Signature)> {
let account = Pubkey::find_program_address(
&[
group.as_ref(),
b"MangoAccount".as_ref(),
group.as_ref(),
owner.pubkey().as_ref(),
&account_num.to_le_bytes(),
],
@ -434,53 +434,52 @@ impl MangoClient {
Ok(price)
}
pub async fn get_oracle_price(
pub async fn perp_oracle_price(
&self,
token_name: &str,
) -> Result<pyth_sdk_solana::Price, anyhow::Error> {
let token_index = *self.context.token_indexes_by_name.get(token_name).unwrap();
let mint_info = self.context.mint_info(token_index);
let oracle_account = self
perp_market_index: PerpMarketIndex,
) -> anyhow::Result<I80F48> {
let perp = self.context.perp(perp_market_index);
let oracle = self
.account_fetcher
.fetch_raw_account(&mint_info.oracle)
.fetch_raw_account(&perp.market.oracle)
.await?;
Ok(pyth_sdk_solana::load_price(&oracle_account.data()).unwrap())
let price = perp.market.oracle_price(
&KeyedAccountSharedData::new(perp.market.oracle, oracle.into()),
None,
)?;
Ok(price)
}
//
// Serum3
//
pub async fn serum3_create_open_orders(&self, name: &str) -> anyhow::Result<Signature> {
pub fn serum3_create_open_orders_instruction(
&self,
market_index: Serum3MarketIndex,
) -> Instruction {
let account_pubkey = self.mango_account_address;
let market_index = *self
.context
.serum3_market_indexes_by_name
.get(name)
.unwrap();
let serum3_info = self.context.serum3_markets.get(&market_index).unwrap();
let s3 = self.context.serum3(market_index);
let open_orders = Pubkey::find_program_address(
&[
account_pubkey.as_ref(),
b"Serum3OO".as_ref(),
serum3_info.address.as_ref(),
account_pubkey.as_ref(),
s3.address.as_ref(),
],
&mango_v4::ID,
)
.0;
let ix = Instruction {
Instruction {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::Serum3CreateOpenOrders {
group: self.group(),
account: account_pubkey,
serum_market: serum3_info.address,
serum_program: serum3_info.market.serum_program,
serum_market_external: serum3_info.market.serum_market_external,
serum_market: s3.address,
serum_program: s3.market.serum_program,
serum_market_external: s3.market.serum_market_external,
open_orders,
owner: self.owner(),
payer: self.owner(),
@ -492,116 +491,47 @@ impl MangoClient {
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::Serum3CreateOpenOrders {},
),
};
}
}
pub async fn serum3_create_open_orders(&self, name: &str) -> anyhow::Result<Signature> {
let market_index = self.context.serum3_market_index(name);
let ix = self.serum3_create_open_orders_instruction(market_index);
self.send_and_confirm_owner_tx(vec![ix]).await
}
fn serum3_data_by_market_name<'a>(&'a self, name: &str) -> Result<Serum3Data<'a>, ClientError> {
let market_index = *self
.context
.serum3_market_indexes_by_name
.get(name)
.unwrap();
self.serum3_data_by_market_index(market_index)
}
fn serum3_data_by_market_index<'a>(
&'a self,
market_index: Serum3MarketIndex,
) -> Result<Serum3Data<'a>, ClientError> {
let serum3_info = self.context.serum3_markets.get(&market_index).unwrap();
let quote_info = self.context.token(serum3_info.market.quote_token_index);
let base_info = self.context.token(serum3_info.market.base_token_index);
Ok(Serum3Data {
market_index,
market: serum3_info,
quote: quote_info,
base: base_info,
})
}
#[allow(clippy::too_many_arguments)]
pub async fn serum3_place_order(
pub fn serum3_place_order_instruction(
&self,
name: &str,
account: &MangoAccountValue,
market_index: Serum3MarketIndex,
side: Serum3Side,
price: f64,
size: f64,
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,
) -> anyhow::Result<Signature> {
let s3 = self.serum3_data_by_market_name(name)?;
) -> anyhow::Result<Instruction> {
let s3 = self.context.serum3(market_index);
let base = self.context.serum3_base_token(market_index);
let quote = self.context.serum3_quote_token(market_index);
let open_orders = account
.serum3_orders(market_index)
.expect("oo is created")
.open_orders;
let account = self.mango_account().await?;
let open_orders = account.serum3_orders(s3.market_index).unwrap().open_orders;
let health_check_metas = self.context.derive_health_check_remaining_account_metas(
account,
vec![],
vec![],
vec![],
)?;
let health_check_metas = self
.derive_health_check_remaining_account_metas(vec![], vec![], vec![])
.await?;
// https://github.com/project-serum/serum-ts/blob/master/packages/serum/src/market.ts#L1306
let limit_price = {
(price * ((10u64.pow(s3.quote.decimals as u32) * s3.market.coin_lot_size) as f64))
as u64
/ (10u64.pow(s3.base.decimals as u32) * s3.market.pc_lot_size)
};
// https://github.com/project-serum/serum-ts/blob/master/packages/serum/src/market.ts#L1333
let max_base_qty =
{ (size * 10u64.pow(s3.base.decimals as u32) as f64) as u64 / s3.market.coin_lot_size };
let max_native_quote_qty_including_fees = {
fn get_fee_tier(msrm_balance: u64, srm_balance: u64) -> u64 {
if msrm_balance >= 1 {
6
} else if srm_balance >= 1_000_000 {
5
} else if srm_balance >= 100_000 {
4
} else if srm_balance >= 10_000 {
3
} else if srm_balance >= 1_000 {
2
} else if srm_balance >= 100 {
1
} else {
0
}
}
fn get_fee_rates(fee_tier: u64) -> (f64, f64) {
if fee_tier == 1 {
// SRM2
return (0.002, -0.0003);
} else if fee_tier == 2 {
// SRM3
return (0.0018, -0.0003);
} else if fee_tier == 3 {
// SRM4
return (0.0016, -0.0003);
} else if fee_tier == 4 {
// SRM5
return (0.0014, -0.0003);
} else if fee_tier == 5 {
// SRM6
return (0.0012, -0.0003);
} else if fee_tier == 6 {
// MSRM
return (0.001, -0.0005);
}
// Base
(0.0022, -0.0003)
}
let fee_tier = get_fee_tier(0, 0);
let rates = get_fee_rates(fee_tier);
(s3.market.pc_lot_size as f64 * (1f64 + rates.0)) as u64 * (limit_price * max_base_qty)
};
let payer_mint_info = match side {
Serum3Side::Bid => s3.quote.mint_info,
Serum3Side::Ask => s3.base.mint_info,
Serum3Side::Bid => quote.mint_info,
Serum3Side::Ask => base.mint_info,
};
let ix = Instruction {
@ -615,16 +545,16 @@ impl MangoClient {
payer_bank: payer_mint_info.first_bank(),
payer_vault: payer_mint_info.first_vault(),
payer_oracle: payer_mint_info.oracle,
serum_market: s3.market.address,
serum_program: s3.market.market.serum_program,
serum_market_external: s3.market.market.serum_market_external,
market_bids: s3.market.bids,
market_asks: s3.market.asks,
market_event_queue: s3.market.event_q,
market_request_queue: s3.market.req_q,
market_base_vault: s3.market.coin_vault,
market_quote_vault: s3.market.pc_vault,
market_vault_signer: s3.market.vault_signer,
serum_market: s3.address,
serum_program: s3.market.serum_program,
serum_market_external: s3.market.serum_market_external,
market_bids: s3.bids,
market_asks: s3.asks,
market_event_queue: s3.event_q,
market_request_queue: s3.req_q,
market_base_vault: s3.coin_vault,
market_quote_vault: s3.pc_vault,
market_vault_signer: s3.vault_signer,
owner: self.owner(),
token_program: Token::id(),
},
@ -644,51 +574,123 @@ impl MangoClient {
limit,
}),
};
Ok(ix)
}
#[allow(clippy::too_many_arguments)]
pub async fn serum3_place_order(
&self,
name: &str,
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,
) -> anyhow::Result<Signature> {
let account = self.mango_account().await?;
let market_index = self.context.serum3_market_index(name);
let ix = self.serum3_place_order_instruction(
&account,
market_index,
side,
limit_price,
max_base_qty,
max_native_quote_qty_including_fees,
self_trade_behavior,
order_type,
client_order_id,
limit,
)?;
self.send_and_confirm_owner_tx(vec![ix]).await
}
pub async fn serum3_settle_funds(&self, name: &str) -> anyhow::Result<Signature> {
let s3 = self.serum3_data_by_market_name(name)?;
let market_index = self.context.serum3_market_index(name);
let s3 = self.context.serum3(market_index);
let base = self.context.serum3_base_token(market_index);
let quote = self.context.serum3_quote_token(market_index);
let account = self.mango_account().await?;
let open_orders = account.serum3_orders(s3.market_index).unwrap().open_orders;
let open_orders = account.serum3_orders(market_index).unwrap().open_orders;
let ix = Instruction {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::Serum3SettleFunds {
group: self.group(),
account: self.mango_account_address,
open_orders,
quote_bank: s3.quote.mint_info.first_bank(),
quote_vault: s3.quote.mint_info.first_vault(),
base_bank: s3.base.mint_info.first_bank(),
base_vault: s3.base.mint_info.first_vault(),
serum_market: s3.market.address,
serum_program: s3.market.market.serum_program,
serum_market_external: s3.market.market.serum_market_external,
market_base_vault: s3.market.coin_vault,
market_quote_vault: s3.market.pc_vault,
market_vault_signer: s3.market.vault_signer,
owner: self.owner(),
token_program: Token::id(),
&mango_v4::accounts::Serum3SettleFundsV2 {
v1: mango_v4::accounts::Serum3SettleFunds {
group: self.group(),
account: self.mango_account_address,
open_orders,
quote_bank: quote.mint_info.first_bank(),
quote_vault: quote.mint_info.first_vault(),
base_bank: base.mint_info.first_bank(),
base_vault: base.mint_info.first_vault(),
serum_market: s3.address,
serum_program: s3.market.serum_program,
serum_market_external: s3.market.serum_market_external,
market_base_vault: s3.coin_vault,
market_quote_vault: s3.pc_vault,
market_vault_signer: s3.vault_signer,
owner: self.owner(),
token_program: Token::id(),
},
v2: mango_v4::accounts::Serum3SettleFundsV2Extra {
quote_oracle: quote.mint_info.oracle,
base_oracle: base.mint_info.oracle,
},
},
None,
),
data: anchor_lang::InstructionData::data(&mango_v4::instruction::Serum3SettleFunds {}),
data: anchor_lang::InstructionData::data(&mango_v4::instruction::Serum3SettleFundsV2 {
fees_to_dao: true,
}),
};
self.send_and_confirm_owner_tx(vec![ix]).await
}
pub fn serum3_cancel_all_orders_instruction(
&self,
account: &MangoAccountValue,
market_index: Serum3MarketIndex,
limit: u8,
) -> anyhow::Result<Instruction> {
let s3 = self.context.serum3(market_index);
let open_orders = account.serum3_orders(market_index)?.open_orders;
let ix = Instruction {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::Serum3CancelAllOrders {
group: self.group(),
account: self.mango_account_address,
open_orders,
market_bids: s3.bids,
market_asks: s3.asks,
market_event_queue: s3.event_q,
serum_market: s3.address,
serum_program: s3.market.serum_program,
serum_market_external: s3.market.serum_market_external,
owner: self.owner(),
},
None,
),
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::Serum3CancelAllOrders { limit },
),
};
Ok(ix)
}
pub async fn serum3_cancel_all_orders(
&self,
market_name: &str,
) -> Result<Vec<u128>, anyhow::Error> {
let market_index = *self
.context
.serum3_market_indexes_by_name
.get(market_name)
.unwrap();
let market_index = self.context.serum3_market_index(market_name);
let account = self.mango_account().await?;
let open_orders = account.serum3_orders(market_index).unwrap().open_orders;
let open_orders_acc = self.account_fetcher.fetch_raw_account(&open_orders).await?;
@ -721,7 +723,9 @@ impl MangoClient {
market_index: Serum3MarketIndex,
open_orders: &Pubkey,
) -> anyhow::Result<Signature> {
let s3 = self.serum3_data_by_market_index(market_index)?;
let s3 = self.context.serum3(market_index);
let base = self.context.serum3_base_token(market_index);
let quote = self.context.serum3_quote_token(market_index);
let health_remaining_ams = self
.context
@ -736,19 +740,19 @@ impl MangoClient {
group: self.group(),
account: *liqee.0,
open_orders: *open_orders,
serum_market: s3.market.address,
serum_program: s3.market.market.serum_program,
serum_market_external: s3.market.market.serum_market_external,
market_bids: s3.market.bids,
market_asks: s3.market.asks,
market_event_queue: s3.market.event_q,
market_base_vault: s3.market.coin_vault,
market_quote_vault: s3.market.pc_vault,
market_vault_signer: s3.market.vault_signer,
quote_bank: s3.quote.mint_info.first_bank(),
quote_vault: s3.quote.mint_info.first_vault(),
base_bank: s3.base.mint_info.first_bank(),
base_vault: s3.base.mint_info.first_vault(),
serum_market: s3.address,
serum_program: s3.market.serum_program,
serum_market_external: s3.market.serum_market_external,
market_bids: s3.bids,
market_asks: s3.asks,
market_event_queue: s3.event_q,
market_base_vault: s3.coin_vault,
market_quote_vault: s3.pc_vault,
market_vault_signer: s3.vault_signer,
quote_bank: quote.mint_info.first_bank(),
quote_vault: quote.mint_info.first_vault(),
base_bank: base.mint_info.first_bank(),
base_vault: base.mint_info.first_vault(),
token_program: Token::id(),
},
None,
@ -769,10 +773,11 @@ impl MangoClient {
side: Serum3Side,
order_id: u128,
) -> anyhow::Result<Signature> {
let s3 = self.serum3_data_by_market_name(market_name)?;
let market_index = self.context.serum3_market_index(market_name);
let s3 = self.context.serum3(market_index);
let account = self.mango_account().await?;
let open_orders = account.serum3_orders(s3.market_index).unwrap().open_orders;
let open_orders = account.serum3_orders(market_index).unwrap().open_orders;
let ix = Instruction {
program_id: mango_v4::id(),
@ -781,13 +786,13 @@ impl MangoClient {
&mango_v4::accounts::Serum3CancelOrder {
group: self.group(),
account: self.mango_account_address,
serum_market: s3.market.address,
serum_program: s3.market.market.serum_program,
serum_market_external: s3.market.market.serum_market_external,
serum_market: s3.address,
serum_program: s3.market.serum_program,
serum_market_external: s3.market.serum_market_external,
open_orders,
market_bids: s3.market.bids,
market_asks: s3.market.asks,
market_event_queue: s3.market.event_q,
market_bids: s3.bids,
market_asks: s3.asks,
market_event_queue: s3.event_q,
owner: self.owner(),
},
None,
@ -804,6 +809,8 @@ impl MangoClient {
//
// Perps
//
#[allow(clippy::too_many_arguments)]
pub fn perp_place_order_instruction(
&self,
account: &MangoAccountValue,
@ -863,6 +870,7 @@ impl MangoClient {
Ok(ix)
}
#[allow(clippy::too_many_arguments)]
pub async fn perp_place_order(
&self,
market_index: PerpMarketIndex,
@ -1250,6 +1258,53 @@ impl MangoClient {
self.send_and_confirm_owner_tx(vec![ix]).await
}
pub async fn token_conditional_swap_trigger(
&self,
liqee: (&Pubkey, &MangoAccountValue),
token_conditional_swap_id: u64,
max_buy_token_to_liqee: u64,
max_sell_token_to_liqor: u64,
) -> anyhow::Result<Signature> {
let (tcs_index, tcs) = liqee
.1
.token_conditional_swap_by_id(token_conditional_swap_id)?;
let health_remaining_ams = self
.derive_liquidation_health_check_remaining_account_metas(
liqee.1,
vec![tcs.buy_token_index, tcs.sell_token_index],
&[tcs.buy_token_index, tcs.sell_token_index],
)
.await
.unwrap();
let ix = Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::TokenConditionalSwapTrigger {
group: self.group(),
liqee: *liqee.0,
liqor: self.mango_account_address,
liqor_authority: self.owner(),
},
None,
);
ams.extend(health_remaining_ams);
ams
},
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::TokenConditionalSwapTrigger {
token_conditional_swap_id,
token_conditional_swap_index: tcs_index.try_into().unwrap(),
max_buy_token_to_liqee,
max_sell_token_to_liqor,
},
),
};
self.send_and_confirm_owner_tx(vec![ix]).await
}
// health region
pub fn health_region_begin_instruction(
@ -1320,6 +1375,21 @@ impl MangoClient {
// jupiter
async fn http_error_handling<T: serde::de::DeserializeOwned>(
response: reqwest::Response,
) -> anyhow::Result<T> {
let status = response.status();
let response_text = response
.text()
.await
.context("awaiting body of quote request to jupiter")?;
if !status.is_success() {
anyhow::bail!("request failed, status: {status}, body: {response_text}");
}
serde_json::from_str::<T>(&response_text)
.with_context(|| format!("response has unexpected format, body: {response_text}"))
}
pub async fn jupiter_route(
&self,
input_mint: Pubkey,
@ -1329,7 +1399,7 @@ impl MangoClient {
swap_mode: JupiterSwapMode,
only_direct_routes: bool,
) -> anyhow::Result<jupiter::QueryRoute> {
let quote = self
let response = self
.http_client
.get("https://quote-api.jup.ag/v4/quote")
.query(&[
@ -1351,28 +1421,19 @@ impl MangoClient {
])
.send()
.await
.context("quote request to jupiter")?
.json::<jupiter::QueryResult>()
.await
.context("receiving json response from jupiter quote request")?;
// Find the top route that doesn't involve Raydium (that has too many accounts)
let route = quote
.data
.iter()
.find(|route| {
!route
.market_infos
.iter()
.any(|mi| mi.label.contains("Raydium"))
})
.ok_or_else(|| {
anyhow::anyhow!(
"no route for swap. found {} routes, but none were usable",
quote.data.len()
)
.context("quote request to jupiter")?;
let quote: jupiter::QueryResult =
Self::http_error_handling(response).await.with_context(|| {
format!("error requesting jupiter route between {input_mint} and {output_mint}")
})?;
let route = quote.data.first().ok_or_else(|| {
anyhow::anyhow!(
"no route for swap. found {} routes, but none were usable",
quote.data.len()
)
})?;
Ok(route.clone())
}
@ -1389,7 +1450,7 @@ impl MangoClient {
let source_token = self.context.token_by_mint(&input_mint)?;
let target_token = self.context.token_by_mint(&output_mint)?;
let swap = self
let swap_response = self
.http_client
.post("https://quote-api.jup.ag/v4/swap")
.json(&jupiter::SwapRequest {
@ -1400,10 +1461,11 @@ impl MangoClient {
})
.send()
.await
.context("swap transaction request to jupiter")?
.json::<jupiter::SwapResponse>()
.context("swap transaction request to jupiter")?;
let swap: jupiter::SwapResponse = Self::http_error_handling(swap_response)
.await
.context("receiving json response from jupiter swap transaction request")?;
.context("error requesting jupiter swap")?;
if swap.setup_transaction.is_some() || swap.cleanup_transaction.is_some() {
anyhow::bail!(
@ -1691,13 +1753,6 @@ impl MangoClient {
}
}
struct Serum3Data<'a> {
market_index: Serum3MarketIndex,
market: &'a Serum3MarketContext,
quote: &'a TokenContext,
base: &'a TokenContext,
}
#[derive(Debug, thiserror::Error)]
pub enum MangoClientError {
#[error("Transaction simulation error. Error: {err:?}, Logs: {}",

View File

@ -2,10 +2,10 @@ use std::collections::HashMap;
use anchor_client::ClientError;
use anchor_lang::__private::bytemuck;
use anchor_lang::__private::bytemuck::{self, Zeroable};
use mango_v4::state::{
Group, MangoAccountValue, MintInfo, PerpMarket, PerpMarketIndex, Serum3Market,
Bank, Group, MangoAccountValue, MintInfo, PerpMarket, PerpMarketIndex, Serum3Market,
Serum3MarketIndex, TokenIndex,
};
@ -27,6 +27,8 @@ pub struct TokenContext {
pub mint_info: MintInfo,
pub mint_info_address: Pubkey,
pub decimals: u8,
/// Bank snapshot is never updated, only use static parts!
pub bank: Bank,
}
impl TokenContext {
@ -51,6 +53,7 @@ pub struct Serum3MarketContext {
pub struct PerpMarketContext {
pub address: Pubkey,
/// PerpMarket snapshot is never updated, only use static parts!
pub market: PerpMarket,
}
@ -78,14 +81,34 @@ impl MangoGroupContext {
self.token(token_index).mint_info
}
pub fn token(&self, token_index: TokenIndex) -> &TokenContext {
self.tokens.get(&token_index).unwrap()
}
pub fn perp(&self, perp_market_index: PerpMarketIndex) -> &PerpMarketContext {
self.perp_markets.get(&perp_market_index).unwrap()
}
pub fn perp_market_address(&self, perp_market_index: PerpMarketIndex) -> Pubkey {
self.perp(perp_market_index).address
}
pub fn serum3_market_index(&self, name: &str) -> Serum3MarketIndex {
*self.serum3_market_indexes_by_name.get(name).unwrap()
}
pub fn serum3(&self, market_index: Serum3MarketIndex) -> &Serum3MarketContext {
self.serum3_markets.get(&market_index).unwrap()
}
pub fn serum3_base_token(&self, market_index: Serum3MarketIndex) -> &TokenContext {
self.token(self.serum3(market_index).market.base_token_index)
}
pub fn serum3_quote_token(&self, market_index: Serum3MarketIndex) -> &TokenContext {
self.token(self.serum3(market_index).market.quote_token_index)
}
pub fn token(&self, token_index: TokenIndex) -> &TokenContext {
self.tokens.get(&token_index).unwrap()
}
pub fn token_by_mint(&self, mint: &Pubkey) -> anyhow::Result<&TokenContext> {
self.tokens
.iter()
@ -93,10 +116,6 @@ impl MangoGroupContext {
.ok_or_else(|| anyhow::anyhow!("no token for mint {}", mint))
}
pub fn perp_market_address(&self, perp_market_index: PerpMarketIndex) -> Pubkey {
self.perp(perp_market_index).address
}
pub async fn new_from_rpc(rpc: &RpcClientAsync, group: Pubkey) -> anyhow::Result<Self> {
let program = mango_v4::ID;
@ -113,6 +132,7 @@ impl MangoGroupContext {
mint_info: *mi,
mint_info_address: *pk,
decimals: u8::MAX,
bank: Bank::zeroed(),
},
)
})
@ -126,6 +146,7 @@ impl MangoGroupContext {
let token = tokens.get_mut(&bank.token_index).unwrap();
token.name = bank.name().into();
token.decimals = bank.mint_decimals;
token.bank = bank.clone();
}
assert!(tokens.values().all(|t| t.decimals != u8::MAX));

View File

@ -12,10 +12,10 @@ use solana_sdk::{account::AccountSharedData, commitment_config::CommitmentConfig
use anyhow::Context;
use futures::{stream, StreamExt};
use log::*;
use std::str::FromStr;
use std::time::Duration;
use tokio::time;
use tracing::*;
use crate::account_update_stream::{AccountUpdate, Message};
use crate::AnyhowWrap;
@ -230,10 +230,10 @@ pub fn start(config: Config, mango_oracles: Vec<Pubkey>, sender: async_channel::
}))
.await
.expect("always Ok");
log::debug!("latest slot for snapshot {}", epoch_info.absolute_slot);
debug!("latest slot for snapshot {}", epoch_info.absolute_slot);
if epoch_info.absolute_slot > config.min_slot {
log::debug!("continuing to fetch snapshot now, min_slot {} is older than latest epoch slot {}", config.min_slot, epoch_info.absolute_slot);
debug!("continuing to fetch snapshot now, min_slot {} is older than latest epoch slot {}", config.min_slot, epoch_info.absolute_slot);
break;
}
}

View File

@ -9,21 +9,6 @@ use solana_sdk::{
use std::{thread, time};
// #[allow(dead_code)]
// pub fn retry<T>(request: impl Fn() -> Result<T, anchor_client::ClientError>) -> anyhow::Result<T> {
// for _i in 0..5 {
// match request() {
// Ok(res) => return Ok(res),
// Err(err) => {
// // TODO: only retry for recoverable errors
// log::error!("{:#?}", err);
// continue;
// }
// }
// }
// Err(anyhow!("Retry failed"))
// }
/// Some Result<> types don't convert to anyhow::Result nicely. Force them through stringification.
pub trait AnyhowWrap {
type Value;
@ -114,3 +99,14 @@ pub fn send_and_confirm_transaction(
)
.into())
}
/// Convenience function used in binaries to set up the fmt tracing_subscriber,
/// with cololring enabled only if logging to a terminal and with EnvFilter.
pub fn tracing_subscriber_init() {
let format = tracing_subscriber::fmt::format().with_ansi(atty::is(atty::Stream::Stdout));
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.event_format(format)
.init();
}

View File

@ -11,9 +11,9 @@ use solana_rpc::rpc_pubsub::RpcSolPubSubClient;
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey};
use anyhow::Context;
use log::*;
use std::time::Duration;
use tokio_stream::StreamMap;
use tracing::*;
use crate::account_update_stream::{AccountUpdate, Message};
use crate::AnyhowWrap;

File diff suppressed because it is too large Load Diff

View File

@ -73,7 +73,8 @@
},
"resolutions": {
"@coral-xyz/anchor": "^0.27.0",
"**/@solana/web3.js/node-fetch": "https://github.com/blockworks-foundation/node-fetch.git#v2.6.11-fixed"
"**/@solana/web3.js/node-fetch": "npm:@blockworks-foundation/node-fetch@2.6.11",
"**/cross-fetch/node-fetch": "npm:@blockworks-foundation/node-fetch@2.6.11"
},
"license": "MIT"
}

View File

@ -2,7 +2,7 @@ cargo-features = ["workspace-inheritance"]
[package]
name = "mango-v4"
version = "0.17.1"
version = "0.18.0"
description = "Created with Anchor"
edition = "2021"

View File

@ -15,7 +15,7 @@ pub struct AccountCreate<'info> {
seeds = [b"MangoAccount".as_ref(), group.key().as_ref(), owner.key().as_ref(), &account_num.to_le_bytes()],
bump,
payer = payer,
space = MangoAccount::space(token_count, serum3_count, perp_count, perp_oo_count)?,
space = MangoAccount::space(token_count, serum3_count, perp_count, perp_oo_count, 0)?,
)]
pub account: AccountLoader<'info, MangoAccountFixed>,
pub owner: Signer<'info>,

View File

@ -0,0 +1,52 @@
use anchor_lang::prelude::*;
use anchor_spl::token;
use anchor_spl::token::Token;
use anchor_spl::token::TokenAccount;
use crate::error::*;
use crate::state::*;
#[derive(Accounts)]
pub struct AdminPerpWithdrawFees<'info> {
#[account(
constraint = group.load()?.is_ix_enabled(IxGate::AdminPerpWithdrawFees) @ MangoError::IxIsDisabled,
has_one = admin,
)]
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = group,
)]
pub perp_market: AccountLoader<'info, PerpMarket>,
#[account(
mut,
has_one = group,
has_one = vault,
constraint = bank.load()?.token_index == perp_market.load()?.settle_token_index
)]
pub bank: AccountLoader<'info, Bank>,
#[account(mut)]
pub vault: Account<'info, TokenAccount>,
#[account(mut)]
pub token_account: Box<Account<'info, TokenAccount>>,
pub token_program: Program<'info, Token>,
pub admin: Signer<'info>,
}
impl<'info> AdminPerpWithdrawFees<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = token::Transfer {
from: self.vault.to_account_info(),
to: self.token_account.to_account_info(),
authority: self.group.to_account_info(),
};
CpiContext::new(program, accounts)
}
}

View File

@ -0,0 +1,45 @@
use anchor_lang::prelude::*;
use anchor_spl::token;
use anchor_spl::token::Token;
use anchor_spl::token::TokenAccount;
use crate::error::*;
use crate::state::*;
#[derive(Accounts)]
pub struct AdminTokenWithdrawFees<'info> {
#[account(
constraint = group.load()?.is_ix_enabled(IxGate::AdminTokenWithdrawFees) @ MangoError::IxIsDisabled,
has_one = admin,
)]
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = group,
has_one = vault,
)]
pub bank: AccountLoader<'info, Bank>,
#[account(mut)]
pub vault: Account<'info, TokenAccount>,
#[account(mut)]
pub token_account: Box<Account<'info, TokenAccount>>,
pub token_program: Program<'info, Token>,
pub admin: Signer<'info>,
}
impl<'info> AdminTokenWithdrawFees<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = token::Transfer {
from: self.vault.to_account_info(),
to: self.token_account.to_account_info(),
authority: self.group.to_account_info(),
};
CpiContext::new(program, accounts)
}
}

View File

@ -4,6 +4,8 @@ pub use account_create::*;
pub use account_edit::*;
pub use account_expand::*;
pub use account_toggle_freeze::*;
pub use admin_perp_withdraw_fees::*;
pub use admin_token_withdraw_fees::*;
pub use alt_extend::*;
pub use alt_set::*;
pub use benchmark::*;
@ -46,6 +48,9 @@ pub use stub_oracle_close::*;
pub use stub_oracle_create::*;
pub use stub_oracle_set::*;
pub use token_add_bank::*;
pub use token_conditional_swap_cancel::*;
pub use token_conditional_swap_create::*;
pub use token_conditional_swap_trigger::*;
pub use token_deposit::*;
pub use token_deregister::*;
pub use token_edit::*;
@ -63,6 +68,8 @@ mod account_create;
mod account_edit;
mod account_expand;
mod account_toggle_freeze;
mod admin_perp_withdraw_fees;
mod admin_token_withdraw_fees;
mod alt_extend;
mod alt_set;
mod benchmark;
@ -105,6 +112,9 @@ mod stub_oracle_close;
mod stub_oracle_create;
mod stub_oracle_set;
mod token_add_bank;
mod token_conditional_swap_cancel;
mod token_conditional_swap_create;
mod token_conditional_swap_trigger;
mod token_deposit;
mod token_deregister;
mod token_edit;

View File

@ -0,0 +1,32 @@
use crate::error::*;
use crate::state::*;
use anchor_lang::prelude::*;
#[derive(Accounts)]
pub struct TokenConditionalSwapCancel<'info> {
#[account(
constraint = group.load()?.is_ix_enabled(IxGate::TokenConditionalSwapCancel) @ MangoError::IxIsDisabled,
)]
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = group,
constraint = account.load()?.is_operational() @ MangoError::AccountIsFrozen,
constraint = account.load()?.is_owner_or_delegate(authority.key()),
)]
pub account: AccountLoader<'info, MangoAccountFixed>,
pub authority: Signer<'info>,
/// The bank's token_index is checked at #1
#[account(
mut,
has_one = group,
)]
pub buy_bank: AccountLoader<'info, Bank>,
#[account(
mut,
has_one = group,
)]
pub sell_bank: AccountLoader<'info, Bank>,
}

View File

@ -0,0 +1,29 @@
use crate::error::*;
use crate::state::*;
use anchor_lang::prelude::*;
#[derive(Accounts)]
pub struct TokenConditionalSwapCreate<'info> {
#[account(
constraint = group.load()?.is_ix_enabled(IxGate::TokenConditionalSwapCreate) @ MangoError::IxIsDisabled,
)]
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = group,
constraint = account.load()?.is_operational() @ MangoError::AccountIsFrozen,
constraint = account.load()?.is_owner_or_delegate(authority.key()),
)]
pub account: AccountLoader<'info, MangoAccountFixed>,
pub authority: Signer<'info>,
#[account(
has_one = group,
)]
pub buy_bank: AccountLoader<'info, Bank>,
#[account(
has_one = group,
)]
pub sell_bank: AccountLoader<'info, Bank>,
}

View File

@ -0,0 +1,27 @@
use crate::error::*;
use crate::state::*;
use anchor_lang::prelude::*;
#[derive(Accounts)]
pub struct TokenConditionalSwapTrigger<'info> {
#[account(
constraint = group.load()?.is_ix_enabled(IxGate::TokenConditionalSwapTrigger) @ MangoError::IxIsDisabled,
)]
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = group,
constraint = liqee.load()?.is_operational() @ MangoError::AccountIsFrozen
)]
pub liqee: AccountLoader<'info, MangoAccountFixed>,
#[account(
mut,
has_one = group,
constraint = liqor.load()?.is_operational() @ MangoError::AccountIsFrozen,
constraint = liqor.load()?.is_owner_or_delegate(liqor_authority.key()),
)]
pub liqor: AccountLoader<'info, MangoAccountFixed>,
pub liqor_authority: Signer<'info>,
}

View File

@ -103,6 +103,8 @@ pub enum MangoError {
InvalidHealthAccountCount,
#[msg("would self trade")]
WouldSelfTrade,
#[msg("conditional token swap price is not in execution range")]
TokenConditionalSwapPriceNotInRange,
}
impl MangoError {

View File

@ -553,6 +553,24 @@ impl HealthCache {
health
}
/// The health ratio is
/// - 0 if health is 0 - meaning assets = liabs
/// - 100 if there's 2x as many assets as liabs
/// - 200 if there's 3x as many assets as liabs
/// - MAX if liabs = 0
///
/// Maybe talking about the collateralization ratio assets/liabs is more intuitive?
pub fn health_ratio(&self, health_type: HealthType) -> I80F48 {
let (assets, liabs) = self.health_assets_and_liabs_stable_liabs(health_type);
let hundred = I80F48::from(100);
if liabs > 0 {
// feel free to saturate to MAX for tiny liabs
(hundred * (assets - liabs)).saturating_div(liabs)
} else {
I80F48::MAX
}
}
pub fn health_assets_and_liabs_stable_assets(
&self,
health_type: HealthType,

View File

@ -19,24 +19,6 @@ impl HealthCache {
}
}
/// The health ratio is
/// - 0 if health is 0 - meaning assets = liabs
/// - 100 if there's 2x as many assets as liabs
/// - 200 if there's 3x as many assets as liabs
/// - MAX if liabs = 0
///
/// Maybe talking about the collateralization ratio assets/liabs is more intuitive?
pub fn health_ratio(&self, health_type: HealthType) -> I80F48 {
let (assets, liabs) = self.health_assets_and_liabs_stable_liabs(health_type);
let hundred = I80F48::from(100);
if liabs > 0 {
// feel free to saturate to MAX for tiny liabs
(hundred * (assets - liabs)).saturating_div(liabs)
} else {
I80F48::MAX
}
}
/// Return a copy of the current cache where a swap between two banks was executed.
///
/// Errors:

View File

@ -29,7 +29,7 @@ pub fn account_create(
account.fixed.delegate = Pubkey::default();
account.fixed.set_being_liquidated(false);
account.expand_dynamic_content(token_count, serum3_count, perp_count, perp_oo_count)?;
account.expand_dynamic_content(token_count, serum3_count, perp_count, perp_oo_count, 0)?;
Ok(())
}

View File

@ -9,8 +9,15 @@ pub fn account_expand(
serum3_count: u8,
perp_count: u8,
perp_oo_count: u8,
token_conditional_swap_count: u8,
) -> Result<()> {
let new_space = MangoAccount::space(token_count, serum3_count, perp_count, perp_oo_count)?;
let new_space = MangoAccount::space(
token_count,
serum3_count,
perp_count,
perp_oo_count,
token_conditional_swap_count,
)?;
let new_rent_minimum = Rent::get()?.minimum_balance(new_space);
let realloc_account = ctx.accounts.account.as_ref();
@ -36,7 +43,13 @@ pub fn account_expand(
// expand dynamic content, e.g. to grow token positions, we need to slide serum3orders further later, and so on....
let mut account = ctx.accounts.account.load_full_mut()?;
account.expand_dynamic_content(token_count, serum3_count, perp_count, perp_oo_count)?;
account.expand_dynamic_content(
token_count,
serum3_count,
perp_count,
perp_oo_count,
token_conditional_swap_count,
)?;
Ok(())
}

View File

@ -0,0 +1,21 @@
use anchor_lang::prelude::*;
use anchor_spl::token;
use crate::{accounts_ix::*, group_seeds};
pub fn admin_perp_withdraw_fees(ctx: Context<AdminPerpWithdrawFees>) -> Result<()> {
let group = ctx.accounts.group.load()?;
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let group_seeds = group_seeds!(group);
let fees = perp_market.fees_settled.floor().to_num::<u64>() - perp_market.fees_withdrawn;
let amount = fees.min(ctx.accounts.vault.amount);
token::transfer(
ctx.accounts.transfer_ctx().with_signer(&[group_seeds]),
amount,
)?;
perp_market.fees_withdrawn += amount;
Ok(())
}

View File

@ -0,0 +1,21 @@
use anchor_lang::prelude::*;
use anchor_spl::token;
use crate::{accounts_ix::*, group_seeds};
pub fn admin_token_withdraw_fees(ctx: Context<AdminTokenWithdrawFees>) -> Result<()> {
let group = ctx.accounts.group.load()?;
let mut bank = ctx.accounts.bank.load_mut()?;
let group_seeds = group_seeds!(group);
let fees = bank.collected_fees_native.floor().to_num::<u64>() - bank.fees_withdrawn;
let amount = fees.min(ctx.accounts.vault.amount);
token::transfer(
ctx.accounts.transfer_ctx().with_signer(&[group_seeds]),
amount,
)?;
bank.fees_withdrawn += amount;
Ok(())
}

View File

@ -18,6 +18,8 @@ pub fn group_edit(
buyback_fees_swap_mango_account_opt: Option<Pubkey>,
mngo_token_index_opt: Option<TokenIndex>,
buyback_fees_expiry_interval_opt: Option<u64>,
token_conditional_swap_taker_fee_fraction_opt: Option<f32>,
token_conditional_swap_maker_fee_fraction_opt: Option<f32>,
) -> Result<()> {
let mut group = ctx.accounts.group.load_mut()?;
@ -106,5 +108,24 @@ pub fn group_edit(
group.buyback_fees_expiry_interval = buyback_fees_expiry_interval;
}
if let Some(fee_fraction) = token_conditional_swap_taker_fee_fraction_opt {
msg!(
"Token conditional swap taker fee fraction old {:?}, new {:?}",
group.token_conditional_swap_taker_fee_fraction,
fee_fraction
);
require_gte!(fee_fraction, 0.0); // values <0 are not currently supported
group.token_conditional_swap_taker_fee_fraction = fee_fraction;
}
if let Some(fees_fraction) = token_conditional_swap_maker_fee_fraction_opt {
msg!(
"Token conditional swap maker fee fraction old {:?}, new {:?}",
group.token_conditional_swap_maker_fee_fraction,
fees_fraction
);
require_gte!(fees_fraction, 0.0); // values <0 are not currently supported
group.token_conditional_swap_maker_fee_fraction = fees_fraction;
}
Ok(())
}

View File

@ -67,6 +67,21 @@ pub fn ix_gate_set(ctx: Context<IxGateSet>, ix_gate: u128) -> Result<()> {
log_if_changed(&group, ix_gate, IxGate::TokenForceCloseBorrowsWithToken);
log_if_changed(&group, ix_gate, IxGate::PerpForceClosePosition);
log_if_changed(&group, ix_gate, IxGate::GroupWithdrawInsuranceFund);
log_if_changed(&group, ix_gate, IxGate::TokenConditionalSwapCreate);
log_if_changed(&group, ix_gate, IxGate::TokenConditionalSwapTrigger);
log_if_changed(&group, ix_gate, IxGate::TokenConditionalSwapCancel);
log_if_changed(&group, ix_gate, IxGate::OpenbookV2CancelOrder);
log_if_changed(&group, ix_gate, IxGate::OpenbookV2CloseOpenOrders);
log_if_changed(&group, ix_gate, IxGate::OpenbookV2CreateOpenOrders);
log_if_changed(&group, ix_gate, IxGate::OpenbookV2DeregisterMarket);
log_if_changed(&group, ix_gate, IxGate::OpenbookV2EditMarket);
log_if_changed(&group, ix_gate, IxGate::OpenbookV2LiqForceCancelOrders);
log_if_changed(&group, ix_gate, IxGate::OpenbookV2PlaceOrder);
log_if_changed(&group, ix_gate, IxGate::OpenbookV2PlaceTakeOrder);
log_if_changed(&group, ix_gate, IxGate::OpenbookV2RegisterMarket);
log_if_changed(&group, ix_gate, IxGate::OpenbookV2SettleFunds);
log_if_changed(&group, ix_gate, IxGate::AdminTokenWithdrawFees);
log_if_changed(&group, ix_gate, IxGate::AdminPerpWithdrawFees);
group.ix_gate = ix_gate;

View File

@ -4,6 +4,8 @@ pub use account_create::*;
pub use account_edit::*;
pub use account_expand::*;
pub use account_toggle_freeze::*;
pub use admin_perp_withdraw_fees::*;
pub use admin_token_withdraw_fees::*;
pub use alt_extend::*;
pub use alt_set::*;
pub use benchmark::*;
@ -46,6 +48,9 @@ pub use stub_oracle_close::*;
pub use stub_oracle_create::*;
pub use stub_oracle_set::*;
pub use token_add_bank::*;
pub use token_conditional_swap_cancel::*;
pub use token_conditional_swap_create::*;
pub use token_conditional_swap_trigger::*;
pub use token_deposit::*;
pub use token_deregister::*;
pub use token_edit::*;
@ -63,6 +68,8 @@ mod account_create;
mod account_edit;
mod account_expand;
mod account_toggle_freeze;
mod admin_perp_withdraw_fees;
mod admin_token_withdraw_fees;
mod alt_extend;
mod alt_set;
mod benchmark;
@ -105,6 +112,9 @@ mod stub_oracle_close;
mod stub_oracle_create;
mod stub_oracle_set;
mod token_add_bank;
mod token_conditional_swap_cancel;
mod token_conditional_swap_create;
mod token_conditional_swap_trigger;
mod token_deposit;
mod token_deregister;
mod token_edit;

View File

@ -5,7 +5,7 @@ use crate::error::MangoError;
use crate::state::*;
use crate::accounts_ix::*;
use crate::logs::{emit_perp_balances, FillLogV2};
use crate::logs::{emit_perp_balances, FillLogV3};
/// Load a mango account by key from the list of account infos.
///
@ -66,7 +66,7 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
let fill: &FillEvent = cast_ref(event);
// handle self trade separately because of rust borrow checker
if fill.maker == fill.taker {
let (maker_closed_pnl, taker_closed_pnl) = if fill.maker == fill.taker {
load_mango_account!(
maker_taker,
fill.maker,
@ -74,6 +74,9 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
group,
event_queue
);
let before_pnl = maker_taker
.perp_position(perp_market_index)?
.realized_trade_pnl_native;
maker_taker.execute_perp_maker(
perp_market_index,
&mut perp_market,
@ -87,10 +90,22 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
maker_taker.perp_position(perp_market_index).unwrap(),
&perp_market,
);
let after_pnl = maker_taker
.perp_position(perp_market_index)?
.realized_trade_pnl_native;
let closed_pnl = after_pnl - before_pnl;
(closed_pnl, closed_pnl)
} else {
load_mango_account!(maker, fill.maker, mango_account_ais, group, event_queue);
load_mango_account!(taker, fill.taker, mango_account_ais, group, event_queue);
let maker_before_pnl = maker
.perp_position(perp_market_index)?
.realized_trade_pnl_native;
let taker_before_pnl = taker
.perp_position(perp_market_index)?
.realized_trade_pnl_native;
maker.execute_perp_maker(perp_market_index, &mut perp_market, fill, &group)?;
taker.execute_perp_taker(perp_market_index, &mut perp_market, fill)?;
emit_perp_balances(
@ -105,8 +120,18 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
taker.perp_position(perp_market_index).unwrap(),
&perp_market,
);
}
emit!(FillLogV2 {
let maker_after_pnl = maker
.perp_position(perp_market_index)?
.realized_trade_pnl_native;
let taker_after_pnl = taker
.perp_position(perp_market_index)?
.realized_trade_pnl_native;
let maker_closed_pnl = maker_after_pnl - maker_before_pnl;
let taker_closed_pnl = taker_after_pnl - taker_before_pnl;
(maker_closed_pnl, taker_closed_pnl)
};
emit!(FillLogV3 {
mango_group: group_key,
market_index: perp_market_index,
taker_side: fill.taker_side as u8,
@ -123,6 +148,8 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
taker_fee: fill.taker_fee,
price: fill.price,
quantity: fill.quantity,
maker_closed_pnl: maker_closed_pnl.to_num(),
taker_closed_pnl: taker_closed_pnl.to_num()
});
}
EventType::Out => {

View File

@ -91,14 +91,19 @@ pub fn perp_create_market(
maint_overall_asset_weight: I80F48::from_num(maint_overall_asset_weight),
init_overall_asset_weight: I80F48::from_num(init_overall_asset_weight),
positive_pnl_liquidation_fee: I80F48::from_num(positive_pnl_liquidation_fee),
reserved: [0; 1888],
fees_withdrawn: 0,
reserved: [0; 1880],
};
let oracle_price =
perp_market.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, None)?;
perp_market
.stable_price_model
.reset_to_price(oracle_price.to_num(), now_ts);
if let Ok(oracle_price) =
perp_market.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, None)
{
perp_market
.stable_price_model
.reset_to_price(oracle_price.to_num(), now_ts);
} else {
perp_market.stable_price_model.reset_on_nonzero_price = 1;
}
let mut orderbook = Orderbook {
bids: ctx.accounts.bids.load_init()?,

View File

@ -34,9 +34,9 @@ pub fn serum3_close_open_orders(ctx: Context<Serum3CloseOpenOrders>) -> Result<(
// Reduce the in_use_count on the token positions - they no longer need to be forced open.
// We cannot immediately dust tiny positions because we don't have the banks.
let (base_position, _) = account.token_position_mut(serum_market.base_token_index)?;
base_position.in_use_count = base_position.in_use_count.saturating_sub(1);
base_position.decrement_in_use();
let (quote_position, _) = account.token_position_mut(serum_market.quote_token_index)?;
quote_position.in_use_count = quote_position.in_use_count.saturating_sub(1);
quote_position.decrement_in_use();
// Deactivate the serum open orders account itself
account.deactivate_serum3_orders(serum_market.market_index)?;

View File

@ -25,9 +25,9 @@ pub fn serum3_create_open_orders(ctx: Context<Serum3CreateOpenOrders>) -> Result
// stay permanently blocked. Otherwise users may end up in situations where
// they can't settle a market because they don't have free token_account_map!
let (quote_position, _, _) = account.ensure_token_position(serum_market.quote_token_index)?;
quote_position.in_use_count += 1;
quote_position.increment_in_use();
let (base_position, _, _) = account.ensure_token_position(serum_market.base_token_index)?;
base_position.in_use_count += 1;
base_position.increment_in_use();
Ok(())
}

View File

@ -1,3 +1,4 @@
use crate::util::fill_from_str;
use crate::{accounts_ix::*, error::MangoError};
use anchor_lang::prelude::*;
@ -5,6 +6,7 @@ pub fn serum3_edit_market(
ctx: Context<Serum3EditMarket>,
reduce_only_opt: Option<bool>,
force_close_opt: Option<bool>,
name_opt: Option<String>,
) -> Result<()> {
let mut serum3_market = ctx.accounts.market.load_mut()?;
@ -38,6 +40,12 @@ pub fn serum3_edit_market(
require_group_admin = true;
};
if let Some(name) = name_opt.as_ref() {
msg!("Name: old - {:?}, new - {:?}", serum3_market.name, name);
serum3_market.name = fill_from_str(&name)?;
require_group_admin = true;
};
if require_group_admin {
require!(
group.admin == ctx.accounts.admin.key(),

View File

@ -0,0 +1,43 @@
use anchor_lang::prelude::*;
use crate::accounts_ix::*;
use crate::logs::TokenConditionalSwapCancelLog;
use crate::state::*;
#[allow(clippy::too_many_arguments)]
pub fn token_conditional_swap_cancel(
ctx: Context<TokenConditionalSwapCancel>,
token_conditional_swap_index: usize,
token_conditional_swap_id: u64,
) -> Result<()> {
let mut buy_bank = ctx.accounts.buy_bank.load_mut()?;
let mut sell_bank = ctx.accounts.sell_bank.load_mut()?;
let mut account = ctx.accounts.account.load_full_mut()?;
let tcs = account.token_conditional_swap_mut_by_index(token_conditional_swap_index)?;
require_eq!(tcs.buy_token_index, buy_bank.token_index);
require_eq!(tcs.sell_token_index, sell_bank.token_index);
// If the tcs is already inactive, this just is a noop
if !tcs.has_data() {
return Ok(());
}
require_eq!(tcs.id, token_conditional_swap_id);
*tcs = TokenConditionalSwap::default();
drop(tcs);
emit!(TokenConditionalSwapCancelLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
id: token_conditional_swap_id,
});
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
// Free up any locks on token positions, possibly dust and deactivate them.
account.token_decrement_dust_deactivate(&mut buy_bank, now_ts, ctx.accounts.account.key())?;
account.token_decrement_dust_deactivate(&mut sell_bank, now_ts, ctx.accounts.account.key())?;
Ok(())
}

View File

@ -0,0 +1,71 @@
use anchor_lang::prelude::*;
use crate::accounts_ix::*;
use crate::logs::TokenConditionalSwapCreateLog;
use crate::state::*;
#[allow(clippy::too_many_arguments)]
pub fn token_conditional_swap_create(
ctx: Context<TokenConditionalSwapCreate>,
token_conditional_swap: TokenConditionalSwap,
) -> Result<()> {
let group = ctx.accounts.group.load()?;
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
if token_conditional_swap.is_expired(now_ts) {
msg!("Already expired, ignoring");
return Ok(());
}
let mut account = ctx.accounts.account.load_full_mut()?;
{
let buy_pos = account
.ensure_token_position(token_conditional_swap.buy_token_index)?
.0;
buy_pos.increment_in_use();
let sell_pos = account
.ensure_token_position(token_conditional_swap.sell_token_index)?
.0;
sell_pos.increment_in_use();
}
let id = account.fixed.next_token_conditional_swap_id;
account.fixed.next_token_conditional_swap_id =
account.fixed.next_token_conditional_swap_id.wrapping_add(1);
let tcs = account.free_token_conditional_swap_mut()?;
*tcs = token_conditional_swap;
tcs.id = id;
tcs.taker_fee_fraction = group.token_conditional_swap_taker_fee_fraction;
tcs.maker_fee_fraction = group.token_conditional_swap_maker_fee_fraction;
tcs.has_data = 1;
tcs.bought = 0;
tcs.sold = 0;
require_neq!(tcs.buy_token_index, tcs.sell_token_index);
require_gte!(tcs.price_premium_fraction, 0.0);
require_gte!(tcs.maker_fee_fraction, 0.0);
require_gte!(tcs.taker_fee_fraction, 0.0);
require_gte!(tcs.price_lower_limit, 0.0);
require_gte!(tcs.price_upper_limit, 0.0);
emit!(TokenConditionalSwapCreateLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
id,
max_buy: tcs.max_buy,
max_sell: tcs.max_sell,
expiry_timestamp: tcs.expiry_timestamp,
price_lower_limit: tcs.price_lower_limit,
price_upper_limit: tcs.price_upper_limit,
price_premium_fraction: tcs.price_premium_fraction,
taker_fee_fraction: tcs.taker_fee_fraction,
maker_fee_fraction: tcs.maker_fee_fraction,
buy_token_index: tcs.buy_token_index,
sell_token_index: tcs.sell_token_index,
allow_creating_borrows: tcs.allow_creating_borrows(),
allow_creating_deposits: tcs.allow_creating_deposits(),
});
Ok(())
}

View File

@ -0,0 +1,901 @@
use anchor_lang::prelude::*;
use fixed::types::I80F48;
use crate::accounts_ix::*;
use crate::error::*;
use crate::health::*;
use crate::i80f48::ClampToInt;
use crate::logs::TokenConditionalSwapCancelLog;
use crate::logs::{
LoanOriginationFeeInstruction, TokenBalanceLog, TokenConditionalSwapTriggerLog, WithdrawLoanLog,
};
use crate::state::*;
#[allow(clippy::too_many_arguments)]
pub fn token_conditional_swap_trigger(
ctx: Context<TokenConditionalSwapTrigger>,
token_conditional_swap_index: usize,
token_conditional_swap_id: u64,
max_buy_token_to_liqee: u64,
max_sell_token_to_liqor: u64,
) -> Result<()> {
let group_pk = &ctx.accounts.group.key();
let liqee_key = ctx.accounts.liqee.key();
let liqor_key = ctx.accounts.liqor.key();
require_keys_neq!(liqee_key, liqor_key);
let mut liqor = ctx.accounts.liqor.load_full_mut()?;
require_msg_typed!(
!liqor.fixed.being_liquidated(),
MangoError::BeingLiquidated,
"liqor account"
);
let mut account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)
.context("create account retriever")?;
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
let tcs = liqee.token_conditional_swap_by_index(token_conditional_swap_index)?;
require!(tcs.has_data(), MangoError::SomeError);
require_eq!(tcs.id, token_conditional_swap_id);
let buy_token_index = tcs.buy_token_index;
let sell_token_index = tcs.sell_token_index;
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
let tcs_is_expired = tcs.is_expired(now_ts);
// As a precaution, ensure that the liqee (and its health cache) will have an entry for both tokens:
// we will want to adjust their values later. This is already guaranteed by the in_use_count
// 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)
.context("create liqee health cache")?;
let (buy_bank, buy_token_price, sell_bank_and_oracle_opt) =
account_retriever.banks_mut_and_oracles(buy_token_index, sell_token_index)?;
let (sell_bank, sell_token_price) = sell_bank_and_oracle_opt.unwrap();
// Possibly wipe the tcs and exit, if it's already expired
if tcs_is_expired {
let tcs = liqee.token_conditional_swap_mut_by_index(token_conditional_swap_index)?;
*tcs = TokenConditionalSwap::default();
// Release the hold on token positions and potentially close them
liqee.token_decrement_dust_deactivate(buy_bank, now_ts, liqee_key)?;
liqee.token_decrement_dust_deactivate(sell_bank, now_ts, liqee_key)?;
msg!("TokenConditionalSwap is expired, removing");
emit!(TokenConditionalSwapCancelLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.liqee.key(),
id: token_conditional_swap_id,
});
return Ok(());
}
let liqee_pre_init_health = liqee.check_health_pre(&liqee_health_cache)?;
let (liqee_buy_change, liqee_sell_change) = action(
&mut liqor.borrow_mut(),
liqor_key,
&mut liqee.borrow_mut(),
liqee_key,
&mut liqee_health_cache,
token_conditional_swap_index,
buy_bank,
buy_token_price,
max_buy_token_to_liqee,
sell_bank,
sell_token_price,
max_sell_token_to_liqor,
now_ts,
)?;
// Check liqee and liqor health
liqee.check_health_post(&liqee_health_cache, liqee_pre_init_health)?;
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
.context("compute liqor health")?;
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
Ok(())
}
/// Figure out the trade amounts based on:
/// - the max requested
/// - remainder on the tcs
/// - allow_deposits / allow_borrows flags
/// - bank reduce only state
///
/// Returns (buy_amount, sell_amount)
fn trade_amount(
tcs: &TokenConditionalSwap,
sell_per_buy_price: I80F48,
max_buy_token_to_liqee: u64,
max_sell_token_to_liqor: u64,
liqee_buy_balance: I80F48,
liqee_sell_balance: I80F48,
liqor_buy_balance: I80F48,
liqor_sell_balance: I80F48,
buy_bank: &Bank,
sell_bank: &Bank,
) -> (u64, u64) {
let max_buy = max_buy_token_to_liqee
.min(tcs.remaining_buy())
.min(
if tcs.allow_creating_deposits() && !buy_bank.are_deposits_reduce_only() {
u64::MAX
} else {
// ceil() because we're ok reaching 0..1 deposited native tokens
(-liqee_buy_balance).ceil().clamp_to_u64()
},
)
.min(if buy_bank.are_borrows_reduce_only() {
// floor() so we never go below 0
liqor_buy_balance.floor().clamp_to_u64()
} else {
u64::MAX
});
let max_sell = max_sell_token_to_liqor
.min(tcs.remaining_sell())
.min(
if tcs.allow_creating_borrows() && !sell_bank.are_borrows_reduce_only() {
u64::MAX
} else {
// floor() so we never go below 0
liqee_sell_balance.floor().clamp_to_u64()
},
)
.min(if sell_bank.are_deposits_reduce_only() {
// ceil() because we're ok reaching 0..1 deposited native tokens
(-liqor_sell_balance).ceil().clamp_to_u64()
} else {
u64::MAX
});
trade_amount_inner(max_buy, max_sell, sell_per_buy_price)
}
fn trade_amount_inner(max_buy: u64, max_sell: u64, sell_per_buy_price: I80F48) -> (u64, u64) {
// This logic looks confusing, but also check the test_trade_amount_inner
let buy_for_sell: u64 = if sell_per_buy_price > I80F48::ONE {
// Example: max_sell=1 and price=1.9. Result should be buy=1, sell=1
// since we're ok flooring the sell amount.
((I80F48::from(max_sell) + I80F48::ONE - I80F48::DELTA) / sell_per_buy_price)
.floor()
.to_num()
} else {
// Example: max_buy=7, max_sell=4, price=0.6. Result should be buy=7, sell=4
// Example: max_buy=1, max_sell=1, price=0.01. Result should be 0, 0
((I80F48::from(max_buy) * sell_per_buy_price)
.floor()
.min(I80F48::from(max_sell))
/ sell_per_buy_price)
.ceil()
.to_num()
};
let buy_amount = max_buy.min(buy_for_sell);
let sell_for_buy = (I80F48::from(buy_amount) * sell_per_buy_price)
.floor()
.to_num::<u64>();
let sell_amount = max_sell.min(sell_for_buy);
// Invariant: never exchange something for nothing.
// the proof for buy==0 => sell==0 is trivial, other directly is less clear but should hold
assert!(!((buy_amount > 0) ^ (sell_amount > 0)));
(buy_amount, sell_amount)
}
fn action(
liqor: &mut MangoAccountRefMut,
liqor_key: Pubkey,
liqee: &mut MangoAccountRefMut,
liqee_key: Pubkey,
liqee_health_cache: &mut HealthCache,
token_conditional_swap_index: usize,
buy_bank: &mut Bank,
buy_token_price: I80F48,
max_buy_token_to_liqee: u64,
sell_bank: &mut Bank,
sell_token_price: I80F48,
max_sell_token_to_liqor: u64,
now_ts: u64,
) -> Result<(I80F48, I80F48)> {
let tcs = liqee
.token_conditional_swap_by_index(token_conditional_swap_index)?
.clone();
require!(tcs.has_data(), MangoError::SomeError);
require!(!tcs.is_expired(now_ts), MangoError::SomeError);
require_eq!(buy_bank.token_index, tcs.buy_token_index);
require_eq!(sell_bank.token_index, tcs.sell_token_index);
// amount of sell token native per buy token native
let price = buy_token_price.to_num::<f64>() / sell_token_price.to_num::<f64>();
require!(
tcs.price_in_range(price),
MangoError::TokenConditionalSwapPriceNotInRange
);
let premium_price = tcs.premium_price(price);
let maker_price = tcs.maker_price(premium_price);
let maker_price_i80f48 = I80F48::from_num(maker_price);
let pre_liqee_buy_token = liqee.token_position(tcs.buy_token_index)?.native(&buy_bank);
let pre_liqee_sell_token = liqee
.token_position(tcs.sell_token_index)?
.native(&sell_bank);
let pre_liqor_buy_token = liqor
.ensure_token_position(tcs.buy_token_index)?
.0
.native(&buy_bank);
let pre_liqor_sell_token = liqor
.ensure_token_position(tcs.sell_token_index)?
.0
.native(&sell_bank);
// derive trade amount based on limits in the tcs and by the liqor
// the sell_token_amount_from_liqee is the amount to deduct from the liqee, it's adjusted upwards
// for the taker fee (since this is included in the maker_price)
let (buy_token_amount, sell_token_amount_from_liqee) = trade_amount(
&tcs,
maker_price_i80f48,
max_buy_token_to_liqee,
max_sell_token_to_liqor,
pre_liqee_buy_token,
pre_liqee_sell_token,
pre_liqor_buy_token,
pre_liqor_sell_token,
buy_bank,
sell_bank,
);
// NOTE: It's possible that buy_token_amount == sell_token_amount == 0!
// Proceed with it anyway because we already mutated the account anyway and might want
// to drop the token stop loss entry later.
let sell_token_amount =
(I80F48::from(buy_token_amount) * I80F48::from_num(premium_price)).floor();
let maker_fee = tcs.maker_fee(sell_token_amount);
let taker_fee = tcs.taker_fee(sell_token_amount);
let sell_token_amount_to_liqor = sell_token_amount_from_liqee - maker_fee - taker_fee;
// do the token transfer between liqee and liqor
let buy_token_amount_i80f48 = I80F48::from(buy_token_amount);
let (liqee_buy_token, liqee_buy_raw_index) = liqee.token_position_mut(tcs.buy_token_index)?;
let (liqor_buy_token, liqor_buy_raw_index) = liqor.token_position_mut(tcs.buy_token_index)?;
let liqee_buy_active = 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 post_liqee_buy_token = liqee_buy_token.native(&buy_bank);
let post_liqor_buy_token = liqor_buy_token.native(&buy_bank);
let (liqee_sell_token, liqee_sell_raw_index) =
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(
liqor_sell_token,
I80F48::from(sell_token_amount_to_liqor),
now_ts,
)?;
let liqee_sell_withdraw = sell_bank.withdraw_with_fee(
liqee_sell_token,
I80F48::from(sell_token_amount_from_liqee),
now_ts,
)?;
sell_bank.collected_fees_native += I80F48::from(maker_fee + taker_fee);
// No need to check net borrow limits on buy_bank or sell_bank, because this mostly transfers
// tokens between two accounts. For the sell token, the withdraw is higher than the deposit
// due to fees, so net borrows can technically increase a bit: but the difference gets "deposited"
// into collected_fees_native.
let post_liqee_sell_token = liqee_sell_token.native(&sell_bank);
let post_liqor_sell_token = liqor_sell_token.native(&sell_bank);
// With a scanning account retriever, it's safe to deactivate inactive token positions immediately
if !liqee_buy_active {
liqee.deactivate_token_position_and_log(liqee_buy_raw_index, liqee_key);
}
if !liqee_sell_withdraw.position_is_active {
liqee.deactivate_token_position_and_log(liqee_sell_raw_index, liqee_key);
}
if !liqor_buy_withdraw.position_is_active {
liqor.deactivate_token_position_and_log(liqor_buy_raw_index, liqor_key);
}
if !liqor_sell_active {
liqor.deactivate_token_position_and_log(liqor_sell_raw_index, liqor_key)
}
// Log info
// liqee buy token
emit!(TokenBalanceLog {
mango_group: liqee.fixed.group,
mango_account: liqee_key,
token_index: tcs.buy_token_index,
indexed_position: post_liqee_buy_token.to_bits(),
deposit_index: buy_bank.deposit_index.to_bits(),
borrow_index: buy_bank.borrow_index.to_bits(),
});
// liqee sell token
emit!(TokenBalanceLog {
mango_group: liqee.fixed.group,
mango_account: liqee_key,
token_index: tcs.sell_token_index,
indexed_position: post_liqee_sell_token.to_bits(),
deposit_index: sell_bank.deposit_index.to_bits(),
borrow_index: sell_bank.borrow_index.to_bits(),
});
// liqor buy token
emit!(TokenBalanceLog {
mango_group: liqee.fixed.group,
mango_account: liqor_key,
token_index: tcs.buy_token_index,
indexed_position: post_liqor_buy_token.to_bits(),
deposit_index: buy_bank.deposit_index.to_bits(),
borrow_index: buy_bank.borrow_index.to_bits(),
});
// liqor sell token
emit!(TokenBalanceLog {
mango_group: liqee.fixed.group,
mango_account: liqor_key,
token_index: tcs.sell_token_index,
indexed_position: post_liqor_sell_token.to_bits(),
deposit_index: sell_bank.deposit_index.to_bits(),
borrow_index: sell_bank.borrow_index.to_bits(),
});
if liqor_buy_withdraw.has_loan() {
emit!(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(),
instruction: LoanOriginationFeeInstruction::TokenConditionalSwapTrigger,
price: Some(buy_token_price.to_bits()),
});
}
if liqee_sell_withdraw.has_loan() {
emit!(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(),
instruction: LoanOriginationFeeInstruction::TokenConditionalSwapTrigger,
price: Some(sell_token_price.to_bits()),
});
}
// Check liqee health after the transaction
// using sell_token_amount_i80f48 here would not account for loan origination fees!
let liqee_buy_change = buy_token_amount_i80f48;
let liqee_sell_change = post_liqee_sell_token - pre_liqee_sell_token;
liqee_health_cache.adjust_token_balance(&buy_bank, liqee_buy_change)?;
liqee_health_cache.adjust_token_balance(&sell_bank, liqee_sell_change)?;
// update tcs information on the account
let closed = {
// record amount
let tcs = liqee.token_conditional_swap_mut_by_index(token_conditional_swap_index)?;
tcs.bought += buy_token_amount;
tcs.sold += sell_token_amount_from_liqee;
assert!(tcs.bought <= tcs.max_buy);
assert!(tcs.sold <= tcs.max_sell);
// Maybe remove token stop loss entry
//
// This drops the tcs if no more swapping is possible at the current price:
// - if bought/sold reached the max
// - if the "don't create deposits/borrows" constraint is reached
// - if the price is such that swapping 1 native token would already exceed the buy/sell limit
// - if the liqee health is so low that we believe the triggerer attempted to
// trigger as much as was possible given the liqee's account health
let (future_buy, future_sell) = trade_amount(
tcs,
maker_price_i80f48,
u64::MAX,
u64::MAX,
post_liqee_buy_token,
post_liqee_sell_token,
I80F48::MAX, // other liqors might not have reduce-only related restrictions
I80F48::MIN,
buy_bank,
sell_bank,
);
// The health check depends on the account's health _ratio_ because it needs to work
// with liquidators trying to trigger tcs maximally: they can't bring the health exactly
// to 0 or even very close to it, because oracles will change before the transaction
// is executed. So instead, they will target a certain health ratio.
// This says, that as long as they bring the account's health ratio below 1%, we will
// consider the tcs as fully executed.
let liqee_health_is_low =
liqee_health_cache.health_ratio(HealthType::Init) < I80F48::from(1);
if future_buy == 0 || future_sell == 0 || liqee_health_is_low {
*tcs = TokenConditionalSwap::default();
true
} else {
false
}
};
if closed {
// Free up token position locks, maybe dusting and deactivating them
liqee.token_decrement_dust_deactivate(buy_bank, now_ts, liqee_key)?;
liqee.token_decrement_dust_deactivate(sell_bank, now_ts, liqee_key)?;
}
emit!(TokenConditionalSwapTriggerLog {
mango_group: liqee.fixed.group,
liqee: liqee_key,
liqor: liqor_key,
token_conditional_swap_id: tcs.id,
buy_token_index: tcs.buy_token_index,
sell_token_index: tcs.sell_token_index,
buy_amount: buy_token_amount,
sell_amount: sell_token_amount_from_liqee,
maker_fee,
taker_fee,
buy_token_price: buy_token_price.to_bits(),
sell_token_price: sell_token_price.to_bits(),
closed,
});
// Return the change in liqee token account balances
Ok((liqee_buy_change, liqee_sell_change))
}
#[cfg(test)]
mod tests {
use bytemuck::Zeroable;
use super::*;
use crate::health::test::*;
#[test]
fn test_trade_amount_inner() {
let cases = vec![
("null 1", (0, 0, 1.0), (0, 0)),
("null 2", (0, 10, 1.0), (0, 0)),
("null 3", (10, 0, 1.0), (0, 0)),
("buy limit 1", (10, 30, 2.11), (10, 21)),
("buy limit 2", (10, 50, 0.75), (10, 7)),
("sell limit 1", (10, 15, 2.1), (7, 14)),
("sell limit 2", (10, 5, 0.75), (7, 5)),
("sell limit 3", (50, 50, 1.1), (46, 50)),
("sell limit 4", (60, 50, 0.9), (56, 50)),
("less than one 1", (10, 10, 100.0), (0, 0)),
("less than one 2", (10, 10, 0.001), (0, 0)),
("round 1", (10, 110, 100.0), (1, 100)),
("round 2", (199, 10, 0.01), (100, 1)),
];
for (name, (max_buy, max_sell, price), (buy_amount, sell_amount)) in cases {
println!("{name}");
let (actual_buy, actual_sell) =
trade_amount_inner(max_buy, max_sell, I80F48::from_num(price));
println!("actual: {actual_buy} {actual_sell}, expected: {buy_amount}, {sell_amount}");
assert_eq!(actual_buy, buy_amount);
assert_eq!(actual_sell, (actual_buy as f64 * price).floor() as u64); // invariant
assert_eq!(actual_sell, sell_amount);
}
}
#[test]
fn test_trade_amount_outer() {
let cases = vec![
(
"limit 1",
(1, 100, 100, 100, 1.0),
(0.0, 0.0, true, true),
(0.0, 0.0, 0, 0),
(1, 1),
),
(
"limit 2",
(100, 1, 100, 100, 1.0),
(0.0, 0.0, true, true),
(0.0, 0.0, 0, 0),
(1, 1),
),
(
"limit 3",
(100, 100, 1, 100, 1.0),
(0.0, 0.0, true, true),
(0.0, 0.0, 0, 0),
(1, 1),
),
(
"limit 4",
(100, 100, 100, 1, 1.0),
(0.0, 0.0, true, true),
(0.0, 0.0, 0, 0),
(1, 1),
),
(
"limit 5",
(100, 100, 100, 100, 1.0),
(-0.3, 0.0, false, true),
(0.0, 0.0, 0, 0),
(1, 1),
),
(
"limit 6",
(100, 100, 100, 100, 1.0),
(0.0, 1.8, true, false),
(0.0, 0.0, 0, 0),
(1, 1),
),
(
"full 1",
(100, 100, 100, 100, 1.0),
(-100.0, 100.0, false, false),
(0.0, 0.0, 0, 0),
(100, 100),
),
(
"full 2",
(100, 100, 100, 100, 1.0),
(0.0, 0.0, true, true),
(0.0, 0.0, 0, 0),
(100, 100),
),
(
"reduce only buy 1",
(100, 100, 100, 100, 1.0),
(-10.0, 0.0, true, true),
(20.0, 0.0, 1, 0),
(10, 10),
),
(
"reduce only buy 2",
(100, 100, 100, 100, 1.0),
(-20.0, 0.0, true, true),
(10.0, 0.0, 1, 0),
(10, 10),
),
(
"reduce only buy 3",
(100, 100, 100, 100, 1.0),
(-10.0, 0.0, true, true),
(20.0, 0.0, 2, 0),
(20, 20),
),
(
"reduce only sell 1",
(100, 100, 100, 100, 1.0),
(0.0, 10.0, true, true),
(0.0, -20.0, 0, 1),
(10, 10),
),
(
"reduce only sell 2",
(100, 100, 100, 100, 1.0),
(0.0, 20.0, true, true),
(0.0, -10.0, 0, 1),
(10, 10),
),
(
"reduce only sell 3",
(100, 100, 100, 100, 1.0),
(0.0, 20.0, true, true),
(0.0, -10.0, 0, 2),
(20, 20),
),
(
"price 1",
(100, 100, 100, 100, 1.23456),
(0.0, 0.0, true, true),
(0.0, 0.0, 0, 0),
(81, 99),
),
(
"price 2",
(100, 100, 100, 100, 0.76543),
(0.0, 0.0, true, true),
(0.0, 0.0, 0, 0),
(100, 76),
),
];
for (
name,
(tcs_buy, tcs_sell, liqor_buy, liqor_sell, price),
(liqee_buy_balance, liqee_sell_balance, liqee_allow_deposit, liqee_allow_borrow),
(liqor_buy_balance, liqor_sell_balance, buy_reduce_only, sell_reduce_only),
(buy_amount, sell_amount),
) in cases
{
println!("{name}");
let tcs = TokenConditionalSwap {
max_buy: 42 + tcs_buy,
max_sell: 100 + tcs_sell,
bought: 42,
sold: 100,
allow_creating_borrows: u8::from(liqee_allow_borrow),
allow_creating_deposits: u8::from(liqee_allow_deposit),
..Default::default()
};
let buy_bank = Bank {
reduce_only: buy_reduce_only,
..Bank::zeroed()
};
let sell_bank = Bank {
reduce_only: sell_reduce_only,
..Bank::zeroed()
};
let (actual_buy, actual_sell) = trade_amount(
&tcs,
I80F48::from_num(price),
liqor_buy,
liqor_sell,
I80F48::from_num(liqee_buy_balance),
I80F48::from_num(liqee_sell_balance),
I80F48::from_num(liqor_buy_balance),
I80F48::from_num(liqor_sell_balance),
&buy_bank,
&sell_bank,
);
println!("actual: {actual_buy} {actual_sell}, expected: {buy_amount}, {sell_amount}");
assert_eq!(actual_buy, buy_amount);
assert_eq!(actual_sell, (actual_buy as f64 * price).floor() as u64); // invariant
assert_eq!(actual_sell, sell_amount);
}
}
#[derive(Clone)]
struct TestSetup {
group: Pubkey,
asset_bank: TestAccount<Bank>,
liab_bank: TestAccount<Bank>,
asset_oracle: TestAccount<StubOracle>,
liab_oracle: TestAccount<StubOracle>,
liqee: MangoAccountValue,
liqor: MangoAccountValue,
}
impl TestSetup {
fn new() -> Self {
let group = Pubkey::new_unique();
let (asset_bank, asset_oracle) = mock_bank_and_oracle(group, 0, 1.0, 0.0, 0.0);
let (liab_bank, liab_oracle) = mock_bank_and_oracle(group, 1, 1.0, 0.0, 0.0);
let mut liqee_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
liqee_buffer.extend_from_slice(&[0u8; 256]);
let mut liqee = MangoAccountValue::from_bytes(&liqee_buffer).unwrap();
{
liqee.expand_dynamic_content(3, 5, 4, 6, 1).unwrap();
liqee.ensure_token_position(0).unwrap();
liqee.ensure_token_position(1).unwrap();
}
let liqor_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut liqor = MangoAccountValue::from_bytes(&liqor_buffer).unwrap();
{
liqor.ensure_token_position(0).unwrap();
liqor.ensure_token_position(1).unwrap();
}
Self {
group,
asset_bank,
liab_bank,
asset_oracle,
liab_oracle,
liqee,
liqor,
}
}
fn liqee_asset_pos(&mut self) -> I80F48 {
self.liqee
.token_position(0)
.unwrap()
.native(self.asset_bank.data())
}
fn liqee_liab_pos(&mut self) -> I80F48 {
self.liqee
.token_position(1)
.unwrap()
.native(self.liab_bank.data())
}
fn liqor_asset_pos(&mut self) -> I80F48 {
self.liqor
.token_position(0)
.unwrap()
.native(self.asset_bank.data())
}
fn liqor_liab_pos(&mut self) -> I80F48 {
self.liqor
.token_position(1)
.unwrap()
.native(self.liab_bank.data())
}
fn trigger(
&mut self,
buy_price: f64,
buy_max: u64,
sell_price: f64,
sell_max: u64,
) -> Result<(I80F48, I80F48)> {
let mut setup = self.clone();
let ais = vec![
setup.asset_bank.as_account_info(),
setup.liab_bank.as_account_info(),
setup.asset_oracle.as_account_info(),
setup.liab_oracle.as_account_info(),
];
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();
action(
&mut self.liqor.borrow_mut(),
Pubkey::default(),
&mut self.liqee.borrow_mut(),
Pubkey::default(),
&mut liqee_health_cache,
0,
self.liab_bank.data(),
I80F48::from_num(buy_price),
buy_max,
self.asset_bank.data(),
I80F48::from_num(sell_price),
sell_max,
0,
)
}
}
#[test]
fn test_token_conditional_swap_trigger() {
let mut setup = TestSetup::new();
setup
.asset_bank
.data()
.deposit(
&mut setup.liqee.token_position_mut(0).unwrap().0,
I80F48::from(1000),
0,
)
.unwrap();
let tcs = TokenConditionalSwap {
max_buy: 100,
max_sell: 100,
price_lower_limit: 1.0,
price_upper_limit: 3.0,
price_premium_fraction: 0.11,
buy_token_index: 1,
sell_token_index: 0,
has_data: 1,
allow_creating_borrows: 1,
allow_creating_deposits: 1,
..Default::default()
};
*setup.liqee.free_token_conditional_swap_mut().unwrap() = tcs.clone();
assert_eq!(setup.liqee.active_token_conditional_swaps().count(), 1);
assert!(setup.trigger(0.99, 40, 1.0, 100,).is_err());
assert!(setup.trigger(1.0, 40, 0.33, 100,).is_err());
let (buy_change, sell_change) = setup.trigger(2.0, 40, 1.0, 100).unwrap();
assert_eq!(buy_change.round(), 40);
assert_eq!(sell_change.round(), -88);
assert_eq!(setup.liqee.active_token_conditional_swaps().count(), 1);
let tcs = setup
.liqee
.token_conditional_swap_by_index(0)
.unwrap()
.clone();
assert_eq!(tcs.bought, 40);
assert_eq!(tcs.sold, 88);
assert_eq!(setup.liqee_liab_pos().round(), 40);
assert_eq!(setup.liqee_asset_pos().round(), 1000 - 88);
assert_eq!(setup.liqor_liab_pos().round(), -40);
assert_eq!(setup.liqor_asset_pos().round(), 88);
let (buy_change, sell_change) = setup.trigger(2.0, 40, 1.0, 100).unwrap();
assert_eq!(buy_change.round(), 5);
assert_eq!(sell_change.round(), -11);
assert_eq!(setup.liqee.active_token_conditional_swaps().count(), 0);
assert_eq!(setup.liqee_liab_pos().round(), 45);
assert_eq!(setup.liqee_asset_pos().round(), 1000 - 99);
assert_eq!(setup.liqor_liab_pos().round(), -45);
assert_eq!(setup.liqor_asset_pos().round(), 99);
}
#[test]
fn test_token_conditional_swap_low_health_close() {
let mut setup = TestSetup::new();
setup
.asset_bank
.data()
.deposit(
&mut setup.liqee.token_position_mut(0).unwrap().0,
I80F48::from(100),
0,
)
.unwrap();
let tcs = TokenConditionalSwap {
max_buy: 10000,
max_sell: 10000,
price_lower_limit: 1.0,
price_upper_limit: 3.0,
price_premium_fraction: 0.0,
buy_token_index: 1,
sell_token_index: 0,
has_data: 1,
allow_creating_borrows: 1,
allow_creating_deposits: 1,
..Default::default()
};
*setup.liqee.free_token_conditional_swap_mut().unwrap() = tcs.clone();
let (buy_change, sell_change) = setup.trigger(2.0, 1000, 1.0, 1000).unwrap();
assert_eq!(buy_change.round(), 500);
assert_eq!(sell_change.round(), -1000);
// Overall health went negative, causing the tcs to close (even though max_buy/max_sell aren't reached)
assert_eq!(setup.liqee.active_token_conditional_swaps().count(), 0);
assert_eq!(setup.liqee_liab_pos().round(), 500);
assert_eq!(setup.liqee_asset_pos().round(), -900);
}
#[test]
fn test_token_conditional_swap_trigger_fees() {
let mut setup = TestSetup::new();
let tcs = TokenConditionalSwap {
max_buy: 1000,
max_sell: 1000,
price_lower_limit: 1.0,
price_upper_limit: 3.0,
price_premium_fraction: 0.02,
maker_fee_fraction: 0.03,
taker_fee_fraction: 0.05,
buy_token_index: 1,
sell_token_index: 0,
has_data: 1,
allow_creating_borrows: 1,
allow_creating_deposits: 1,
..Default::default()
};
*setup.liqee.free_token_conditional_swap_mut().unwrap() = tcs.clone();
assert_eq!(setup.liqee.active_token_conditional_swaps().count(), 1);
let (buy_change, sell_change) = setup.trigger(1.0, 1000, 1.0, 1000).unwrap();
assert_eq!(buy_change.round(), 952);
assert_eq!(sell_change.round(), -1000);
assert_eq!(setup.liqee_liab_pos().round(), 952);
assert_eq!(setup.liqee_asset_pos().round(), -1000);
assert_eq!(setup.liqor_liab_pos().round(), -952);
assert_eq!(setup.liqor_asset_pos().round(), 923); // floor(952*1.02*0.95)
assert_eq!(setup.asset_bank.data().collected_fees_native, 77);
}
}

View File

@ -69,8 +69,10 @@ pub fn token_liq_bankruptcy(
// In particular, a very negative perp hupnl does not allow token bankruptcy to happen,
// and if the perp hupnl is positive, we need to liquidate that before dealing with token
// bankruptcy!
require_gt!(I80F48::ZERO, initial_liab_native);
require_gt!(I80F48::ZERO, liqee_liab_health_balance);
// guaranteed positive
let mut remaining_liab_loss = (-initial_liab_native).min(-liqee_liab_health_balance);
require_gt!(remaining_liab_loss, I80F48::ZERO);
// We pay for the liab token in quote. Example: SOL is at $20 and USDC is at $2, then for a liab
// of 3 SOL, we'd pay 3 * 20 / 2 * (1+fee) = 30 * (1+fee) USDC.

View File

@ -89,14 +89,20 @@ pub fn token_register(
deposit_weight_scale_start_quote: f64::MAX,
reduce_only: 0,
force_close: 0,
reserved: [0; 2118],
padding: Default::default(),
fees_withdrawn: 0,
reserved: [0; 2104],
};
require_gt!(bank.max_rate, MINIMUM_MAX_RATE);
let oracle_price =
bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, None)?;
bank.stable_price_model
.reset_to_price(oracle_price.to_num(), now_ts);
if let Ok(oracle_price) =
bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, None)
{
bank.stable_price_model
.reset_to_price(oracle_price.to_num(), now_ts);
} else {
bank.stable_price_model.reset_on_nonzero_price = 1;
}
let mut mint_info = ctx.accounts.mint_info.load_init()?;
*mint_info = MintInfo {

View File

@ -75,14 +75,20 @@ pub fn token_register_trustless(
deposit_weight_scale_start_quote: 5_000_000_000.0, // $5k
reduce_only: 2, // deposit-only
force_close: 0,
reserved: [0; 2118],
padding: Default::default(),
fees_withdrawn: 0,
reserved: [0; 2104],
};
require_gt!(bank.max_rate, MINIMUM_MAX_RATE);
let oracle_price =
bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, None)?;
bank.stable_price_model
.reset_to_price(oracle_price.to_num(), now_ts);
if let Ok(oracle_price) =
bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, None)
{
bank.stable_price_model
.reset_to_price(oracle_price.to_num(), now_ts);
} else {
bank.stable_price_model.reset_on_nonzero_price = 1;
}
let mut mint_info = ctx.accounts.mint_info.load_init()?;
*mint_info = MintInfo {

View File

@ -32,7 +32,7 @@ compile_error!("compiling the program entrypoint without 'enable-gpl' makes no s
use state::{
OracleConfigParams, PerpMarketIndex, PlaceOrderType, SelfTradeBehavior, Serum3MarketIndex,
Side, TokenIndex,
Side, TokenConditionalSwap, TokenIndex,
};
declare_id!("4MangoMjqJ2firMokCjjGgoK8d4MXcrgL7XJaL3w6fVg");
@ -43,6 +43,18 @@ pub mod mango_v4 {
use super::*;
use error::*;
pub fn adming_token_withdraw_fees(ctx: Context<AdminTokenWithdrawFees>) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::admin_token_withdraw_fees(ctx)?;
Ok(())
}
pub fn admin_perp_withdraw_fees(ctx: Context<AdminPerpWithdrawFees>) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::admin_perp_withdraw_fees(ctx)?;
Ok(())
}
pub fn group_create(
ctx: Context<GroupCreate>,
group_num: u32,
@ -68,6 +80,8 @@ pub mod mango_v4 {
buyback_fees_swap_mango_account_opt: Option<Pubkey>,
mngo_token_index_opt: Option<TokenIndex>,
buyback_fees_expiry_interval_opt: Option<u64>,
token_conditional_swap_taker_fee_fraction_opt: Option<f32>,
token_conditional_swap_maker_fee_fraction_opt: Option<f32>,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::group_edit(
@ -83,6 +97,8 @@ pub mod mango_v4 {
buyback_fees_swap_mango_account_opt,
mngo_token_index_opt,
buyback_fees_expiry_interval_opt,
token_conditional_swap_taker_fee_fraction_opt,
token_conditional_swap_maker_fee_fraction_opt,
)?;
Ok(())
}
@ -271,7 +287,27 @@ pub mod mango_v4 {
perp_oo_count: u8,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::account_expand(ctx, token_count, serum3_count, perp_count, perp_oo_count)?;
instructions::account_expand(ctx, token_count, serum3_count, perp_count, perp_oo_count, 0)?;
Ok(())
}
pub fn account_expand_v2(
ctx: Context<AccountExpand>,
token_count: u8,
serum3_count: u8,
perp_count: u8,
perp_oo_count: u8,
token_conditional_swap_count: u8,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::account_expand(
ctx,
token_count,
serum3_count,
perp_count,
perp_oo_count,
token_conditional_swap_count,
)?;
Ok(())
}
@ -415,9 +451,10 @@ pub mod mango_v4 {
ctx: Context<Serum3EditMarket>,
reduce_only_opt: Option<bool>,
force_close_opt: Option<bool>,
name_opt: Option<String>,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::serum3_edit_market(ctx, reduce_only_opt, force_close_opt)?;
instructions::serum3_edit_market(ctx, reduce_only_opt, force_close_opt, name_opt)?;
Ok(())
}
@ -487,13 +524,14 @@ pub mod mango_v4 {
Ok(())
}
/// Settles all free funds from the OpenOrders account into the MangoAccount.
/// Deprecated instruction that used to settles all free funds from the OpenOrders account
/// into the MangoAccount.
///
/// Any serum "referrer rebates" (ui fees) are considered Mango fees.
pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::serum3_settle_funds(ctx.accounts, None, true)?;
Ok(())
Err(error_msg!(
"Serum3SettleFunds was replaced by Serum3SettleFundsV2"
))
}
/// Like Serum3SettleFunds, but `fees_to_dao` determines if referrer rebates are considered fees
@ -1112,6 +1150,75 @@ pub mod mango_v4 {
Ok(())
}
pub fn token_conditional_swap_create(
ctx: Context<TokenConditionalSwapCreate>,
max_buy: u64,
max_sell: u64,
expiry_timestamp: u64,
price_lower_limit: f64,
price_upper_limit: f64,
price_premium_fraction: f64,
allow_creating_deposits: bool,
allow_creating_borrows: bool,
) -> Result<()> {
let tcs = TokenConditionalSwap {
id: u64::MAX, // set inside
max_buy,
max_sell,
bought: 0,
sold: 0,
expiry_timestamp,
price_lower_limit,
price_upper_limit,
price_premium_fraction,
taker_fee_fraction: 0.0, // set inside
maker_fee_fraction: 0.0, // set inside
buy_token_index: ctx.accounts.buy_bank.load()?.token_index,
sell_token_index: ctx.accounts.sell_bank.load()?.token_index,
has_data: 1,
allow_creating_deposits: u8::from(allow_creating_deposits),
allow_creating_borrows: u8::from(allow_creating_borrows),
reserved: [0; 113],
};
#[cfg(feature = "enable-gpl")]
instructions::token_conditional_swap_create(ctx, tcs)?;
Ok(())
}
pub fn token_conditional_swap_cancel(
ctx: Context<TokenConditionalSwapCancel>,
token_conditional_swap_index: u8,
token_conditional_swap_id: u64,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::token_conditional_swap_cancel(
ctx,
token_conditional_swap_index.into(),
token_conditional_swap_id,
)?;
Ok(())
}
// NOTE: It's the triggerer's job to compute liqor_max_* numbers that work with the liqee's health.
pub fn token_conditional_swap_trigger(
ctx: Context<TokenConditionalSwapTrigger>,
token_conditional_swap_index: u8,
token_conditional_swap_id: u64,
max_buy_token_to_liqee: u64,
max_sell_token_to_liqor: u64,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::token_conditional_swap_trigger(
ctx,
token_conditional_swap_index.into(),
token_conditional_swap_id,
max_buy_token_to_liqee,
max_sell_token_to_liqor,
)?;
Ok(())
}
pub fn alt_set(ctx: Context<AltSet>, index: u8) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::alt_set(ctx, index)?;

View File

@ -137,6 +137,33 @@ pub struct FillLogV2 {
pub quantity: i64, // number of base lots
}
#[event]
pub struct FillLogV3 {
pub mango_group: Pubkey,
pub market_index: u16,
pub taker_side: u8, // side from the taker's POV
pub maker_slot: u8,
pub maker_out: bool, // true if maker order quantity == 0
pub timestamp: u64,
pub seq_num: u64, // note: usize same as u64
pub maker: Pubkey,
pub maker_client_order_id: u64,
pub maker_fee: f32,
// Timestamp of when the maker order was placed; copied over from the LeafNode
pub maker_timestamp: u64,
pub taker: Pubkey,
pub taker_client_order_id: u64,
pub taker_fee: f32,
pub price: i64,
pub quantity: i64, // number of base lots
pub maker_closed_pnl: f64, // settle-token-native units
pub taker_closed_pnl: f64, // settle-token-native units
}
#[event]
pub struct PerpUpdateFundingLog {
pub mango_group: Pubkey,
@ -246,6 +273,7 @@ pub enum LoanOriginationFeeInstruction {
Serum3PlaceOrder,
Serum3SettleFunds,
TokenWithdraw,
TokenConditionalSwapTrigger,
}
#[event]
@ -446,3 +474,46 @@ pub struct TokenForceCloseBorrowsWithTokenLog {
pub liab_price: i128,
pub fee_factor: i128,
}
#[event]
pub struct TokenConditionalSwapCreateLog {
pub mango_group: Pubkey,
pub mango_account: Pubkey,
pub id: u64,
pub max_buy: u64,
pub max_sell: u64,
pub expiry_timestamp: u64,
pub price_lower_limit: f64,
pub price_upper_limit: f64,
pub price_premium_fraction: f64,
pub taker_fee_fraction: f32,
pub maker_fee_fraction: f32,
pub buy_token_index: u16,
pub sell_token_index: u16,
pub allow_creating_deposits: bool,
pub allow_creating_borrows: bool,
}
#[event]
pub struct TokenConditionalSwapTriggerLog {
pub mango_group: Pubkey,
pub liqee: Pubkey,
pub liqor: Pubkey,
pub token_conditional_swap_id: u64,
pub buy_token_index: u16,
pub sell_token_index: u16,
pub buy_amount: u64, // amount the liqee got
pub sell_amount: u64, // amount the liqee paid (including fees)
pub maker_fee: u64, // in native units of sell token (included in sell amount)
pub taker_fee: u64, // in native units of sell token (deducted from the sell amount the liqor received)
pub buy_token_price: i128, // I80F48
pub sell_token_price: i128, // I80F48
pub closed: bool,
}
#[event]
pub struct TokenConditionalSwapCancelLog {
pub mango_group: Pubkey,
pub mango_account: Pubkey,
pub id: u64,
}

View File

@ -16,6 +16,7 @@ use std::mem::size_of;
pub const HOUR: i64 = 3600;
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);
@ -135,8 +136,14 @@ pub struct Bank {
pub reduce_only: u8,
pub force_close: u8,
pub padding: [u8; 6],
// Do separate bookkeping for how many tokens were withdrawn
// This ensures that collected_fees_native is strictly increasing for stats gathering purposes
pub fees_withdrawn: u64,
#[derivative(Debug = "ignore")]
pub reserved: [u8; 2118],
pub reserved: [u8; 2104],
}
const_assert_eq!(
size_of::<Bank>(),
@ -165,7 +172,9 @@ const_assert_eq!(
+ 8
+ 1
+ 1
+ 2118
+ 6
+ 8
+ 2104
);
const_assert_eq!(size_of::<Bank>(), 3064);
const_assert_eq!(size_of::<Bank>() % 8, 0);
@ -176,6 +185,12 @@ pub struct WithdrawResult {
pub loan_amount: I80F48,
}
impl WithdrawResult {
pub fn has_loan(&self) -> bool {
self.loan_amount.is_positive()
}
}
impl Bank {
pub fn from_existing_bank(
existing_bank: &Bank,
@ -233,7 +248,9 @@ impl Bank {
deposit_weight_scale_start_quote: f64::MAX,
reduce_only: 0,
force_close: 0,
reserved: [0; 2118],
padding: [0; 6],
fees_withdrawn: 0,
reserved: [0; 2104],
}
}
@ -570,6 +587,19 @@ impl Bank {
})
}
/// Returns true if the position remains active
pub fn dust_if_possible(&mut self, position: &mut TokenPosition, now_ts: u64) -> Result<bool> {
if position.is_in_use() {
return Ok(true);
}
let native = position.native(self);
if native >= 0 && native < 1 {
// Withdrawing 0 triggers the dusting check
return self.withdraw_without_fee(position, I80F48::ZERO, now_ts);
}
Ok(true)
}
/// Change a position without applying the loan origination fee
pub fn change_without_fee(
&mut self,
@ -954,7 +984,7 @@ mod tests {
let mut account = TokenPosition {
indexed_position: I80F48::ZERO,
token_index: 0,
in_use_count: u8::from(is_in_use),
in_use_count: u16::from(is_in_use),
cumulative_deposit_interest: 0.0,
cumulative_borrow_interest: 0.0,
previous_index: I80F48::ZERO,

View File

@ -88,11 +88,15 @@ pub struct Group {
/// When set to 0, there's no expiry of buyback fees.
pub buyback_fees_expiry_interval: u64,
pub reserved: [u8; 1824],
/// Fees for the token conditional swap feature
pub token_conditional_swap_taker_fee_fraction: f32,
pub token_conditional_swap_maker_fee_fraction: f32,
pub reserved: [u8; 1816],
}
const_assert_eq!(
size_of::<Group>(),
32 + 4 + 32 * 2 + 4 + 32 * 2 + 4 + 4 + 20 * 32 + 32 + 8 + 16 + 32 + 8 + 1824
32 + 4 + 32 * 2 + 4 + 32 * 2 + 4 + 4 + 20 * 32 + 32 + 8 + 16 + 32 + 8 + 4 * 2 + 1816
);
const_assert_eq!(size_of::<Group>(), 2736);
const_assert_eq!(size_of::<Group>() % 8, 0);
@ -190,6 +194,21 @@ pub enum IxGate {
TokenForceCloseBorrowsWithToken = 49,
PerpForceClosePosition = 50,
GroupWithdrawInsuranceFund = 51,
TokenConditionalSwapCreate = 52,
TokenConditionalSwapTrigger = 53,
TokenConditionalSwapCancel = 54,
OpenbookV2CancelOrder = 55,
OpenbookV2CloseOpenOrders = 56,
OpenbookV2CreateOpenOrders = 57,
OpenbookV2DeregisterMarket = 58,
OpenbookV2EditMarket = 59,
OpenbookV2LiqForceCancelOrders = 60,
OpenbookV2PlaceOrder = 61,
OpenbookV2PlaceTakeOrder = 62,
OpenbookV2RegisterMarket = 63,
OpenbookV2SettleFunds = 64,
AdminTokenWithdrawFees = 65,
AdminPerpWithdrawFees = 66,
// NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction.
}

View File

@ -21,6 +21,7 @@ use super::PerpMarket;
use super::PerpMarketIndex;
use super::PerpOpenOrder;
use super::Serum3MarketIndex;
use super::TokenConditionalSwap;
use super::TokenIndex;
use super::FREE_ORDER_SLOT;
use super::{dynamic_account::*, Group};
@ -104,7 +105,10 @@ pub struct MangoAccount {
/// End timestamp of the current expiry interval of the buyback fees amount.
pub buyback_fees_expiry_timestamp: u64,
pub reserved: [u8; 208],
/// Next id to use when adding a token condition swap
pub next_token_conditional_swap_id: u64,
pub reserved: [u8; 200],
// dynamic
pub header_version: u8,
@ -142,7 +146,8 @@ impl MangoAccount {
buyback_fees_accrued_current: 0,
buyback_fees_accrued_previous: 0,
buyback_fees_expiry_timestamp: 0,
reserved: [0; 208],
next_token_conditional_swap_id: 0,
reserved: [0; 200],
header_version: DEFAULT_MANGO_ACCOUNT_VERSION,
padding3: Default::default(),
padding4: Default::default(),
@ -163,14 +168,22 @@ impl MangoAccount {
serum3_count: u8,
perp_count: u8,
perp_oo_count: u8,
token_conditional_swap_count: u8,
) -> Result<usize> {
require_gte!(16, token_count);
require_gte!(8, serum3_count);
require_gte!(8, perp_count);
require_gte!(64, perp_oo_count);
require_gte!(64, token_conditional_swap_count);
Ok(8 + size_of::<MangoAccountFixed>()
+ Self::dynamic_size(token_count, serum3_count, perp_count, perp_oo_count))
+ Self::dynamic_size(
token_count,
serum3_count,
perp_count,
perp_oo_count,
token_conditional_swap_count,
))
}
pub fn dynamic_token_vec_offset() -> usize {
@ -196,7 +209,7 @@ impl MangoAccount {
+ BORSH_VEC_PADDING_BYTES
}
pub fn dynamic_size(
pub fn dynamic_token_conditional_swap_vec_offset(
token_count: u8,
serum3_count: u8,
perp_count: u8,
@ -204,12 +217,28 @@ impl MangoAccount {
) -> usize {
Self::dynamic_perp_oo_vec_offset(token_count, serum3_count, perp_count)
+ (BORSH_VEC_SIZE_BYTES + size_of::<PerpOpenOrder>() * usize::from(perp_oo_count))
+ BORSH_VEC_PADDING_BYTES
}
pub fn dynamic_size(
token_count: u8,
serum3_count: u8,
perp_count: u8,
perp_oo_count: u8,
token_conditional_swap_count: u8,
) -> usize {
Self::dynamic_token_conditional_swap_vec_offset(
token_count,
serum3_count,
perp_count,
perp_oo_count,
) + (BORSH_VEC_SIZE_BYTES
+ size_of::<TokenConditionalSwap>() * usize::from(token_conditional_swap_count))
}
}
// Mango Account fixed part for easy zero copy deserialization
#[zero_copy]
#[derive(bytemuck::Pod, bytemuck::Zeroable)]
pub struct MangoAccountFixed {
pub group: Pubkey,
pub owner: Pubkey,
@ -227,9 +256,10 @@ pub struct MangoAccountFixed {
pub buyback_fees_accrued_current: u64,
pub buyback_fees_accrued_previous: u64,
pub buyback_fees_expiry_timestamp: u64,
pub reserved: [u8; 208],
pub next_token_conditional_swap_id: u64,
pub reserved: [u8; 200],
}
const_assert_eq!(size_of::<MangoAccountFixed>(), 32 * 4 + 8 + 7 * 8 + 208);
const_assert_eq!(size_of::<MangoAccountFixed>(), 32 * 4 + 8 + 8 * 8 + 200);
const_assert_eq!(size_of::<MangoAccountFixed>(), 400);
const_assert_eq!(size_of::<MangoAccountFixed>() % 8, 0);
@ -341,6 +371,7 @@ pub struct MangoAccountDynamicHeader {
pub serum3_count: u8,
pub perp_count: u8,
pub perp_oo_count: u8,
pub token_conditional_swap_count: u8,
}
impl DynamicHeader for MangoAccountDynamicHeader {
@ -377,11 +408,32 @@ impl DynamicHeader for MangoAccountDynamicHeader {
]))
.unwrap();
let token_conditional_swap_vec_offset =
MangoAccount::dynamic_token_conditional_swap_vec_offset(
token_count,
serum3_count,
perp_count,
perp_oo_count,
);
let token_conditional_swap_count = if dynamic_data.len()
> token_conditional_swap_vec_offset + BORSH_VEC_SIZE_BYTES
{
u8::try_from(BorshVecLength::from_le_bytes(*array_ref![
dynamic_data,
token_conditional_swap_vec_offset,
BORSH_VEC_SIZE_BYTES
]))
.unwrap()
} else {
0
};
Ok(Self {
token_count,
serum3_count,
perp_count,
perp_oo_count,
token_conditional_swap_count,
})
}
_ => err!(MangoError::NotImplementedError).context("unexpected header version number"),
@ -436,6 +488,16 @@ impl MangoAccountDynamicHeader {
+ raw_index * size_of::<PerpOpenOrder>()
}
fn token_conditional_swap_offset(&self, raw_index: usize) -> usize {
MangoAccount::dynamic_token_conditional_swap_vec_offset(
self.token_count,
self.serum3_count,
self.perp_count,
self.perp_oo_count,
) + BORSH_VEC_SIZE_BYTES
+ raw_index * size_of::<TokenConditionalSwap>()
}
pub fn token_count(&self) -> usize {
self.token_count.into()
}
@ -448,6 +510,9 @@ impl MangoAccountDynamicHeader {
pub fn perp_oo_count(&self) -> usize {
self.perp_oo_count.into()
}
pub fn token_conditional_swap_count(&self) -> usize {
self.token_conditional_swap_count.into()
}
}
/// Fully owned MangoAccount, useful for tests
@ -542,13 +607,18 @@ impl<
.map(|(p, _)| p)
}
pub fn token_position_by_raw_index(&self, raw_index: usize) -> &TokenPosition {
pub(crate) fn token_position_by_raw_index_unchecked(&self, raw_index: usize) -> &TokenPosition {
get_helper(self.dynamic(), self.header().token_offset(raw_index))
}
pub fn token_position_by_raw_index(&self, raw_index: usize) -> Result<&TokenPosition> {
require_gt!(self.header().token_count(), raw_index);
Ok(self.token_position_by_raw_index_unchecked(raw_index))
}
// get iter over all TokenPositions (including inactive)
pub fn all_token_positions(&self) -> impl Iterator<Item = &TokenPosition> + '_ {
(0..self.header().token_count()).map(|i| self.token_position_by_raw_index(i))
(0..self.header().token_count()).map(|i| self.token_position_by_raw_index_unchecked(i))
}
// get iter over all active TokenPositions
@ -562,12 +632,17 @@ impl<
.ok_or_else(|| error_msg!("serum3 orders for market index {} not found", market_index))
}
pub fn serum3_orders_by_raw_index(&self, raw_index: usize) -> &Serum3Orders {
pub(crate) fn serum3_orders_by_raw_index_unchecked(&self, raw_index: usize) -> &Serum3Orders {
get_helper(self.dynamic(), self.header().serum3_offset(raw_index))
}
pub fn serum3_orders_by_raw_index(&self, raw_index: usize) -> Result<&Serum3Orders> {
require_gt!(self.header().serum3_count(), raw_index);
Ok(self.serum3_orders_by_raw_index_unchecked(raw_index))
}
pub fn all_serum3_orders(&self) -> impl Iterator<Item = &Serum3Orders> + '_ {
(0..self.header().serum3_count()).map(|i| self.serum3_orders_by_raw_index(i))
(0..self.header().serum3_count()).map(|i| self.serum3_orders_by_raw_index_unchecked(i))
}
pub fn active_serum3_orders(&self) -> impl Iterator<Item = &Serum3Orders> + '_ {
@ -581,24 +656,34 @@ impl<
.ok_or_else(|| error!(MangoError::PerpPositionDoesNotExist))
}
pub fn perp_position_by_raw_index(&self, raw_index: usize) -> &PerpPosition {
pub(crate) fn perp_position_by_raw_index_unchecked(&self, raw_index: usize) -> &PerpPosition {
get_helper(self.dynamic(), self.header().perp_offset(raw_index))
}
pub fn perp_position_by_raw_index(&self, raw_index: usize) -> Result<&PerpPosition> {
require_gt!(self.header().perp_count(), raw_index);
Ok(self.perp_position_by_raw_index_unchecked(raw_index))
}
pub fn all_perp_positions(&self) -> impl Iterator<Item = &PerpPosition> {
(0..self.header().perp_count()).map(|i| self.perp_position_by_raw_index(i))
(0..self.header().perp_count()).map(|i| self.perp_position_by_raw_index_unchecked(i))
}
pub fn active_perp_positions(&self) -> impl Iterator<Item = &PerpPosition> {
self.all_perp_positions().filter(|p| p.is_active())
}
pub fn perp_order_by_raw_index(&self, raw_index: usize) -> &PerpOpenOrder {
pub(crate) fn perp_order_by_raw_index_unchecked(&self, raw_index: usize) -> &PerpOpenOrder {
get_helper(self.dynamic(), self.header().perp_oo_offset(raw_index))
}
pub fn perp_order_by_raw_index(&self, raw_index: usize) -> Result<&PerpOpenOrder> {
require_gt!(self.header().perp_oo_count(), raw_index);
Ok(self.perp_order_by_raw_index_unchecked(raw_index))
}
pub fn all_perp_orders(&self) -> impl Iterator<Item = &PerpOpenOrder> {
(0..self.header().perp_oo_count()).map(|i| self.perp_order_by_raw_index(i))
(0..self.header().perp_oo_count()).map(|i| self.perp_order_by_raw_index_unchecked(i))
}
pub fn perp_next_order_slot(&self) -> Result<usize> {
@ -629,6 +714,41 @@ impl<
self.fixed().being_liquidated()
}
fn token_conditional_swap_by_index_unchecked(&self, index: usize) -> &TokenConditionalSwap {
get_helper(
self.dynamic(),
self.header().token_conditional_swap_offset(index),
)
}
pub fn token_conditional_swap_by_index(&self, index: usize) -> Result<&TokenConditionalSwap> {
require_gt!(self.header().token_conditional_swap_count(), index);
Ok(self.token_conditional_swap_by_index_unchecked(index))
}
pub fn token_conditional_swap_by_id(&self, id: u64) -> Result<(usize, &TokenConditionalSwap)> {
let index = self
.all_token_conditional_swaps()
.position(|tcs| tcs.has_data() && tcs.id == id)
.ok_or_else(|| error_msg!("token conditional swap with id {} not found", id))?;
Ok((index, self.token_conditional_swap_by_index_unchecked(index)))
}
pub fn all_token_conditional_swaps(&self) -> impl Iterator<Item = &TokenConditionalSwap> {
(0..self.header().token_conditional_swap_count())
.map(|i| self.token_conditional_swap_by_index_unchecked(i))
}
pub fn active_token_conditional_swaps(&self) -> impl Iterator<Item = &TokenConditionalSwap> {
self.all_token_conditional_swaps().filter(|p| p.has_data())
}
pub fn token_conditional_swap_free_index(&self) -> Result<usize> {
self.all_token_conditional_swaps()
.position(|&v| !v.has_data())
.ok_or_else(|| error_msg!("no free token conditional swap index"))
}
pub fn borrow(&self) -> MangoAccountRef {
MangoAccountRef {
header: self.header(),
@ -743,7 +863,7 @@ impl<
raw_index: usize,
mango_account_pubkey: Pubkey,
) {
let mango_group = self.fixed.deref_or_borrow().group;
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 {
@ -756,6 +876,32 @@ impl<
self.token_position_mut_by_raw_index(raw_index).token_index = TokenIndex::MAX;
}
/// Decrements the in_use_count for the token position for the bank.
///
/// If it goes to 0, the position may be dusted (if between 0 and 1 native tokens)
/// and closed.
pub fn token_decrement_dust_deactivate(
&mut self,
bank: &mut crate::state::Bank,
now_ts: u64,
mango_account_pubkey: Pubkey,
) -> Result<()> {
let token_result = self.token_position_mut(bank.token_index);
if token_result.is_anchor_error_with_code(MangoError::TokenPositionDoesNotExist.into()) {
// Already deactivated is ok
return Ok(());
}
let (position, raw_index) = token_result?;
position.decrement_in_use();
let active = bank.dust_if_possible(position, now_ts)?;
if !active {
self.deactivate_token_position_and_log(raw_index, mango_account_pubkey);
}
Ok(())
}
// get mut Serum3Orders at raw_index
pub fn serum3_orders_mut_by_raw_index(&mut self, raw_index: usize) -> &mut Serum3Orders {
let offset = self.header().serum3_offset(raw_index);
@ -841,8 +987,8 @@ impl<
*perp_position = PerpPosition::default();
perp_position.market_index = perp_market_index;
let mut settle_token_position = self.ensure_token_position(settle_token_index)?.0;
settle_token_position.in_use_count += 1;
let settle_token_position = self.ensure_token_position(settle_token_index)?.0;
settle_token_position.increment_in_use();
}
}
if let Some(raw_index) = raw_index_opt {
@ -859,8 +1005,8 @@ impl<
) -> Result<()> {
self.perp_position_mut(perp_market_index)?.market_index = PerpMarketIndex::MAX;
let mut settle_token_position = self.token_position_mut(settle_token_index)?.0;
settle_token_position.in_use_count -= 1;
let settle_token_position = self.token_position_mut(settle_token_index)?.0;
settle_token_position.decrement_in_use();
Ok(())
}
@ -871,7 +1017,7 @@ impl<
settle_token_index: TokenIndex,
mango_account_pubkey: Pubkey,
) -> Result<()> {
let mango_group = self.fixed.deref_or_borrow().group;
let mango_group = self.fixed().group;
let perp_position = self.perp_position_mut(perp_market_index)?;
emit!(DeactivatePerpPositionLog {
@ -887,8 +1033,8 @@ impl<
perp_position.market_index = PerpMarketIndex::MAX;
let mut settle_token_position = self.token_position_mut(settle_token_index)?.0;
settle_token_position.in_use_count -= 1;
let settle_token_position = self.token_position_mut(settle_token_index)?.0;
settle_token_position.decrement_in_use();
Ok(())
}
@ -1008,6 +1154,22 @@ impl<
Ok(())
}
pub fn token_conditional_swap_mut_by_index(
&mut self,
index: usize,
) -> Result<&mut TokenConditionalSwap> {
let count: usize = self.header().token_conditional_swap_count.into();
require_gt!(count, index);
let offset = self.header().token_conditional_swap_offset(index);
Ok(get_helper_mut(self.dynamic_mut(), offset))
}
pub fn free_token_conditional_swap_mut(&mut self) -> Result<&mut TokenConditionalSwap> {
let index = self.token_conditional_swap_free_index()?;
let tcs = self.token_conditional_swap_mut_by_index(index)?;
Ok(tcs)
}
pub fn check_health_pre(&mut self, health_cache: &HealthCache) -> Result<I80F48> {
let pre_init_health = health_cache.health(HealthType::Init);
msg!("pre_init_health: {}", pre_init_health);
@ -1033,7 +1195,7 @@ impl<
&mut self,
health_cache: &HealthCache,
pre_init_health: I80F48,
) -> Result<()> {
) -> Result<I80F48> {
let post_init_health = health_cache.health(HealthType::Init);
msg!("post_init_health: {}", post_init_health);
@ -1057,7 +1219,7 @@ impl<
post_init_health >= 0 || health_does_not_decrease,
MangoError::HealthMustBePositiveOrIncrease
);
Ok(())
Ok(post_init_health)
}
pub fn check_liquidatable(&mut self, health_cache: &HealthCache) -> Result<CheckLiquidatable> {
@ -1084,54 +1246,41 @@ impl<
return Ok(CheckLiquidatable::Liquidatable);
}
fn write_borsh_vec_length(&mut self, offset: usize, count: u8) {
let dst: &mut [u8] = &mut self.dynamic_mut()[offset - BORSH_VEC_SIZE_BYTES..offset];
dst.copy_from_slice(&BorshVecLength::from(count).to_le_bytes());
}
// writes length of tokens vec at appropriate offset so that borsh can infer the vector length
// length used is that present in the header
fn write_token_length(&mut self) {
let tokens_offset = self.header().token_offset(0);
// msg!(
// "writing tokens length at {}",
// tokens_offset - size_of::<BorshVecLength>()
// );
let offset = self.header().token_offset(0);
let count = self.header().token_count;
let dst: &mut [u8] =
&mut self.dynamic_mut()[tokens_offset - BORSH_VEC_SIZE_BYTES..tokens_offset];
dst.copy_from_slice(&BorshVecLength::from(count).to_le_bytes());
self.write_borsh_vec_length(offset, count)
}
fn write_serum3_length(&mut self) {
let serum3_offset = self.header().serum3_offset(0);
// msg!(
// "writing serum3 length at {}",
// serum3_offset - size_of::<BorshVecLength>()
// );
let offset = self.header().serum3_offset(0);
let count = self.header().serum3_count;
let dst: &mut [u8] =
&mut self.dynamic_mut()[serum3_offset - BORSH_VEC_SIZE_BYTES..serum3_offset];
dst.copy_from_slice(&BorshVecLength::from(count).to_le_bytes());
self.write_borsh_vec_length(offset, count)
}
fn write_perp_length(&mut self) {
let perp_offset = self.header().perp_offset(0);
// msg!(
// "writing perp length at {}",
// perp_offset - size_of::<BorshVecLength>()
// );
let offset = self.header().perp_offset(0);
let count = self.header().perp_count;
let dst: &mut [u8] =
&mut self.dynamic_mut()[perp_offset - BORSH_VEC_SIZE_BYTES..perp_offset];
dst.copy_from_slice(&BorshVecLength::from(count).to_le_bytes());
self.write_borsh_vec_length(offset, count)
}
fn write_perp_oo_length(&mut self) {
let perp_oo_offset = self.header().perp_oo_offset(0);
// msg!(
// "writing perp length at {}",
// perp_offset - size_of::<BorshVecLength>()
// );
let offset = self.header().perp_oo_offset(0);
let count = self.header().perp_oo_count;
let dst: &mut [u8] =
&mut self.dynamic_mut()[perp_oo_offset - BORSH_VEC_SIZE_BYTES..perp_oo_offset];
dst.copy_from_slice(&BorshVecLength::from(count).to_le_bytes());
self.write_borsh_vec_length(offset, count)
}
fn write_token_conditional_swap_length(&mut self) {
let offset = self.header().token_conditional_swap_offset(0);
let count = self.header().token_conditional_swap_count;
self.write_borsh_vec_length(offset, count)
}
pub fn expand_dynamic_content(
@ -1140,11 +1289,16 @@ impl<
new_serum3_count: u8,
new_perp_count: u8,
new_perp_oo_count: u8,
new_token_conditional_swap_count: u8,
) -> Result<()> {
require_gte!(new_token_count, self.header().token_count);
require_gte!(new_serum3_count, self.header().serum3_count);
require_gte!(new_perp_count, self.header().perp_count);
require_gte!(new_perp_oo_count, self.header().perp_oo_count);
require_gte!(
new_token_conditional_swap_count,
self.header().token_conditional_swap_count
);
// create a temp copy to compute new starting offsets
let new_header = MangoAccountDynamicHeader {
@ -1152,12 +1306,28 @@ impl<
serum3_count: new_serum3_count,
perp_count: new_perp_count,
perp_oo_count: new_perp_oo_count,
token_conditional_swap_count: new_token_conditional_swap_count,
};
let old_header = self.header().clone();
let dynamic = self.dynamic_mut();
// expand dynamic components by first moving existing positions, and then setting new ones to defaults
// token conditional swaps
if old_header.token_conditional_swap_count() > 0 {
unsafe {
sol_memmove(
&mut dynamic[new_header.token_conditional_swap_offset(0)],
&mut dynamic[old_header.token_conditional_swap_offset(0)],
size_of::<TokenConditionalSwap>() * old_header.token_conditional_swap_count(),
);
}
}
for i in old_header.token_conditional_swap_count..new_token_conditional_swap_count {
*get_helper_mut(dynamic, new_header.token_conditional_swap_offset(i.into())) =
TokenConditionalSwap::default();
}
// perp oo
if old_header.perp_oo_count() > 0 {
unsafe {
@ -1223,6 +1393,7 @@ impl<
self.write_serum3_length();
self.write_perp_length();
self.write_perp_oo_length();
self.write_token_conditional_swap_length();
Ok(())
}
@ -1292,7 +1463,32 @@ mod tests {
use super::*;
fn make_test_account() -> MangoAccountValue {
let bytes = AnchorSerialize::try_to_vec(&MangoAccount::default_for_tests()).unwrap();
let account = MangoAccount::default_for_tests();
let mut bytes = AnchorSerialize::try_to_vec(&account).unwrap();
// The MangoAccount struct is missing some dynamic fields, add space for them
let tcs_length = 2;
let expected_space = MangoAccount::space(
account.tokens.len() as u8,
account.serum3.len() as u8,
account.perps.len() as u8,
account.perp_open_orders.len() as u8,
tcs_length,
)
.unwrap();
bytes.extend(vec![0u8; expected_space - bytes.len()]);
// Set the length of these dynamic parts
let (fixed, dynamic) = bytes.split_at_mut(size_of::<MangoAccountFixed>());
let mut header = MangoAccountDynamicHeader::from_bytes(dynamic).unwrap();
header.token_conditional_swap_count = tcs_length;
let mut account = MangoAccountRefMut {
header: &mut header,
fixed: bytemuck::from_bytes_mut(fixed),
dynamic,
};
account.write_token_conditional_swap_length();
MangoAccountValue::from_bytes(&bytes).unwrap()
}
@ -1318,14 +1514,21 @@ mod tests {
account.perps.resize(8, PerpPosition::default());
account.perps[0].market_index = 9;
account.perp_open_orders.resize(8, PerpOpenOrder::default());
account.next_token_conditional_swap_id = 13;
let account_bytes = AnchorSerialize::try_to_vec(&account).unwrap();
let account_bytes_without_tcs = AnchorSerialize::try_to_vec(&account).unwrap();
let account_bytes_with_tcs = {
let mut b = account_bytes_without_tcs.clone();
// tcs adds 4 bytes of padding and 4 bytes of Vec size
b.extend([0u8; 8]);
b
};
assert_eq!(
8 + account_bytes.len(),
MangoAccount::space(8, 8, 8, 8).unwrap()
8 + account_bytes_with_tcs.len(),
MangoAccount::space(8, 8, 8, 8, 0).unwrap()
);
let account2 = MangoAccountValue::from_bytes(&account_bytes).unwrap();
let account2 = MangoAccountValue::from_bytes(&account_bytes_without_tcs).unwrap();
assert_eq!(account.group, account2.fixed.group);
assert_eq!(account.owner, account2.fixed.owner);
assert_eq!(account.name, account2.fixed.name);
@ -1355,18 +1558,30 @@ mod tests {
account.buyback_fees_expiry_timestamp,
account2.fixed.buyback_fees_expiry_timestamp
);
assert_eq!(
account.next_token_conditional_swap_id,
account2.fixed.next_token_conditional_swap_id
);
assert_eq!(
account.tokens[0].token_index,
account2.token_position_by_raw_index(0).token_index
account2
.token_position_by_raw_index_unchecked(0)
.token_index
);
assert_eq!(
account.serum3[0].open_orders,
account2.serum3_orders_by_raw_index(0).open_orders
account2.serum3_orders_by_raw_index_unchecked(0).open_orders
);
assert_eq!(
account.perps[0].market_index,
account2.perp_position_by_raw_index(0).market_index
account2
.perp_position_by_raw_index_unchecked(0)
.market_index
);
assert_eq!(account2.all_token_conditional_swaps().count(), 0);
let account3 = MangoAccountValue::from_bytes(&account_bytes_with_tcs).unwrap();
assert_eq!(account3.all_token_conditional_swaps().count(), 0);
}
#[test]
@ -1376,7 +1591,7 @@ mod tests {
assert!(account.token_position_and_raw_index(2).is_err());
assert!(account.token_position_mut(3).is_err());
assert_eq!(
account.token_position_by_raw_index(0).token_index,
account.token_position_by_raw_index_unchecked(0).token_index,
TokenIndex::MAX
);
@ -1416,7 +1631,7 @@ mod tests {
assert_eq!(account.active_token_positions().count(), 3);
account.deactivate_token_position(0);
assert_eq!(
account.token_position_by_raw_index(0).token_index,
account.token_position_by_raw_index_unchecked(0).token_index,
TokenIndex::MAX
);
assert!(account.token_position(1).is_err());
@ -1444,7 +1659,7 @@ mod tests {
assert!(account.serum3_orders(1).is_err());
assert!(account.serum3_orders_mut(3).is_err());
assert_eq!(
account.serum3_orders_by_raw_index(0).market_index,
account.serum3_orders_by_raw_index_unchecked(0).market_index,
Serum3MarketIndex::MAX
);
@ -1456,11 +1671,14 @@ mod tests {
assert!(account.deactivate_serum3_orders(7).is_ok());
assert_eq!(
account.serum3_orders_by_raw_index(1).market_index,
account.serum3_orders_by_raw_index_unchecked(1).market_index,
Serum3MarketIndex::MAX
);
assert!(account.create_serum3_orders(8).is_ok());
assert_eq!(account.serum3_orders_by_raw_index(1).market_index, 8);
assert_eq!(
account.serum3_orders_by_raw_index_unchecked(1).market_index,
8
);
assert_eq!(account.active_serum3_orders().count(), 3);
assert!(account.deactivate_serum3_orders(1).is_ok());
@ -1481,7 +1699,7 @@ mod tests {
assert!(account.perp_position(1).is_err());
assert!(account.perp_position_mut(3).is_err());
assert_eq!(
account.perp_position_by_raw_index(0).market_index,
account.perp_position_by_raw_index_unchecked(0).market_index,
PerpMarketIndex::MAX
);
@ -1532,7 +1750,7 @@ mod tests {
assert_eq!(account.active_perp_positions().count(), 3);
assert!(account.deactivate_perp_position(1, 0).is_ok());
assert_eq!(
account.perp_position_by_raw_index(0).market_index,
account.perp_position_by_raw_index_unchecked(0).market_index,
PerpMarketIndex::MAX
);
assert!(account.perp_position(1).is_err());
@ -1596,4 +1814,54 @@ mod tests {
fixed.reduce_buyback_fees_accrued(12);
assert_eq!(fixed.buyback_fees_accrued(), 0);
}
#[test]
fn test_token_conditional_swap() {
let mut account = make_test_account();
assert_eq!(account.all_token_conditional_swaps().count(), 2);
assert_eq!(account.active_token_conditional_swaps().count(), 0);
assert_eq!(account.token_conditional_swap_free_index().unwrap(), 0);
let tcs = account.free_token_conditional_swap_mut().unwrap();
tcs.id = 123;
tcs.has_data = 1;
assert_eq!(account.all_token_conditional_swaps().count(), 2);
assert_eq!(account.active_token_conditional_swaps().count(), 1);
assert_eq!(account.token_conditional_swap_free_index().unwrap(), 1);
let tcs = account.free_token_conditional_swap_mut().unwrap();
tcs.id = 234;
tcs.has_data = 1;
assert_eq!(account.all_token_conditional_swaps().count(), 2);
assert_eq!(account.active_token_conditional_swaps().count(), 2);
let (index, tcs) = account.token_conditional_swap_by_id(123).unwrap();
assert_eq!(index, 0);
assert_eq!(tcs.id, 123);
let tcs = account.token_conditional_swap_by_index(0).unwrap();
assert_eq!(tcs.id, 123);
let (index, tcs) = account.token_conditional_swap_by_id(234).unwrap();
assert_eq!(index, 1);
assert_eq!(tcs.id, 234);
let tcs = account.token_conditional_swap_by_index(1).unwrap();
assert_eq!(tcs.id, 234);
assert!(account.free_token_conditional_swap_mut().is_err());
assert!(account.token_conditional_swap_free_index().is_err());
let tcs = account.token_conditional_swap_mut_by_index(0).unwrap();
tcs.has_data = 0;
assert_eq!(account.all_token_conditional_swaps().count(), 2);
assert_eq!(account.active_token_conditional_swaps().count(), 1);
assert_eq!(
account.active_token_conditional_swaps().next().unwrap().id,
234
);
assert!(account.token_conditional_swap_by_id(123).is_err());
assert_eq!(account.token_conditional_swap_free_index().unwrap(), 0);
let tcs = account.free_token_conditional_swap_mut().unwrap();
assert_eq!(tcs.id, 123); // old data
}
}

View File

@ -12,7 +12,7 @@ use crate::state::*;
pub const FREE_ORDER_SLOT: PerpMarketIndex = PerpMarketIndex::MAX;
#[zero_copy]
#[derive(AnchorDeserialize, AnchorSerialize, Derivative, bytemuck::Pod)]
#[derive(AnchorDeserialize, AnchorSerialize, Derivative)]
#[derivative(Debug)]
pub struct TokenPosition {
// TODO: Why did we have deposits and borrows as two different values
@ -27,10 +27,10 @@ pub struct TokenPosition {
pub token_index: TokenIndex,
/// incremented when a market requires this position to stay alive
pub in_use_count: u8,
pub in_use_count: u16,
#[derivative(Debug = "ignore")]
pub padding: [u8; 5],
pub padding: [u8; 4],
// bookkeeping variable for onchain interest calculation
// either deposit_index or borrow_index at last indexed_position change
@ -48,7 +48,7 @@ pub struct TokenPosition {
const_assert_eq!(
size_of::<TokenPosition>(),
16 + 2 + 1 + 5 + 16 + 8 + 8 + 128
16 + 2 + 2 + 4 + 16 + 8 + 8 + 128
);
const_assert_eq!(size_of::<TokenPosition>(), 184);
const_assert_eq!(size_of::<TokenPosition>() % 8, 0);
@ -99,10 +99,18 @@ impl TokenPosition {
pub fn is_in_use(&self) -> bool {
self.in_use_count > 0
}
pub fn increment_in_use(&mut self) {
self.in_use_count += 1; // panic on overflow
}
pub fn decrement_in_use(&mut self) {
self.in_use_count = self.in_use_count.saturating_sub(1);
}
}
#[zero_copy]
#[derive(AnchorSerialize, AnchorDeserialize, Derivative, bytemuck::Pod)]
#[derive(AnchorSerialize, AnchorDeserialize, Derivative)]
#[derivative(Debug)]
pub struct Serum3Orders {
pub open_orders: Pubkey,
@ -158,7 +166,7 @@ impl Default for Serum3Orders {
}
#[zero_copy]
#[derive(AnchorSerialize, AnchorDeserialize, Derivative, bytemuck::Pod)]
#[derive(AnchorSerialize, AnchorDeserialize, Derivative)]
#[derivative(Debug)]
pub struct PerpPosition {
pub market_index: PerpMarketIndex,
@ -789,7 +797,7 @@ impl PerpPosition {
}
#[zero_copy]
#[derive(AnchorSerialize, AnchorDeserialize, Debug, bytemuck::Pod)]
#[derive(AnchorSerialize, AnchorDeserialize, Debug)]
pub struct PerpOpenOrder {
pub side_and_tree: u8, // SideAndOrderTree -- enums aren't POD
pub padding1: [u8; 1],

View File

@ -10,6 +10,7 @@ pub use orderbook::*;
pub use perp_market::*;
pub use serum3_market::*;
pub use stable_price::*;
pub use token_conditional_swap::*;
mod bank;
mod dynamic_account;
@ -23,3 +24,4 @@ mod orderbook;
mod perp_market;
mod serum3_market;
mod stable_price;
mod token_conditional_swap;

View File

@ -57,7 +57,7 @@ pub mod switchboard_v2_mainnet_oracle {
}
#[zero_copy]
#[derive(AnchorDeserialize, AnchorSerialize, Debug, bytemuck::Pod)]
#[derive(AnchorDeserialize, AnchorSerialize, Debug)]
pub struct OracleConfig {
pub conf_filter: I80F48,
pub max_staleness_slots: i64,
@ -134,6 +134,56 @@ pub fn determine_oracle_type(acc_info: &impl KeyedAccountReader) -> Result<Oracl
Err(MangoError::UnknownOracleType.into())
}
/// A modified version of PriceAccount::get_price_no_older_than() which
/// - doesn't need a Clock instance
/// - has the staleness check be optional (negative max_staleness)
/// - returns the publish slot of the price
fn pyth_get_price(
pubkey: &Pubkey,
account: &pyth_sdk_solana::state::PriceAccount,
now_slot: u64,
max_staleness: i64,
) -> Result<(pyth_sdk_solana::Price, u64)> {
use pyth_sdk_solana::*;
if account.agg.status == state::PriceStatus::Trading
&& (max_staleness < 0
|| account.agg.pub_slot.saturating_add(max_staleness as u64) >= now_slot)
{
return Ok((
Price {
conf: account.agg.conf,
expo: account.expo,
price: account.agg.price,
publish_time: account.timestamp,
},
account.agg.pub_slot,
));
}
if max_staleness < 0 || account.prev_slot.saturating_add(max_staleness as u64) >= now_slot {
return Ok((
Price {
conf: account.prev_conf,
expo: account.expo,
price: account.prev_price,
publish_time: account.prev_timestamp,
},
account.prev_slot,
));
}
msg!(
"Pyth price too stale; pubkey {} prev price: {} prev slot: {} agg pub slot: {} agg status: {:?}",
pubkey,
account.prev_price,
account.prev_slot,
account.agg.pub_slot,
account.agg.status,
);
Err(MangoError::OracleStale.into())
}
/// Returns the price of one native base token, in native quote tokens
///
/// Example: The for SOL at 40 USDC/SOL it would return 0.04 (the unit is USDC-native/SOL-native)
@ -162,7 +212,12 @@ pub fn oracle_price_and_state(
),
OracleType::Pyth => {
let price_account = pyth_sdk_solana::state::load_price_account(data).unwrap();
let price_data = price_account.to_price();
let (price_data, pub_slot) = pyth_get_price(
acc_info.key(),
price_account,
staleness_slot,
config.max_staleness_slots,
)?;
let price = I80F48::from_num(price_data.price);
// Filter out bad prices
@ -180,30 +235,12 @@ pub fn oracle_price_and_state(
return Err(MangoError::OracleConfidence.into());
}
// The last_slot is when the price was actually updated
let last_slot = price_account.last_slot;
if config.max_staleness_slots >= 0
&& price_account
.last_slot
.saturating_add(config.max_staleness_slots as u64)
< staleness_slot
{
msg!(
"Pyth price too stale; pubkey {} price: {} last slot: {}",
acc_info.key(),
price.to_num::<f64>(),
last_slot,
);
return Err(MangoError::OracleStale.into());
}
let decimals = (price_account.expo as i8) + QUOTE_DECIMALS - (base_decimals as i8);
let decimal_adj = power_of_ten(decimals);
(
price * decimal_adj,
OracleState {
last_update_slot: last_slot,
last_update_slot: pub_slot,
confidence: I80F48::from_num(price_data.conf),
oracle_type: OracleType::Pyth,
},

View File

@ -358,8 +358,9 @@ impl<'a> Orderbook<'a> {
mut limit: u8,
side_to_cancel_option: Option<Side>,
) -> Result<()> {
// Can't use mango_account.all_perp_orders() for borrow checking reasons.
for i in 0..mango_account.header.perp_oo_count() {
let oo = mango_account.perp_order_by_raw_index(i);
let oo = mango_account.perp_order_by_raw_index(i)?;
if !oo.is_active_for_market(perp_market.perp_market_index) {
continue;
}

View File

@ -141,7 +141,7 @@ mod tests {
u8::MAX,
)
.unwrap();
account.perp_order_by_raw_index(0).id
account.perp_order_by_raw_index_unchecked(0).id
};
// insert bids until book side is full
@ -292,7 +292,8 @@ mod tests {
)
.unwrap();
let order =
order_tree_leaf_by_key(&book.bids, maker.perp_order_by_raw_index(0).id).unwrap();
order_tree_leaf_by_key(&book.bids, maker.perp_order_by_raw_index_unchecked(0).id)
.unwrap();
assert_eq!(order.client_order_id, 42);
assert_eq!(order.quantity, bid_quantity);
assert_eq!(
@ -312,16 +313,34 @@ mod tests {
));
assert!(order_tree_contains_price(&book.bids, price_lots as u64));
assert_eq!(
maker.perp_position_by_raw_index(0).bids_base_lots,
maker.perp_position_by_raw_index_unchecked(0).bids_base_lots,
bid_quantity
);
assert_eq!(maker.perp_position_by_raw_index(0).asks_base_lots, 0);
assert_eq!(maker.perp_position_by_raw_index(0).taker_base_lots, 0);
assert_eq!(maker.perp_position_by_raw_index(0).taker_quote_lots, 0);
assert_eq!(maker.perp_position_by_raw_index(0).base_position_lots(), 0);
assert_eq!(
maker.perp_position_by_raw_index_unchecked(0).asks_base_lots,
0
);
assert_eq!(
maker
.perp_position_by_raw_index(0)
.perp_position_by_raw_index_unchecked(0)
.taker_base_lots,
0
);
assert_eq!(
maker
.perp_position_by_raw_index_unchecked(0)
.taker_quote_lots,
0
);
assert_eq!(
maker
.perp_position_by_raw_index_unchecked(0)
.base_position_lots(),
0
);
assert_eq!(
maker
.perp_position_by_raw_index_unchecked(0)
.quote_position_native()
.to_num::<u32>(),
0
@ -356,7 +375,8 @@ mod tests {
// the remainder of the maker order is still on the book
// (the maker account is unchanged: it was not even passed in)
let order =
order_tree_leaf_by_key(&book.bids, maker.perp_order_by_raw_index(0).id).unwrap();
order_tree_leaf_by_key(&book.bids, maker.perp_order_by_raw_index_unchecked(0).id)
.unwrap();
assert_eq!(fixed_price_lots(order.price_data()), price_lots);
assert_eq!(order.quantity, bid_quantity - match_quantity);
@ -365,20 +385,40 @@ mod tests {
assert_eq!(market.fees_accrued, match_quote * (maker_fee + taker_fee));
// the taker account is updated
assert_eq!(taker.perp_order_by_raw_index(0).market, FREE_ORDER_SLOT);
assert_eq!(taker.perp_position_by_raw_index(0).bids_base_lots, 0);
assert_eq!(taker.perp_position_by_raw_index(0).asks_base_lots, 0);
assert_eq!(
taker.perp_position_by_raw_index(0).taker_base_lots,
taker.perp_order_by_raw_index_unchecked(0).market,
FREE_ORDER_SLOT
);
assert_eq!(
taker.perp_position_by_raw_index_unchecked(0).bids_base_lots,
0
);
assert_eq!(
taker.perp_position_by_raw_index_unchecked(0).asks_base_lots,
0
);
assert_eq!(
taker
.perp_position_by_raw_index_unchecked(0)
.taker_base_lots,
-match_quantity
);
assert_eq!(
taker.perp_position_by_raw_index(0).taker_quote_lots,
taker
.perp_position_by_raw_index_unchecked(0)
.taker_quote_lots,
match_quantity * price_lots
);
assert_eq!(taker.perp_position_by_raw_index(0).base_position_lots(), 0);
assert_eq!(
taker.perp_position_by_raw_index(0).quote_position_native(),
taker
.perp_position_by_raw_index_unchecked(0)
.base_position_lots(),
0
);
assert_eq!(
taker
.perp_position_by_raw_index_unchecked(0)
.quote_position_native(),
-match_quote * taker_fee
);
@ -404,33 +444,70 @@ mod tests {
.unwrap();
assert_eq!(market.open_interest, 2 * match_quantity);
assert_eq!(maker.perp_order_by_raw_index(0).market, 0);
assert_eq!(maker.perp_order_by_raw_index_unchecked(0).market, 0);
assert_eq!(
maker.perp_position_by_raw_index(0).bids_base_lots,
maker.perp_position_by_raw_index_unchecked(0).bids_base_lots,
bid_quantity - match_quantity
);
assert_eq!(maker.perp_position_by_raw_index(0).asks_base_lots, 0);
assert_eq!(maker.perp_position_by_raw_index(0).taker_base_lots, 0);
assert_eq!(maker.perp_position_by_raw_index(0).taker_quote_lots, 0);
assert_eq!(
maker.perp_position_by_raw_index(0).base_position_lots(),
maker.perp_position_by_raw_index_unchecked(0).asks_base_lots,
0
);
assert_eq!(
maker
.perp_position_by_raw_index_unchecked(0)
.taker_base_lots,
0
);
assert_eq!(
maker
.perp_position_by_raw_index_unchecked(0)
.taker_quote_lots,
0
);
assert_eq!(
maker
.perp_position_by_raw_index_unchecked(0)
.base_position_lots(),
match_quantity
);
assert_eq!(
maker.perp_position_by_raw_index(0).quote_position_native(),
maker
.perp_position_by_raw_index_unchecked(0)
.quote_position_native(),
-match_quote - match_quote * maker_fee
);
assert_eq!(taker.perp_position_by_raw_index(0).bids_base_lots, 0);
assert_eq!(taker.perp_position_by_raw_index(0).asks_base_lots, 0);
assert_eq!(taker.perp_position_by_raw_index(0).taker_base_lots, 0);
assert_eq!(taker.perp_position_by_raw_index(0).taker_quote_lots, 0);
assert_eq!(
taker.perp_position_by_raw_index(0).base_position_lots(),
taker.perp_position_by_raw_index_unchecked(0).bids_base_lots,
0
);
assert_eq!(
taker.perp_position_by_raw_index_unchecked(0).asks_base_lots,
0
);
assert_eq!(
taker
.perp_position_by_raw_index_unchecked(0)
.taker_base_lots,
0
);
assert_eq!(
taker
.perp_position_by_raw_index_unchecked(0)
.taker_quote_lots,
0
);
assert_eq!(
taker
.perp_position_by_raw_index_unchecked(0)
.base_position_lots(),
-match_quantity
);
assert_eq!(
taker.perp_position_by_raw_index(0).quote_position_native(),
taker
.perp_position_by_raw_index_unchecked(0)
.quote_position_native(),
match_quote - match_quote * taker_fee
);
}
@ -604,7 +681,7 @@ mod tests {
u8::MAX,
)
.unwrap();
account.perp_order_by_raw_index(0).id
account.perp_order_by_raw_index_unchecked(0).id
};
// Setup

View File

@ -36,7 +36,6 @@ impl OrderTreeType {
}
#[zero_copy]
#[derive(bytemuck::Pod, bytemuck::Zeroable)]
pub struct OrderTreeRoot {
pub maybe_node: NodeHandle,
pub leaf_count: u32,
@ -58,7 +57,6 @@ impl OrderTreeRoot {
///
/// The key encodes the price in the top 64 bits.
#[zero_copy]
#[derive(bytemuck::Pod, bytemuck::Zeroable)]
pub struct OrderTreeNodes {
pub order_tree_type: u8, // OrderTreeType, but that's not POD
pub padding: [u8; 3],

View File

@ -123,7 +123,6 @@ impl<'a> Iterator for EventQueueIterator<'a> {
}
#[zero_copy]
#[derive(bytemuck::Pod, bytemuck::Zeroable)]
pub struct EventQueueHeader {
head: u32,
count: u32,
@ -157,7 +156,7 @@ impl QueueHeader for EventQueueHeader {
const EVENT_SIZE: usize = 208;
#[zero_copy]
#[derive(Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[derive(Debug)]
pub struct AnyEvent {
pub event_type: u8,
pub padding: [u8; 207],

View File

@ -83,7 +83,10 @@ pub struct PerpMarket {
pub maint_base_liab_weight: I80F48,
pub init_base_liab_weight: I80F48,
/// Number of base lot pairs currently active in the market. Always >= 0.
/// Number of base lots currently active in the market. Always >= 0.
///
/// Since this counts positive base lots and negative base lots, the more relevant
/// number of open base lot pairs is half this value.
pub open_interest: i64,
/// Total number of orders seen
@ -121,8 +124,10 @@ pub struct PerpMarket {
pub taker_fee: I80F48,
/// Fees accrued in native quote currency
/// these are increased when new fees are paid and decreased when perp_settle_fees is called
pub fees_accrued: I80F48,
/// Fees settled in native quote currency
/// these are increased when perp_settle_fees is called, and never decreased
pub fees_settled: I80F48,
/// Fee (in quote native) to charge for ioc orders
@ -167,7 +172,11 @@ pub struct PerpMarket {
pub positive_pnl_liquidation_fee: I80F48,
pub reserved: [u8; 1888],
// Do separate bookkeping for how many tokens were withdrawn
// This ensures that fees_settled is strictly increasing for stats gathering purposes
pub fees_withdrawn: u64,
pub reserved: [u8; 1880],
}
const_assert_eq!(
@ -203,7 +212,8 @@ const_assert_eq!(
+ 1
+ 7
+ 3 * 16
+ 1888
+ 8
+ 1880
);
const_assert_eq!(size_of::<PerpMarket>(), 2808);
const_assert_eq!(size_of::<PerpMarket>() % 8, 0);
@ -492,7 +502,8 @@ impl PerpMarket {
maint_overall_asset_weight: I80F48::ONE,
init_overall_asset_weight: I80F48::ONE,
positive_pnl_liquidation_fee: I80F48::ZERO,
reserved: [0; 1888],
fees_withdrawn: 0,
reserved: [0; 1880],
}
}
}

View File

@ -15,7 +15,7 @@ use std::mem::size_of;
/// price over every `delay_interval_seconds` (assume 1h) and then applying the
/// `delay_growth_limit` between intervals.
#[zero_copy]
#[derive(Derivative, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[derive(Derivative, Debug)]
pub struct StablePriceModel {
/// Current stable price to use in health
pub stable_price: f64,
@ -49,15 +49,18 @@ pub struct StablePriceModel {
/// The delay_interval_index that update() was last called on.
pub last_delay_interval_index: u8,
/// If set to 1, the stable price will reset on the next non-zero price it sees.
pub reset_on_nonzero_price: u8,
#[derivative(Debug = "ignore")]
pub padding: [u8; 7],
pub padding: [u8; 6],
#[derivative(Debug = "ignore")]
pub reserved: [u8; 48],
}
const_assert_eq!(
size_of::<StablePriceModel>(),
8 + 8 + 8 * 24 + 8 + 4 + 4 + 4 + 4 + 1 + 7 + 48
8 + 8 + 8 * 24 + 8 + 4 + 4 + 4 + 4 + 1 * 2 + 6 + 48
);
const_assert_eq!(size_of::<StablePriceModel>(), 288);
const_assert_eq!(size_of::<StablePriceModel>() % 8, 0);
@ -74,6 +77,7 @@ impl Default for StablePriceModel {
delay_growth_limit: 0.06, // 6% per hour, 400% per day
stable_growth_limit: 0.0003, // 0.03% per second, 293% in 1h if updated every 10s, 281% in 1h if updated every 5min
last_delay_interval_index: 0,
reset_on_nonzero_price: 0,
padding: Default::default(),
reserved: [0; 48],
}
@ -87,6 +91,7 @@ impl StablePriceModel {
self.delay_accumulator_price = 0.0;
self.delay_accumulator_time = 0;
self.last_update_timestamp = now_ts;
self.reset_on_nonzero_price = if oracle_price > 0.0 { 0 } else { 1 };
}
pub fn delay_interval_index(&self, timestamp: u64) -> u8 {
@ -103,6 +108,11 @@ impl StablePriceModel {
}
pub fn update(&mut self, now_ts: u64, oracle_price: f64) {
// If a reset is requested (maybe there never was a non-zero price), jump to the current value
if self.reset_on_nonzero_price == 1 && oracle_price > 0.0 {
self.reset_to_price(oracle_price, now_ts);
}
let dt = now_ts.saturating_sub(self.last_update_timestamp);
// Hardcoded. Requiring a minimum time between updates reduces the possible difference
// between frequent updates and infrequent ones.

View File

@ -0,0 +1,166 @@
use anchor_lang::prelude::*;
use derivative::Derivative;
use fixed::types::I80F48;
use static_assertions::const_assert_eq;
use std::mem::size_of;
use crate::state::*;
#[zero_copy]
#[derive(AnchorDeserialize, AnchorSerialize, Derivative)]
#[derivative(Debug)]
pub struct TokenConditionalSwap {
pub id: u64,
/// maximum amount of native tokens to buy or sell
pub max_buy: u64,
pub max_sell: u64,
/// how many native tokens were already bought/sold
pub bought: u64,
pub sold: u64,
/// timestamp until which the conditional swap is valid
pub expiry_timestamp: u64,
/// The price must exceed this threshold to allow execution.
///
/// This threshold is compared to the "sell_token per buy_token" oracle price
/// (which can be computed by dividing the buy token oracle price by the
/// sell token oracle price). If that price is >= lower_limit and <= upper_limit
/// the tcs may be executable.
///
/// Example: Stop loss to get out of a SOL long: The user bought SOL at 20 USDC/SOL
/// and wants to stop loss at 18 USDC/SOL. They'd set buy_token=USDC, sell_token=SOL
/// so the reference price is in SOL/USDC units. Set price_lower_limit=toNative(1/18)
/// and price_upper_limit=toNative(1/10). Also set allow_borrows=false.
///
/// Example: Want to buy SOL with USDC if the price falls below 22 USDC/SOL.
/// buy_token=SOL, sell_token=USDC, reference price is in USDC/SOL units. Set
/// price_upper_limit=toNative(22), price_lower_limit=0.
pub price_lower_limit: f64,
/// Parallel to price_lower_limit, but an upper limit.
pub price_upper_limit: f64,
/// The premium to pay over oracle price to incentivize execution.
pub price_premium_fraction: f64,
/// The taker receives only premium_price * (1 - taker_fee_fraction)
pub taker_fee_fraction: f32,
/// The maker has to pay premium_price * (1 + maker_fee_fraction)
pub maker_fee_fraction: f32,
/// indexes of tokens for the swap
pub buy_token_index: TokenIndex,
pub sell_token_index: TokenIndex,
pub has_data: u8,
/// may token purchases create deposits? (often users just want to get out of a borrow)
pub allow_creating_deposits: u8,
/// may token selling create borrows? (often users just want to get out of a long)
pub allow_creating_borrows: u8,
#[derivative(Debug = "ignore")]
pub reserved: [u8; 113],
}
const_assert_eq!(
size_of::<TokenConditionalSwap>(),
8 * 6 + 8 * 3 + 2 * 4 + 2 * 2 + 1 * 3 + 113
);
const_assert_eq!(size_of::<TokenConditionalSwap>(), 200);
const_assert_eq!(size_of::<TokenConditionalSwap>() % 8, 0);
impl Default for TokenConditionalSwap {
fn default() -> Self {
Self {
id: 0,
max_buy: 0,
max_sell: 0,
bought: 0,
sold: 0,
expiry_timestamp: u64::MAX,
price_lower_limit: 0.0,
price_upper_limit: 0.0,
price_premium_fraction: 0.0,
taker_fee_fraction: 0.0,
maker_fee_fraction: 0.0,
buy_token_index: TokenIndex::MAX,
sell_token_index: TokenIndex::MAX,
has_data: 0,
allow_creating_borrows: 0,
allow_creating_deposits: 0,
reserved: [0; 113],
}
}
}
impl TokenConditionalSwap {
/// Whether the entry is in use
///
/// Note that it's possible for an entry to be in use but be expired
pub fn has_data(&self) -> bool {
self.has_data == 1
}
pub fn set_has_data(&mut self, has_data: bool) {
self.has_data = u8::from(has_data);
}
pub fn is_expired(&self, now_ts: u64) -> bool {
now_ts >= self.expiry_timestamp
}
pub fn allow_creating_deposits(&self) -> bool {
self.allow_creating_deposits == 1
}
pub fn allow_creating_borrows(&self) -> bool {
self.allow_creating_borrows == 1
}
pub fn remaining_buy(&self) -> u64 {
self.max_buy - self.bought
}
pub fn remaining_sell(&self) -> u64 {
self.max_sell - self.sold
}
/// Base price adjusted for the premium
///
/// Base price is the amount of sell_token to pay for one buy_token.
pub fn premium_price(&self, base_price: f64) -> f64 {
base_price * (1.0 + self.price_premium_fraction)
}
/// Premium price adjusted for the maker fee
pub fn maker_price(&self, premium_price: f64) -> f64 {
premium_price * (1.0 + self.maker_fee_fraction as f64)
}
/// Premium price adjusted for the taker fee
pub fn taker_price(&self, premium_price: f64) -> f64 {
premium_price * (1.0 - self.taker_fee_fraction as f64)
}
pub fn maker_fee(&self, base_sell_amount: I80F48) -> u64 {
(base_sell_amount * I80F48::from_num(self.maker_fee_fraction))
.floor()
.to_num()
}
pub fn taker_fee(&self, base_sell_amount: I80F48) -> u64 {
(base_sell_amount * I80F48::from_num(self.taker_fee_fraction))
.floor()
.to_num()
}
pub fn price_in_range(&self, price: f64) -> bool {
price >= self.price_lower_limit && price <= self.price_upper_limit
}
}

View File

@ -34,4 +34,5 @@ mod test_perp_settle_fees;
mod test_position_lifetime;
mod test_reduce_only;
mod test_serum;
mod test_token_conditional_swap;
mod test_token_update_index_and_rate;

View File

@ -68,6 +68,7 @@ async fn test_basic() -> Result<(), TransportError> {
group,
owner,
payer,
..Default::default()
},
)
.await

View File

@ -2,7 +2,9 @@ use super::*;
#[tokio::test]
async fn test_ix_gate_set() -> Result<(), TransportError> {
let context = TestContext::new().await;
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(200_000); // lots of logging
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();

View File

@ -135,16 +135,7 @@ impl SerumOrderPlacer {
}
async fn settle(&self) {
send_tx(
&self.solana,
Serum3SettleFundsInstruction {
account: self.account,
owner: self.owner,
serum_market: self.serum_market,
},
)
.await
.unwrap();
self.settle_v2(true).await
}
async fn settle_v2(&self, fees_to_dao: bool) {
@ -298,10 +289,28 @@ async fn test_serum_basics() -> Result<(), TransportError> {
assert_eq!(native1, 900);
let account_data = get_mango_account(solana, account).await;
assert_eq!(account_data.token_position_by_raw_index(0).in_use_count, 1);
assert_eq!(account_data.token_position_by_raw_index(1).in_use_count, 1);
assert_eq!(account_data.token_position_by_raw_index(2).in_use_count, 0);
let serum_orders = account_data.serum3_orders_by_raw_index(0);
assert_eq!(
account_data
.token_position_by_raw_index(0)
.unwrap()
.in_use_count,
1
);
assert_eq!(
account_data
.token_position_by_raw_index(1)
.unwrap()
.in_use_count,
1
);
assert_eq!(
account_data
.token_position_by_raw_index(2)
.unwrap()
.in_use_count,
0
);
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);
@ -342,8 +351,20 @@ async fn test_serum_basics() -> Result<(), TransportError> {
.unwrap();
let account_data = get_mango_account(solana, account).await;
assert_eq!(account_data.token_position_by_raw_index(0).in_use_count, 0);
assert_eq!(account_data.token_position_by_raw_index(1).in_use_count, 0);
assert_eq!(
account_data
.token_position_by_raw_index(0)
.unwrap()
.in_use_count,
0
);
assert_eq!(
account_data
.token_position_by_raw_index(1)
.unwrap()
.in_use_count,
0
);
// deregister serum3 market
send_tx(
@ -540,7 +561,7 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
let account_data = solana.get_account::<MangoAccount>(account).await;
assert_eq!(
account_data.buyback_fees_accrued_current,
0 // the v1 function doesn't accumulate buyback fees
serum_maker_rebate(fill_amount) as u64
);
assert_eq!(

View File

@ -0,0 +1,292 @@
use super::*;
#[tokio::test]
async fn test_token_conditional_swap() -> 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, register a token (mint0)
//
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];
let deposit_amount = 1000;
let account = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
let liqor = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
send_tx(
solana,
GroupEdit {
group,
admin,
options: mango_v4::instruction::GroupEdit {
token_conditional_swap_taker_fee_fraction_opt: Some(0.05),
token_conditional_swap_maker_fee_fraction_opt: Some(0.1),
..group_edit_instruction_default()
},
},
)
.await
.unwrap();
//
// TEST: Trying to add a tcs on an account without space will fail
//
let tx_result = send_tx(
solana,
TokenConditionalSwapCreateInstruction {
account,
owner,
buy_mint: quote_token.mint.pubkey,
sell_mint: base_token.mint.pubkey,
max_buy: 1000,
max_sell: 1000,
price_lower_limit: 1.0,
price_upper_limit: 10.0,
price_premium_fraction: 0.01,
allow_creating_deposits: true,
allow_creating_borrows: true,
},
)
.await;
assert!(tx_result.is_err());
//
// TEST: Extending an account to have space for tcs works
//
send_tx(
solana,
AccountExpandInstruction {
account_num: 0,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
token_conditional_swap_count: 2,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
let account_data = get_mango_account(solana, account).await;
assert_eq!(account_data.header.token_conditional_swap_count, 2);
//
// TEST: Can create tsls until all slots are filled
//
let tcs_ix = TokenConditionalSwapCreateInstruction {
account,
owner,
buy_mint: quote_token.mint.pubkey,
sell_mint: base_token.mint.pubkey,
max_buy: 100,
max_sell: 100,
price_lower_limit: 0.9,
price_upper_limit: 10.0,
price_premium_fraction: 0.1,
allow_creating_deposits: true,
allow_creating_borrows: true,
};
send_tx(
solana,
TokenConditionalSwapCreateInstruction {
max_buy: 101,
..tcs_ix
},
)
.await
.unwrap();
send_tx(
solana,
TokenConditionalSwapCreateInstruction {
max_buy: 102,
price_lower_limit: 1.1,
..tcs_ix
},
)
.await
.unwrap();
let tx_result = send_tx(solana, tcs_ix.clone()).await;
assert!(tx_result.is_err());
let account_data = get_mango_account(solana, account).await;
assert_eq!(
account_data
.token_conditional_swap_by_index(0)
.unwrap()
.max_buy,
101
);
assert_eq!(
account_data
.token_conditional_swap_by_index(1)
.unwrap()
.max_buy,
102
);
//
// TEST: Can cancel, and then readd a new one
//
send_tx(
solana,
TokenConditionalSwapCancelInstruction {
account,
owner,
index: 0,
id: 0,
},
)
.await
.unwrap();
send_tx(
solana,
TokenConditionalSwapCreateInstruction {
max_buy: 103,
..tcs_ix
},
)
.await
.unwrap();
let tx_result = send_tx(solana, tcs_ix.clone()).await;
assert!(tx_result.is_err());
let account_data = get_mango_account(solana, account).await;
assert_eq!(
account_data
.token_conditional_swap_by_index(0)
.unwrap()
.max_buy,
103
);
assert_eq!(
account_data
.token_conditional_swap_by_index(1)
.unwrap()
.max_buy,
102
);
//
// TEST: can't trigger if price threshold not reached
//
let tx_result = send_tx(
solana,
TokenConditionalSwapTriggerInstruction {
liqee: account,
liqor,
liqor_owner: owner,
index: 1,
max_buy_token_to_liqee: 50,
max_sell_token_to_liqor: 50,
},
)
.await;
assert!(tx_result.is_err());
//
// TEST: trigger partially
//
send_tx(
solana,
TokenConditionalSwapTriggerInstruction {
liqee: account,
liqor,
liqor_owner: owner,
index: 0,
max_buy_token_to_liqee: 50,
max_sell_token_to_liqor: 50,
},
)
.await
.unwrap();
let liqee_quote = account_position_f64(solana, account, quote_token.bank).await;
let liqee_base = account_position_f64(solana, account, base_token.bank).await;
assert!(assert_equal_f_f(
liqee_quote,
1000.0 + 42.0, // roughly 50 / (1.1 * 1.1)
0.01
));
assert!(assert_equal_f_f(liqee_base, 1000.0 - 50.0, 0.01));
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
let liqor_base = account_position_f64(solana, liqor, base_token.bank).await;
assert!(assert_equal_f_f(liqor_quote, 1000.0 - 42.0, 0.01));
assert!(assert_equal_f_f(liqor_base, 1000.0 + 44.0, 0.01)); // roughly 42*1.1*0.95
//
// TEST: trigger fully
//
send_tx(
solana,
TokenConditionalSwapTriggerInstruction {
liqee: account,
liqor,
liqor_owner: owner,
index: 0,
max_buy_token_to_liqee: 5000,
max_sell_token_to_liqor: 5000,
},
)
.await
.unwrap();
let liqee_quote = account_position_f64(solana, account, quote_token.bank).await;
let liqee_base = account_position_f64(solana, account, base_token.bank).await;
assert!(assert_equal_f_f(liqee_quote, 1000.0 + 84.0, 0.01));
assert!(assert_equal_f_f(liqee_base, 1000.0 - 100.0, 0.01));
let liqor_quote = account_position_f64(solana, liqor, quote_token.bank).await;
let liqor_base = account_position_f64(solana, liqor, base_token.bank).await;
assert!(assert_equal_f_f(liqor_quote, 1000.0 - 84.0, 0.01));
assert!(assert_equal_f_f(liqor_base, 1000.0 + 88.0, 0.01));
let account_data = get_mango_account(solana, account).await;
assert!(!account_data
.token_conditional_swap_by_index(0)
.unwrap()
.has_data());
Ok(())
}

View File

@ -1530,6 +1530,8 @@ pub fn group_edit_instruction_default() -> mango_v4::instruction::GroupEdit {
buyback_fees_swap_mango_account_opt: None,
mngo_token_index_opt: None,
buyback_fees_expiry_interval_opt: None,
token_conditional_swap_taker_fee_fraction_opt: None,
token_conditional_swap_maker_fee_fraction_opt: None,
}
}
@ -1728,6 +1730,7 @@ impl ClientInstruction for AccountCreateInstruction {
}
}
#[derive(Default)]
pub struct AccountExpandInstruction {
pub account_num: u32,
pub group: Pubkey,
@ -1737,21 +1740,23 @@ pub struct AccountExpandInstruction {
pub serum3_count: u8,
pub perp_count: u8,
pub perp_oo_count: u8,
pub token_conditional_swap_count: u8,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for AccountExpandInstruction {
type Accounts = mango_v4::accounts::AccountExpand;
type Instruction = mango_v4::instruction::AccountExpand;
type Instruction = mango_v4::instruction::AccountExpandV2;
async fn to_instruction(
&self,
_account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = mango_v4::instruction::AccountExpand {
let instruction = Self::Instruction {
token_count: self.token_count,
serum3_count: self.serum3_count,
perp_count: self.perp_count,
perp_oo_count: self.perp_oo_count,
token_conditional_swap_count: self.token_conditional_swap_count,
};
let account = Pubkey::find_program_address(
@ -2383,83 +2388,6 @@ impl ClientInstruction for Serum3CancelAllOrdersInstruction {
}
}
pub struct Serum3SettleFundsInstruction {
pub account: Pubkey,
pub owner: TestKeypair,
pub serum_market: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for Serum3SettleFundsInstruction {
type Accounts = mango_v4::accounts::Serum3SettleFunds;
type Instruction = mango_v4::instruction::Serum3SettleFunds;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {};
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let serum_market: Serum3Market = account_loader.load(&self.serum_market).await.unwrap();
let open_orders = account
.serum3_orders(serum_market.market_index)
.unwrap()
.open_orders;
let quote_info =
get_mint_info_by_token_index(&account_loader, &account, serum_market.quote_token_index)
.await;
let base_info =
get_mint_info_by_token_index(&account_loader, &account, serum_market.base_token_index)
.await;
let market_external_bytes = account_loader
.load_bytes(&serum_market.serum_market_external)
.await
.unwrap();
let market_external: &serum_dex::state::MarketState = bytemuck::from_bytes(
&market_external_bytes[5..5 + std::mem::size_of::<serum_dex::state::MarketState>()],
);
// unpack the data, to avoid unaligned references
let coin_vault = market_external.coin_vault;
let pc_vault = market_external.pc_vault;
let vault_signer = serum_dex::state::gen_vault_signer_key(
market_external.vault_signer_nonce,
&serum_market.serum_market_external,
&serum_market.serum_program,
)
.unwrap();
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
open_orders,
quote_bank: quote_info.first_bank(),
quote_vault: quote_info.first_vault(),
base_bank: base_info.first_bank(),
base_vault: base_info.first_vault(),
serum_market: self.serum_market,
serum_program: serum_market.serum_program,
serum_market_external: serum_market.serum_market_external,
market_base_vault: from_serum_style_pubkey(&coin_vault),
market_quote_vault: from_serum_style_pubkey(&pc_vault),
market_vault_signer: vault_signer,
owner: self.owner.pubkey(),
token_program: Token::id(),
};
let instruction = make_instruction(program_id, &accounts, &instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.owner]
}
}
pub struct Serum3SettleFundsV2Instruction {
pub account: Pubkey,
pub owner: TestKeypair,
@ -4201,3 +4129,197 @@ impl ClientInstruction for AltExtendInstruction {
vec![self.admin, self.payer]
}
}
#[derive(Clone)]
pub struct TokenConditionalSwapCreateInstruction {
pub account: Pubkey,
pub owner: TestKeypair,
pub buy_mint: Pubkey,
pub sell_mint: Pubkey,
pub max_buy: u64,
pub max_sell: u64,
pub price_lower_limit: f64,
pub price_upper_limit: f64,
pub price_premium_fraction: f64,
pub allow_creating_deposits: bool,
pub allow_creating_borrows: bool,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for TokenConditionalSwapCreateInstruction {
type Accounts = mango_v4::accounts::TokenConditionalSwapCreate;
type Instruction = mango_v4::instruction::TokenConditionalSwapCreate;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {
max_buy: self.max_buy,
max_sell: self.max_sell,
expiry_timestamp: u64::MAX,
price_lower_limit: self.price_lower_limit,
price_upper_limit: self.price_upper_limit,
price_premium_fraction: self.price_premium_fraction,
allow_creating_deposits: self.allow_creating_deposits,
allow_creating_borrows: self.allow_creating_borrows,
};
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let buy_mint_info_address = Pubkey::find_program_address(
&[
b"MintInfo".as_ref(),
account.fixed.group.as_ref(),
self.buy_mint.as_ref(),
],
&program_id,
)
.0;
let sell_mint_info_address = Pubkey::find_program_address(
&[
b"MintInfo".as_ref(),
account.fixed.group.as_ref(),
self.sell_mint.as_ref(),
],
&program_id,
)
.0;
let buy_mint_info: MintInfo = account_loader.load(&buy_mint_info_address).await.unwrap();
let sell_mint_info: MintInfo = account_loader.load(&sell_mint_info_address).await.unwrap();
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
authority: self.owner.pubkey(),
buy_bank: buy_mint_info.first_bank(),
sell_bank: sell_mint_info.first_bank(),
};
let instruction = make_instruction(program_id, &accounts, &instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.owner]
}
}
#[derive(Clone)]
pub struct TokenConditionalSwapCancelInstruction {
pub account: Pubkey,
pub owner: TestKeypair,
pub index: u8,
pub id: u64,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for TokenConditionalSwapCancelInstruction {
type Accounts = mango_v4::accounts::TokenConditionalSwapCancel;
type Instruction = mango_v4::instruction::TokenConditionalSwapCancel;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {
token_conditional_swap_index: self.index,
token_conditional_swap_id: self.id,
};
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let tcs = account.token_conditional_swap_by_id(self.id).unwrap().1;
let buy_mint_info =
get_mint_info_by_token_index(&account_loader, &account, tcs.buy_token_index).await;
let sell_mint_info =
get_mint_info_by_token_index(&account_loader, &account, tcs.sell_token_index).await;
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
authority: self.owner.pubkey(),
buy_bank: buy_mint_info.first_bank(),
sell_bank: sell_mint_info.first_bank(),
};
let instruction = make_instruction(program_id, &accounts, &instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.owner]
}
}
#[derive(Clone)]
pub struct TokenConditionalSwapTriggerInstruction {
pub liqee: Pubkey,
pub liqor: Pubkey,
pub liqor_owner: TestKeypair,
pub index: u8,
pub max_buy_token_to_liqee: u64,
pub max_sell_token_to_liqor: u64,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for TokenConditionalSwapTriggerInstruction {
type Accounts = mango_v4::accounts::TokenConditionalSwapTrigger;
type Instruction = mango_v4::instruction::TokenConditionalSwapTrigger;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let liqee = account_loader
.load_mango_account(&self.liqee)
.await
.unwrap();
let liqor = account_loader
.load_mango_account(&self.liqor)
.await
.unwrap();
let tcs = liqee
.token_conditional_swap_by_index(self.index.into())
.unwrap()
.clone();
let instruction = Self::Instruction {
token_conditional_swap_index: self.index,
token_conditional_swap_id: tcs.id,
max_buy_token_to_liqee: self.max_buy_token_to_liqee,
max_sell_token_to_liqor: self.max_sell_token_to_liqor,
};
let health_check_metas = derive_liquidation_remaining_account_metas(
&account_loader,
&liqee,
&liqor,
tcs.buy_token_index,
0,
tcs.sell_token_index,
0,
)
.await;
let accounts = Self::Accounts {
group: liqee.fixed.group,
liqee: self.liqee,
liqor: self.liqor,
liqor_authority: self.liqor_owner.pubkey(),
};
let mut instruction = make_instruction(program_id, &accounts, &instruction);
instruction.accounts.extend(health_check_metas.into_iter());
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.liqor_owner]
}
}

View File

@ -0,0 +1,110 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { MangoClient } from '../../src/client';
import { MANGO_V4_ID } from '../../src/constants';
async function main(): Promise<void> {
try {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(process.env.CLUSTER_URL!, options);
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.USER_KEYPAIR!, 'utf-8')),
),
);
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
//
// mainnet
//
const client = await MangoClient.connect(
userProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
{
idsSource: 'get-program-accounts',
},
);
const group = await client.getGroup(
new PublicKey('78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX'),
);
console.log(
await client.getMangoAccountForOwner(
group,
new PublicKey('v3mmtZ8JjXkaAbRRMBiNsjJF1rnN3qsMQqRLMk7Nz2C'),
3,
),
);
console.log(
await client.getMangoAccountsForDelegate(
group,
new PublicKey('5P9rHX22jb3MDq46VgeaHZ2TxQDKezPxsxNX3MaXyHwT'),
),
);
//
// devnet
//
// const client = await MangoClient.connect(
// userProvider,
// 'devnet',
// MANGO_V4_ID['devnet'],
// {
// idsSource: 'get-program-accounts',
// },
// );
// const admin = Keypair.fromSecretKey(
// Buffer.from(
// JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
// ),
// );
// const group = await client.getGroupForCreator(admin.publicKey, 23);
// const mangoAccount = (await client.getMangoAccountForOwner(
// group,
// user.publicKey,
// 0,
// )) as MangoAccount;
// console.log(mangoAccount.tokenConditionalSwaps.length);
// console.log(mangoAccount.tokenConditionalSwaps);
// console.log(mangoAccount.tokenConditionalSwaps[1]);
// console.log(mangoAccount.tokenConditionalSwaps[0]);
// let sig = await client.accountExpandV2(
// group,
// mangoAccount,
// 16,
// 8,
// 8,
// 32,
// 8,
// );
// console.log(sig);
// mangoAccount = await client.getOrCreateMangoAccount(group);
// let sig = await client.tokenConditionalSwapCreate(
// group,
// mangoAccount,
// 0 as TokenIndex,
// 1 as TokenIndex,
// 0,
// 73,
// 81,
// TokenConditionalSwapPriceThresholdType.priceOverThreshold,
// 99,
// 101,
// true,
// true,
// );
// console.log(sig);
} catch (error) {
console.log(error);
}
}
main();

View File

@ -1,6 +1,7 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { MangoAccount } from '../../src/accounts/mangoAccount';
import { MangoClient } from '../../src/client';
import { MANGO_V4_ID } from '../../src/constants';
@ -37,7 +38,7 @@ export const DEVNET_SERUM3_MARKETS = new Map([
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
async function main() {
async function main(): Promise<void> {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
@ -71,9 +72,14 @@ async function main() {
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(group);
const mangoAccount = (await client.getMangoAccountForOwner(
group,
user.publicKey,
0,
)) as MangoAccount;
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
// eslint-disable-next-line no-constant-condition
if (true) {
// deposit and withdraw
@ -135,7 +141,14 @@ async function main() {
console.log(
`...expanding mango account to max 16 token positions, 8 serum3, 8 perp position and 8 perp oo slots, previous (tokens ${mangoAccount.tokens.length}, serum3 ${mangoAccount.serum3.length}, perps ${mangoAccount.perps.length}, perps oo ${mangoAccount.perpOpenOrders.length})`,
);
let sig = await client.expandMangoAccount(group, mangoAccount, 16, 8, 8, 8);
const sig = await client.expandMangoAccount(
group,
mangoAccount,
16,
8,
8,
8,
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await mangoAccount.reload(client);
}

View File

@ -3,6 +3,7 @@ import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import { expect } from 'chai';
import fs from 'fs';
import { Group } from '../../src/accounts/group';
import { MangoAccount } from '../../src/accounts/mangoAccount';
import { PerpOrderSide, PerpOrderType } from '../../src/accounts/perp';
import { MangoClient } from '../../src/client';
import { MANGO_V4_ID } from '../../src/constants';
@ -62,14 +63,19 @@ async function main(): Promise<void> {
// create + fetch account
console.log(`Creating mangoaccount...`);
let mangoAccount = (await client.getOrCreateMangoAccount(group))!;
await mangoAccount.reload(client);
const mangoAccount = (await client.getMangoAccountForOwner(
group,
user.publicKey,
0,
)) as MangoAccount;
await mangoAccount!.reload(client);
if (!mangoAccount) {
throw new Error(`MangoAccount not found for user ${user.publicKey}`);
}
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
// set delegate, and change name
// eslint-disable-next-line no-constant-condition
if (true) {
console.log(`...changing mango account name, and setting a delegate`);
const newName = 'my_changed_name';
@ -105,7 +111,14 @@ async function main(): Promise<void> {
console.log(
`...expanding mango account to max 16 token positions, 8 serum3, 8 perp position and 8 perp oo slots, previous (tokens ${mangoAccount.tokens.length}, serum3 ${mangoAccount.serum3.length}, perps ${mangoAccount.perps.length}, perps oo ${mangoAccount.perpOpenOrders.length})`,
);
let sig = await client.expandMangoAccount(group, mangoAccount, 16, 8, 8, 8);
const sig = await client.expandMangoAccount(
group,
mangoAccount,
16,
8,
8,
8,
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await mangoAccount.reload(client);
expect(mangoAccount.tokens.length).equals(16);
@ -115,6 +128,7 @@ async function main(): Promise<void> {
}
// deposit and withdraw
// eslint-disable-next-line no-constant-condition
if (true) {
console.log(`...depositing 50 USDC, 1 SOL, 1 MNGO`);
@ -311,6 +325,7 @@ async function main(): Promise<void> {
// );
// }
// eslint-disable-next-line no-constant-condition
if (true) {
await mangoAccount.reload(client);
console.log(
@ -344,8 +359,10 @@ async function main(): Promise<void> {
);
}
// eslint-disable-next-line no-constant-condition
if (true) {
function getMaxSourceForTokenSwapWrapper(src, tgt) {
// eslint-disable-next-line no-inner-declarations
function getMaxSourceForTokenSwapWrapper(src, tgt): void {
console.log(
`getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` +
mangoAccount.getMaxSourceUiForTokenSwap(
@ -397,9 +414,10 @@ async function main(): Promise<void> {
}
// perps
// eslint-disable-next-line no-constant-condition
if (true) {
let sig;
let perpMarket = group.getPerpMarketByName('BTC-PERP');
const perpMarket = group.getPerpMarketByName('BTC-PERP');
const orders = await mangoAccount.loadPerpOpenOrdersForMarket(
client,
group,
@ -701,6 +719,7 @@ async function main(): Promise<void> {
// sig = await client.perpCancelAllOrders(group, mangoAccount, perpMarket.perpMarketIndex, 10);
// console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
await perpMarket?.loadEventQueue(client)!;
const fr = perpMarket?.getInstantaneousFundingRateUi(
await perpMarket.loadBids(client),
@ -708,6 +727,7 @@ async function main(): Promise<void> {
);
console.log(`current funding rate per hour is ${fr}`);
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
const eq = await perpMarket?.loadEventQueue(client)!;
console.log(
`raw events - ${JSON.stringify(eq.eventsSince(new BN(0)), null, 2)}`,
@ -725,11 +745,13 @@ async function main(): Promise<void> {
process.exit();
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
async function logBidsAndAsks(client: MangoClient, group: Group) {
await group.reloadAll(client);
const perpMarket = group.getPerpMarketByName('BTC-PERP');
const res = [
(await perpMarket?.loadBids(client)).items(),
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
(await perpMarket?.loadAsks(client)!).items(),
];
console.log(`bids ${JSON.stringify(Array.from(res[0]), null, 2)}`);

View File

@ -394,7 +394,11 @@ async function createUser(userKeypair: string) {
const user = result[2];
console.log(`Creating MangoAccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(group);
const mangoAccount = await client.getMangoAccountForOwner(
group,
user.publicKey,
0,
);
if (!mangoAccount) {
throw new Error(`MangoAccount not found for user ${user.publicKey}`);
}

View File

@ -1,7 +1,7 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { HealthType } from '../../src/accounts/mangoAccount';
import { HealthType, MangoAccount } from '../../src/accounts/mangoAccount';
import {
MANGO_V4_ID,
MangoClient,
@ -38,10 +38,15 @@ async function main() {
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(group);
const mangoAccount = (await client.getMangoAccountForOwner(
group,
user.publicKey,
0,
)) as MangoAccount;
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString(group));
// eslint-disable-next-line no-constant-condition
if (true) {
console.log(`...depositing 0.0001 USDC`);
await client.tokenDeposit(

View File

@ -1,4 +1,5 @@
import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor';
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { BN } from '@project-serum/anchor';
import { serializeInstructionToBase64 } from '@solana/spl-governance';
import {
AccountMeta,

View File

@ -0,0 +1,94 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { Cluster, Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { MangoClient } from '../../src/client';
import { MANGO_V4_ID } from '../../src/constants';
import { I80F48 } from '../../src/numbers/I80F48';
import { expect } from 'chai';
import { HealthType } from '../../src/accounts/mangoAccount';
//
// This script creates liquidation candidates
//
const GROUP_NUM = Number(process.env.GROUP_NUM || 200);
const CLUSTER = process.env.CLUSTER || 'mainnet-beta';
async function main() {
const options = AnchorProvider.defaultOptions();
options.commitment = 'processed';
options.preflightCommitment = 'finalized';
const connection = new Connection(process.env.CLUSTER_URL!, options);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.PAYER_KEYPAIR!, 'utf-8')),
),
);
const userWallet = new Wallet(admin);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
CLUSTER as Cluster,
MANGO_V4_ID[CLUSTER],
{
idsSource: 'get-program-accounts',
prioritizationFee: 100,
txConfirmationCommitment: 'confirmed',
},
);
// fetch group
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
const accounts = await client.getMangoAccountsForOwner(
group,
admin.publicKey,
);
const usdcBank = group.banksMapByName.get('USDC')![0];
const solBank = group.banksMapByName.get('SOL')![0];
// LIQEE1 executed up the the margin limit
{
const account = ensure(
accounts.find((account) => account.name == 'LIQTEST, LIQEE1'),
);
expect(account.tokenConditionalSwapsActive.length).equal(0);
expect(Math.round(account.getTokenBalance(usdcBank).toNumber())).equal(
1000 - 4715,
);
expect(account.getHealthRatioUi(group, HealthType.init)).lessThan(1);
}
// LIQEE2 executed fully
{
const account = ensure(
accounts.find((account) => account.name == 'LIQTEST, LIQEE2'),
);
expect(account.tokenConditionalSwapsActive.length).equal(0);
expect(Math.round(account.getTokenBalance(solBank).toNumber())).equal(991);
}
// LIQEE3 was closed due to expiry
{
const account = ensure(
accounts.find((account) => account.name == 'LIQTEST, LIQEE3'),
);
expect(account.tokenConditionalSwapsActive.length).equal(0);
expect(Math.round(account.getTokenBalance(usdcBank).toNumber())).equal(
1000000,
);
}
process.exit();
}
function ensure<T>(value: T | undefined): T {
if (value == null) {
throw new Error('Value was nullish');
}
return value;
}
main();

View File

@ -71,14 +71,7 @@ async function main(): Promise<void> {
mint,
admin.publicKey,
);
await splToken.mintTo(
connection,
admin,
mint,
tokenAccount,
admin,
1000 * 1e6,
);
await splToken.mintTo(connection, admin, mint, tokenAccount, admin, 1e15);
}
//const mints = [new PublicKey('5aMD1uEcWnXnptwmyfxmTWHzx3KeMsZ7jmiJAZ3eiAdH'), new PublicKey('FijXcDUkgTiMsghQVpjRDBdUPtkrJfQdfRZkr6zLkdkW'), new PublicKey('3tVDfiFQAAT3rqLNMXUaH2p5X5R4fjz8LYEvFEQ9fDYB')]

View File

@ -0,0 +1,254 @@
import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor';
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
import { assert } from 'console';
import fs from 'fs';
import { Bank } from '../../src/accounts/bank';
import { MangoAccount } from '../../src/accounts/mangoAccount';
import {
PerpMarket,
PerpOrderSide,
PerpOrderType,
} from '../../src/accounts/perp';
import {
Serum3OrderType,
Serum3SelfTradeBehavior,
Serum3Side,
} from '../../src/accounts/serum3';
import { Builder } from '../../src/builder';
import { MangoClient } from '../../src/client';
import {
NullPerpEditParams,
NullTokenEditParams,
} from '../../src/clientIxParamBuilder';
import { MANGO_V4_ID } from '../../src/constants';
//
// This script creates liquidation candidates
//
const GROUP_NUM = Number(process.env.GROUP_NUM || 200);
const CLUSTER = process.env.CLUSTER || 'mainnet-beta';
// native prices
const PRICES = {
ETH: 1200.0,
SOL: 0.015,
USDC: 1,
MNGO: 0.02,
};
const TOKEN_SCENARIOS: [string, [string, number][], [string, number][]][] = [
[
'LIQTEST, FUNDING',
[
['USDC', 5000000],
['ETH', 100000],
['SOL', 150000000],
],
[],
],
['LIQTEST, LIQOR', [['USDC', 1000000]], []],
['LIQTEST, LIQEE1', [['USDC', 1000]], []],
['LIQTEST, LIQEE2', [['USDC', 1000000]], []],
['LIQTEST, LIQEE3', [['USDC', 1000000]], []],
];
async function main() {
const options = AnchorProvider.defaultOptions();
options.commitment = 'processed';
options.preflightCommitment = 'finalized';
const connection = new Connection(process.env.CLUSTER_URL!, options);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.PAYER_KEYPAIR!, 'utf-8')),
),
);
const userWallet = new Wallet(admin);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
CLUSTER as Cluster,
MANGO_V4_ID[CLUSTER],
{
idsSource: 'get-program-accounts',
prioritizationFee: 100,
txConfirmationCommitment: 'confirmed',
},
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
// fetch group
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(group.toString());
const MINTS = new Map([
['USDC', group.banksMapByName.get('USDC')![0].mint],
['ETH', group.banksMapByName.get('ETH')![0].mint],
['SOL', group.banksMapByName.get('SOL')![0].mint],
]);
const accounts = await client.getMangoAccountsForOwner(
group,
admin.publicKey,
);
let maxAccountNum = Math.max(0, ...accounts.map((a) => a.accountNum));
async function createMangoAccount(name: string): Promise<MangoAccount> {
const accountNum = maxAccountNum + 1;
maxAccountNum = maxAccountNum + 1;
await client.createMangoAccount(group, accountNum, name, 4, 4, 4, 4);
return (await client.getMangoAccountForOwner(
group,
admin.publicKey,
accountNum,
))!;
}
async function setBankPrice(bank: Bank, price: number): Promise<void> {
await client.stubOracleSet(group, bank.oracle, price);
// reset stable price
await client.tokenEdit(
group,
bank.mint,
Builder(NullTokenEditParams).resetStablePrice(true).build(),
);
}
async function setPerpPrice(
perpMarket: PerpMarket,
price: number,
): Promise<void> {
await client.stubOracleSet(group, perpMarket.oracle, price);
// reset stable price
await client.perpEditMarket(
group,
perpMarket.perpMarketIndex,
Builder(NullPerpEditParams).resetStablePrice(true).build(),
);
}
for (const scenario of TOKEN_SCENARIOS) {
const [name, assets, liabs] = scenario;
// create account
console.log(`Creating mangoaccount...`);
const mangoAccount = await createMangoAccount(name);
console.log(
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
for (const [assetName, assetAmount] of assets) {
const assetMint = new PublicKey(MINTS.get(assetName)!);
await client.tokenDepositNative(
group,
mangoAccount,
assetMint,
new BN(assetAmount),
);
await mangoAccount.reload(client);
}
for (const [liabName, liabAmount] of liabs) {
const liabMint = new PublicKey(MINTS.get(liabName)!);
// temporarily drop the borrowed token value, so the borrow goes through
const bank = group.banksMapByName.get(liabName)![0];
try {
await setBankPrice(bank, PRICES[liabName] / 2);
await client.tokenWithdrawNative(
group,
mangoAccount,
liabMint,
new BN(liabAmount),
true,
);
} finally {
// restore the oracle
await setBankPrice(bank, PRICES[liabName]);
}
}
}
const accounts2 = await client.getMangoAccountsForOwner(
group,
admin.publicKey,
);
// Case LIQEE1: The liqee does not have enough health for the tcs
{
const account = ensure(
accounts2.find((account) => account.name == 'LIQTEST, LIQEE1'),
);
await client.accountExpandV2(group, account, 4, 4, 4, 4, 4);
await client.tokenConditionalSwapCreate(
group,
account,
MINTS.get('SOL')!,
MINTS.get('USDC')!,
100000000,
20000, // liqee only has 1k USDC-native, leverage does not go that far!
null,
0.0,
1000000.0,
0.01,
true,
true,
);
}
// Case LIQEE2: Full execution - tcs closes afterward
{
const account = ensure(
accounts2.find((account) => account.name == 'LIQTEST, LIQEE2'),
);
await client.accountExpandV2(group, account, 4, 4, 4, 4, 4);
await client.tokenConditionalSwapCreate(
group,
account,
MINTS.get('SOL')!,
MINTS.get('USDC')!,
1000,
1000,
null,
0.0,
1000000.0,
0.01,
true,
true,
);
}
// Case LIQEE3: Create a tcs that will expire very soon
{
const account = ensure(
accounts2.find((account) => account.name == 'LIQTEST, LIQEE3'),
);
await client.accountExpandV2(group, account, 4, 4, 4, 4, 4);
await client.tokenConditionalSwapCreate(
group,
account,
MINTS.get('SOL')!,
MINTS.get('USDC')!,
1000,
1000,
Date.now() / 1000 + 15, // expire in 15s
0.0,
1000000.0,
0.01,
true,
true,
);
}
process.exit();
}
function ensure<T>(value: T | undefined): T {
if (value == null) {
throw new Error('Value was nullish');
}
return value;
}
main();

View File

@ -0,0 +1,162 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import { BookSide, PerpMarket } from '../src/accounts/perp';
import { MangoClient } from '../src/client';
import { MANGO_V4_ID } from '../src/constants';
const { MB_CLUSTER_URL } = process.env;
const GROUP_PK = '78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX';
async function buildClient(): Promise<MangoClient> {
const clientKeypair = new Keypair();
const options = AnchorProvider.defaultOptions();
const connection = new Connection(MB_CLUSTER_URL!, options);
const clientWallet = new Wallet(clientKeypair);
const clientProvider = new AnchorProvider(connection, clientWallet, options);
return await MangoClient.connect(
clientProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
{
idsSource: 'get-program-accounts',
},
);
}
function doBin(
bs: BookSide,
direction: 'bids' | 'asks',
range: {
start: number;
end: number;
scoreMultiplier: number;
queueMultiplier: number;
},
): Map<string, { size: number; score: number }> {
const bin = new Map<string, number>();
const binWithScore = new Map();
const best = bs.best();
if (!best) {
return binWithScore;
}
const bestPrice = best?.price;
const binStart =
bestPrice +
(((direction == 'bids' ? -1 : 1) * range.start) / 10000) * bestPrice;
const binEnd =
bestPrice +
(((direction == 'bids' ? -1 : 1) * range.end) / 10000) * bestPrice;
let lastSeenPrice = best.price;
let queuePosition = 0; // TODO unused
for (const item of bs.items()) {
if (lastSeenPrice != item.price) {
lastSeenPrice = item.price;
queuePosition = 0;
} else {
queuePosition = queuePosition + 1;
}
if (direction == 'bids' ? item.price <= binEnd : item.price >= binEnd) {
break;
}
if (direction == 'bids' ? item.price > binStart : item.price < binStart) {
continue;
}
bin.set(
item.owner.toBase58(),
(bin.get(item.owner.toBase58()) ?? 0) + item.size,
);
}
for (const key of bin.keys()) {
const size = bin.get(key);
if (!size) {
continue;
}
binWithScore.set(key, {
size,
score: size * range.scoreMultiplier,
});
}
return binWithScore;
}
function doSide(bs: BookSide, direction: 'bids' | 'asks'): Map<string, number> {
const bins: Map<string, { size: number; score: number }>[] = [];
for (const range of [
// Range end is exclusive, and start in inclusive
{ start: 0, end: 1, scoreMultiplier: 100, queueMultiplier: 1.25 },
{ start: 1, end: 5, scoreMultiplier: 50, queueMultiplier: 1.25 },
{ start: 5, end: 10, scoreMultiplier: 20, queueMultiplier: 1.25 },
{ start: 10, end: 20, scoreMultiplier: 7.5, queueMultiplier: 1.25 },
{ start: 20, end: 50, scoreMultiplier: 5, queueMultiplier: 1.25 },
{ start: 50, end: 100, scoreMultiplier: 2.5, queueMultiplier: 1.25 },
]) {
bins.push(doBin(bs, direction, range));
}
const aggr = new Map();
for (const bin of bins) {
for (const accountPk of bin.keys()) {
const value = bin.get(accountPk);
if (!value) {
continue;
}
const binScore = value.score;
aggr.set(accountPk, (aggr.get(accountPk) ?? 0) + binScore);
}
}
return aggr;
}
async function doMarket(
client: MangoClient,
pm: PerpMarket,
): Promise<Map<string, number>> {
const bidsAggr = doSide(await pm.loadBids(client, true), 'bids');
const asksAggr = doSide(await pm.loadAsks(client, true), 'asks');
const marketAggr = new Map();
const marketAggrNorm = new Map();
for (const accountPk of bidsAggr.keys()) {
const score = bidsAggr.get(accountPk);
if (!score) {
continue;
}
marketAggr.set(accountPk, (marketAggr.get(accountPk) ?? 0) + score);
}
for (const accountPk of asksAggr.keys()) {
const score = asksAggr.get(accountPk);
if (!score) {
continue;
}
marketAggr.set(accountPk, (marketAggr.get(accountPk) ?? 0) + score);
}
const scoreSum = Array.from(marketAggr.values()).reduce((a, b) => a + b, 0);
for (const key of marketAggr.keys()) {
marketAggrNorm.set(key, (marketAggr.get(key) / scoreSum) * 100);
}
return marketAggrNorm;
}
async function main(): Promise<void> {
const client = await buildClient();
const group = await client.getGroup(new PublicKey(GROUP_PK));
for (const pm of group.perpMarketsMapByMarketIndex.values()) {
console.log(pm.name, await doMarket(client, pm));
}
}
main();

View File

@ -1,3 +1,27 @@
## Disclaimer:
The following open source code contains an example that documents possible interaction with the smart contract for the purpose of market making. Please note that the use of this code is at your own risk and responsibility.
1. No Warranty: The code is provided "as is," without any warranty or guarantee of any kind, express or implied. The developers and contributors of this code do not make any representations or warranties regarding its accuracy, reliability, or functionality. The use of this code is solely at your own risk.
2. Limitation of Liability: In no event shall the developers and contributors of this code be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services, loss of use, data, or profits, or business interruption) arising in any way out of the use, inability to use, or the results of the use of this code, even if advised of the possibility of such damages.
3. Compliance with Laws: It is your responsibility to ensure that the use of this code complies with all applicable laws, regulations, and policies. The developers and contributors of this code shall not be held responsible for any illegal or unauthorized use of the code.
4. User Accountability: You are solely responsible for any actions performed using this code. The developers and contributors of this code shall not be held liable for any misuse, harm, or damages caused by the bot or its actions.
5. Security Considerations: While efforts have been made to ensure the security of this code, the developers and contributors do not guarantee its absolute security. It is recommended that you take appropriate measures to secure the code and any associated systems from potential vulnerabilities or threats.
6. Third-Party Dependencies: This code may rely on third-party libraries, frameworks, or APIs. The developers and contributors of this code are not responsible for the functionality, availability, or security of any third-party components.
By using this open source code, you acknowledge and agree to the above disclaimer. If you do not agree with any part of the disclaimer, refrain from using the code.
## License
See https://github.com/blockworks-foundation/mango-v4/blob/dev/LICENSE
---
This directory contains a sample market maker (`market-maker.ts`) in typescript, which can be run using ts-node.
The environment variables required are

View File

@ -17,6 +17,7 @@ export class MangoAccount {
public serum3: Serum3Orders[];
public perps: PerpPosition[];
public perpOpenOrders: PerpOo[];
public tokenConditionalSwaps: TokenConditionalSwap[];
static from(
publicKey: PublicKey,
@ -41,6 +42,7 @@ export class MangoAccount {
perps: unknown;
perpOpenOrders: unknown;
},
tokenConditionalSwaps: TokenConditionalSwapDto[],
): MangoAccount {
return new MangoAccount(
publicKey,
@ -63,6 +65,7 @@ export class MangoAccount {
obj.serum3 as Serum3PositionDto[],
obj.perps as PerpPositionDto[],
obj.perpOpenOrders as PerpOoDto[],
tokenConditionalSwaps,
new Map(), // serum3OosMapByMarketIndex
);
}
@ -88,6 +91,7 @@ export class MangoAccount {
serum3: Serum3PositionDto[],
perps: PerpPositionDto[],
perpOpenOrders: PerpOoDto[],
tokenConditionalSwaps: TokenConditionalSwapDto[],
public serum3OosMapByMarketIndex: Map<number, OpenOrders>,
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
@ -95,10 +99,13 @@ export class MangoAccount {
this.serum3 = serum3.map((dto) => Serum3Orders.from(dto));
this.perps = perps.map((dto) => PerpPosition.from(dto));
this.perpOpenOrders = perpOpenOrders.map((dto) => PerpOo.from(dto));
this.tokenConditionalSwaps = tokenConditionalSwaps.map((dto) =>
TokenConditionalSwap.from(dto),
);
}
public async reload(client: MangoClient): Promise<MangoAccount> {
const mangoAccount = await client.getMangoAccount(this);
const mangoAccount = await client.getMangoAccount(this.publicKey);
await mangoAccount.reloadSerum3OpenOrders(client);
Object.assign(this, mangoAccount);
return mangoAccount;
@ -191,6 +198,10 @@ export class MangoAccount {
return this.perps.filter((perp) => perp.isActive());
}
public tokenConditionalSwapsActive(): TokenConditionalSwap[] {
return this.tokenConditionalSwaps.filter((tcs) => tcs.hasData);
}
public perpOrdersActive(): PerpOo[] {
return this.perpOpenOrders.filter(
(oo) => oo.orderMarket !== PerpOo.OrderMarketUnset,
@ -1757,6 +1768,69 @@ export class PerpOoDto {
) {}
}
export class TokenConditionalSwap {
static from(dto: TokenConditionalSwapDto): TokenConditionalSwap {
return new TokenConditionalSwap(
dto.id,
dto.maxBuy,
dto.maxSell,
dto.bought,
dto.sold,
dto.expiryTimestamp,
dto.priceLowerLimit,
dto.priceUpperLimit,
dto.pricePremiumFraction,
dto.takerFeeFraction,
dto.makerFeeFraction,
dto.buyTokenIndex as TokenIndex,
dto.sellTokenIndex as TokenIndex,
dto.hasData == 1,
dto.allowCreatingDeposits == 1,
dto.allowCreatingBorrows == 1,
);
}
constructor(
public id: BN,
public maxBuy: BN,
public maxSell: BN,
public bought: BN,
public sold: BN,
public expiryTimestamp: BN,
public priceLowerLimit: number,
public priceUpperLimit: number,
public pricePremiumFraction: number,
public takerFeeFraction: number,
public makerFeeFraction: number,
public buyTokenIndex: TokenIndex,
public sellTokenIndex: TokenIndex,
public hasData: boolean,
public allowCreatingDeposits: boolean,
public allowCreatingBorrows: boolean,
) {}
}
export class TokenConditionalSwapDto {
constructor(
public id: BN,
public maxBuy: BN,
public maxSell: BN,
public bought: BN,
public sold: BN,
public expiryTimestamp: BN,
public priceLowerLimit: number,
public priceUpperLimit: number,
public pricePremiumFraction: number,
public takerFeeFraction: number,
public makerFeeFraction: number,
public buyTokenIndex: number,
public sellTokenIndex: number,
public hasData: number,
public allowCreatingDeposits: number,
public allowCreatingBorrows: number,
) {}
}
export class HealthType {
static maint = { maint: {} };
static init = { init: {} };

View File

@ -5,6 +5,7 @@ import {
Provider,
Wallet,
} from '@coral-xyz/anchor';
import * as borsh from '@coral-xyz/borsh';
import { OpenOrders } from '@project-serum/serum';
import {
createCloseAccountInstruction,
@ -37,6 +38,7 @@ import {
MangoAccount,
PerpPosition,
Serum3Orders,
TokenConditionalSwapDto,
TokenPosition,
} from './accounts/mangoAccount';
import { StubOracle } from './accounts/oracle';
@ -189,6 +191,8 @@ export class MangoClient {
feesSwapMangoAccount?: PublicKey,
feesMngoTokenIndex?: TokenIndex,
feesExpiryInterval?: BN,
tokenConditionalSwapTakerFeeFraction?: number,
tokenConditionalSwapMakerFeeFraction?: number,
): Promise<TransactionSignature> {
const ix = await this.program.methods
.groupEdit(
@ -203,6 +207,8 @@ export class MangoClient {
feesSwapMangoAccount ?? null,
feesMngoTokenIndex ?? null,
feesExpiryInterval ?? null,
tokenConditionalSwapTakerFeeFraction ?? null,
tokenConditionalSwapMakerFeeFraction ?? null,
)
.accounts({
group: group.publicKey,
@ -658,28 +664,6 @@ export class MangoClient {
// MangoAccount
public async getOrCreateMangoAccount(
group: Group,
loadSerum3Oo = false,
): Promise<MangoAccount> {
const clientOwner = (this.program.provider as AnchorProvider).wallet
.publicKey;
let mangoAccounts = await this.getMangoAccountsForOwner(
group,
(this.program.provider as AnchorProvider).wallet.publicKey,
loadSerum3Oo,
);
if (mangoAccounts.length === 0) {
await this.createMangoAccount(group);
mangoAccounts = await this.getMangoAccountsForOwner(
group,
clientOwner,
loadSerum3Oo,
);
}
return mangoAccounts.sort((a, b) => a.accountNum - b.accountNum)[0];
}
public async createMangoAccount(
group: Group,
accountNumber?: number,
@ -708,34 +692,6 @@ export class MangoClient {
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
}
public async createAndFetchMangoAccount(
group: Group,
accountNumber?: number,
name?: string,
tokenCount?: number,
serum3Count?: number,
perpCount?: number,
perpOoCount?: number,
loadSerum3Oo = false,
): Promise<MangoAccount | undefined> {
const accNum = accountNumber ?? 0;
await this.createMangoAccount(
group,
accNum,
name,
tokenCount,
serum3Count,
perpCount,
perpOoCount,
);
return await this.getMangoAccountForOwner(
group,
(this.program.provider as AnchorProvider).wallet.publicKey,
accNum,
loadSerum3Oo,
);
}
public async expandMangoAccount(
group: Group,
account: MangoAccount,
@ -756,6 +712,33 @@ export class MangoClient {
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
}
public async accountExpandV2(
group: Group,
account: MangoAccount,
tokenCount: number,
serum3Count: number,
perpCount: number,
perpOoCount: number,
tokenConditionalSwapCount: number,
): Promise<TransactionSignature> {
const ix = await this.program.methods
.accountExpandV2(
tokenCount,
serum3Count,
perpCount,
perpOoCount,
tokenConditionalSwapCount,
)
.accounts({
group: group.publicKey,
account: account.publicKey,
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
payer: (this.program.provider as AnchorProvider).wallet.publicKey,
})
.instruction();
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
}
public async editMangoAccount(
group: Group,
mangoAccount: MangoAccount,
@ -815,21 +798,68 @@ export class MangoClient {
}
public async getMangoAccount(
mangoAccount: MangoAccount | PublicKey,
mangoAccountPk: PublicKey,
loadSerum3Oo = false,
): Promise<MangoAccount> {
const mangoAccountPk =
mangoAccount instanceof MangoAccount
? mangoAccount.publicKey
: mangoAccount;
const mangoAccount_ = MangoAccount.from(
mangoAccountPk,
await this.program.account.mangoAccount.fetch(mangoAccountPk),
);
const mangoAccount = await this.getMangoAccountFromPk(mangoAccountPk);
if (loadSerum3Oo) {
await mangoAccount_?.reloadSerum3OpenOrders(this);
await mangoAccount?.reloadSerum3OpenOrders(this);
}
return mangoAccount_;
return mangoAccount;
}
private async getMangoAccountFromPk(
mangoAccountPk: PublicKey,
): Promise<MangoAccount> {
return await this.getMangoAccountFromAi(
mangoAccountPk,
(await this.program.provider.connection.getAccountInfo(
mangoAccountPk,
)) as AccountInfo<Buffer>,
);
}
private async getMangoAccountFromAi(
mangoAccountPk: PublicKey,
ai: AccountInfo<Buffer>,
): Promise<MangoAccount> {
const decodedMangoAccount = this.program.coder.accounts.decode(
'mangoAccount',
ai.data,
);
// Re-encode decoded mango account with v1 layout, this will help identifying
// if account is of type v1 or v2
// Do whole encoding manually, since anchor uses a buffer of a constant length which is too small
const mangoAccountV1Buffer = Buffer.alloc(ai.data.length);
const layout =
this.program.coder.accounts['accountLayouts'].get('mangoAccount');
const discriminatorLen = 8;
const v1DataLen = layout.encode(decodedMangoAccount, mangoAccountV1Buffer);
const v1Len = discriminatorLen + v1DataLen;
const tokenConditionalSwaps =
ai.data.length > v1Len
? (borsh
.vec(
(this.program as any)._coder.types.typeLayouts.get(
'TokenConditionalSwap',
),
)
.decode(
ai.data.subarray(
v1Len +
// This is the padding before tokenConditionalSwaps
4,
),
) as TokenConditionalSwapDto[])
: new Array<TokenConditionalSwapDto>();
return MangoAccount.from(
mangoAccountPk,
decodedMangoAccount,
tokenConditionalSwaps,
);
}
public async getMangoAccountWithSlot(
@ -841,11 +871,10 @@ export class MangoClient {
mangoAccountPk,
);
if (!resp?.value) return;
const decodedMangoAccount = this.program.coder.accounts.decode(
'mangoAccount',
resp.value.data,
const mangoAccount = await this.getMangoAccountFromAi(
mangoAccountPk,
resp.value,
);
const mangoAccount = MangoAccount.from(mangoAccountPk, decodedMangoAccount);
if (loadSerum3Oo) {
await mangoAccount?.reloadSerum3OpenOrders(this);
}
@ -875,24 +904,45 @@ export class MangoClient {
ownerPk: PublicKey,
loadSerum3Oo = false,
): Promise<MangoAccount[]> {
const accounts = (
await this.program.account.mangoAccount.all([
{
memcmp: {
bytes: group.publicKey.toBase58(),
offset: 8,
const discriminatorMemcmp: {
offset: number;
bytes: string;
} = this.program.account.mangoAccount.coder.accounts.memcmp(
'mangoAccount',
undefined,
);
const accounts = await Promise.all(
(
await this.program.provider.connection.getProgramAccounts(
this.programId,
{
filters: [
{
memcmp: {
bytes: discriminatorMemcmp.bytes,
offset: discriminatorMemcmp.offset,
},
},
{
memcmp: {
bytes: group.publicKey.toBase58(),
offset: 8,
},
},
{
memcmp: {
bytes: ownerPk.toBase58(),
offset: 40,
},
},
],
},
},
{
memcmp: {
bytes: ownerPk.toBase58(),
offset: 40,
},
},
])
).map((pa) => {
return MangoAccount.from(pa.publicKey, pa.account);
});
)
).map((account) => {
return this.getMangoAccountFromAi(account.pubkey, account.account);
}),
);
if (loadSerum3Oo) {
await Promise.all(
@ -908,24 +958,45 @@ export class MangoClient {
delegate: PublicKey,
loadSerum3Oo = false,
): Promise<MangoAccount[]> {
const accounts = (
await this.program.account.mangoAccount.all([
{
memcmp: {
bytes: group.publicKey.toBase58(),
offset: 8,
const discriminatorMemcmp: {
offset: number;
bytes: string;
} = this.program.account.mangoAccount.coder.accounts.memcmp(
'mangoAccount',
undefined,
);
const accounts = await Promise.all(
(
await this.program.provider.connection.getProgramAccounts(
this.programId,
{
filters: [
{
memcmp: {
bytes: discriminatorMemcmp.bytes,
offset: discriminatorMemcmp.offset,
},
},
{
memcmp: {
bytes: group.publicKey.toBase58(),
offset: 8,
},
},
{
memcmp: {
bytes: delegate.toBase58(),
offset: 104,
},
},
],
},
},
{
memcmp: {
bytes: delegate.toBase58(),
offset: 104,
},
},
])
).map((pa) => {
return MangoAccount.from(pa.publicKey, pa.account);
});
)
).map((account) => {
return this.getMangoAccountFromAi(account.pubkey, account.account);
}),
);
if (loadSerum3Oo) {
await Promise.all(
@ -940,18 +1011,39 @@ export class MangoClient {
group: Group,
loadSerum3Oo = false,
): Promise<MangoAccount[]> {
const accounts = (
await this.program.account.mangoAccount.all([
{
memcmp: {
bytes: group.publicKey.toBase58(),
offset: 8,
const discriminatorMemcmp: {
offset: number;
bytes: string;
} = this.program.account.mangoAccount.coder.accounts.memcmp(
'mangoAccount',
undefined,
);
const accounts = await Promise.all(
(
await this.program.provider.connection.getProgramAccounts(
this.programId,
{
filters: [
{
memcmp: {
bytes: discriminatorMemcmp.bytes,
offset: discriminatorMemcmp.offset,
},
},
{
memcmp: {
bytes: group.publicKey.toBase58(),
offset: 8,
},
},
],
},
},
])
).map((pa) => {
return MangoAccount.from(pa.publicKey, pa.account);
});
)
).map((account) => {
return this.getMangoAccountFromAi(account.pubkey, account.account);
}),
);
if (loadSerum3Oo) {
const ooPks = accounts
@ -1344,11 +1436,12 @@ export class MangoClient {
serum3MarketIndex: MarketIndex,
reduceOnly: boolean | null,
forceClose: boolean | null,
name: string | null,
): Promise<TransactionSignature> {
const serum3Market =
group.serum3MarketsMapByMarketIndex.get(serum3MarketIndex);
const ix = await this.program.methods
.serum3EditMarket(reduceOnly, forceClose)
.serum3EditMarket(reduceOnly, forceClose, name)
.accounts({
group: group.publicKey,
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
@ -1813,48 +1906,11 @@ export class MangoClient {
);
}
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
const serum3MarketExternal = group.serum3ExternalMarketsMap.get(
externalMarketPk.toBase58(),
)!;
const [serum3MarketExternalVaultSigner, openOrderPublicKey] =
await Promise.all([
generateSerum3MarketExternalVaultSignerAddress(
this.cluster,
serum3Market,
serum3MarketExternal,
),
serum3Market.findOoPda(this.program.programId, mangoAccount.publicKey),
]);
const ix = await this.program.methods
.serum3SettleFunds()
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
openOrders: openOrderPublicKey,
serumMarket: serum3Market.publicKey,
serumProgram: OPENBOOK_PROGRAM_ID[this.cluster],
serumMarketExternal: serum3Market.serumMarketExternal,
marketBaseVault: serum3MarketExternal.decoded.baseVault,
marketQuoteVault: serum3MarketExternal.decoded.quoteVault,
marketVaultSigner: serum3MarketExternalVaultSigner,
quoteBank: group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex)
.publicKey,
quoteVault: group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex)
.vault,
baseBank: group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
.publicKey,
baseVault: group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
.vault,
})
.instruction();
return ix;
return await this.serum3SettleFundsV2Ix(
group,
mangoAccount,
externalMarketPk,
);
}
public async serum3SettleFundsV2Ix(
@ -3249,6 +3305,137 @@ export class MangoClient {
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
}
public async tokenConditionalSwapCreate(
group: Group,
account: MangoAccount,
buyMintPk: PublicKey,
sellMintPk: PublicKey,
maxBuy: number,
maxSell: number,
expiryTimestamp: number | null,
priceLowerLimit: number,
priceUpperLimit: number,
pricePremiumFraction: number,
allowCreatingDeposits: boolean,
allowCreatingBorrows: boolean,
): Promise<TransactionSignature> {
const buyBank: Bank = group.getFirstBankByMint(buyMintPk);
const sellBank: Bank = group.getFirstBankByMint(sellMintPk);
const ix = await this.program.methods
.tokenConditionalSwapCreate(
new BN(maxBuy),
new BN(maxSell),
expiryTimestamp !== null ? new BN(expiryTimestamp) : U64_MAX_BN,
priceLowerLimit,
priceUpperLimit,
pricePremiumFraction,
allowCreatingDeposits,
allowCreatingBorrows,
)
.accounts({
group: group.publicKey,
account: account.publicKey,
authority: (this.program.provider as AnchorProvider).wallet.publicKey,
buyBank: buyBank.publicKey,
sellBank: sellBank.publicKey,
})
.instruction();
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
}
public async tokenConditionalSwapCancel(
group: Group,
account: MangoAccount,
tokenConditionalSwapIndex: number,
tokenConditionalSwapId: BN,
): Promise<TransactionSignature> {
const tcs = account
.tokenConditionalSwapsActive()
.find((tcs) => tcs.id.eq(tokenConditionalSwapId));
if (!tcs) {
throw new Error('tcs with id not found');
}
const buyBank = group.banksMapByTokenIndex.get(tcs.buyTokenIndex)![0];
const sellBank = group.banksMapByTokenIndex.get(tcs.sellTokenIndex)![0];
const ix = await this.program.methods
.tokenConditionalSwapCancel(
tokenConditionalSwapIndex,
new BN(tokenConditionalSwapId),
)
.accounts({
group: group.publicKey,
account: account.publicKey,
authority: (this.program.provider as AnchorProvider).wallet.publicKey,
buyBank: buyBank.publicKey,
sellBank: sellBank.publicKey,
})
.instruction();
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
}
public async tokenConditionalSwapTrigger(
group: Group,
liqee: MangoAccount,
liqor: MangoAccount,
tokenConditionalSwapIndex: number,
tokenConditionalSwapId: BN,
maxBuyTokenToLiqee: number,
maxSellTokenToLiqor: number,
): Promise<TransactionSignature> {
const tcs = liqee
.tokenConditionalSwapsActive()
.find((tcs) => tcs.id.eq(tokenConditionalSwapId));
if (!tcs) {
throw new Error('tcs with id not found');
}
const buyBank = group.banksMapByTokenIndex.get(tcs.buyTokenIndex)![0];
const sellBank = group.banksMapByTokenIndex.get(tcs.sellTokenIndex)![0];
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
group,
[liqor, liqee],
[buyBank, sellBank],
[],
);
const parsedHealthAccounts = healthRemainingAccounts.map(
(pk) =>
({
pubkey: pk,
isWritable:
pk.equals(buyBank.publicKey) || pk.equals(sellBank.publicKey)
? true
: false,
isSigner: false,
} as AccountMeta),
);
const ix = await this.program.methods
.tokenConditionalSwapTrigger(
tokenConditionalSwapIndex,
new BN(tokenConditionalSwapId),
new BN(maxBuyTokenToLiqee),
new BN(maxSellTokenToLiqor),
)
.accounts({
group: group.publicKey,
liqee: liqee.publicKey,
liqor: liqor.publicKey,
liqorAuthority: (this.program.provider as AnchorProvider).wallet
.publicKey,
})
.remainingAccounts(parsedHealthAccounts)
.instruction();
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
}
public async altSet(
group: Group,
addressLookupTable: PublicKey,

View File

@ -179,6 +179,21 @@ export interface IxGateParams {
TokenForceCloseBorrowsWithToken: boolean;
PerpForceClosePosition: boolean;
GroupWithdrawInsuranceFund: boolean;
TokenConditionalSwapCreate: boolean;
TokenConditionalSwapTrigger: boolean;
TokenConditionalSwapCancel: boolean;
OpenbookV2CancelOrder: boolean;
OpenbookV2CloseOpenOrders: boolean;
OpenbookV2CreateOpenOrders: boolean;
OpenbookV2DeregisterMarket: boolean;
OpenbookV2EditMarket: boolean;
OpenbookV2LiqForceCancelOrders: boolean;
OpenbookV2PlaceOrder: boolean;
OpenbookV2PlaceTakeOrder: boolean;
OpenbookV2RegisterMarket: boolean;
OpenbookV2SettleFunds: boolean;
AdminTokenWithdrawFees: boolean;
AdminPerpWithdrawFees: boolean;
}
// Default with all ixs enabled, use with buildIxGate
@ -238,6 +253,21 @@ export const TrueIxGateParams: IxGateParams = {
TokenForceCloseBorrowsWithToken: true,
PerpForceClosePosition: true,
GroupWithdrawInsuranceFund: true,
TokenConditionalSwapCreate: true,
TokenConditionalSwapTrigger: true,
TokenConditionalSwapCancel: true,
OpenbookV2CancelOrder: true,
OpenbookV2CloseOpenOrders: true,
OpenbookV2CreateOpenOrders: true,
OpenbookV2DeregisterMarket: true,
OpenbookV2EditMarket: true,
OpenbookV2LiqForceCancelOrders: true,
OpenbookV2PlaceOrder: true,
OpenbookV2PlaceTakeOrder: true,
OpenbookV2RegisterMarket: true,
OpenbookV2SettleFunds: true,
AdminTokenWithdrawFees: true,
AdminPerpWithdrawFees: true,
};
// build ix gate e.g. buildIxGate(Builder(TrueIxGateParams).TokenDeposit(false).build()).toNumber(),
@ -307,6 +337,21 @@ export function buildIxGate(p: IxGateParams): BN {
toggleIx(ixGate, p, 'TokenForceCloseBorrowsWithToken', 49);
toggleIx(ixGate, p, 'PerpForceClosePosition', 50);
toggleIx(ixGate, p, 'GroupWithdrawInsuranceFund', 51);
toggleIx(ixGate, p, 'TokenConditionalSwapCreate', 52);
toggleIx(ixGate, p, 'TokenConditionalSwapTrigger', 53);
toggleIx(ixGate, p, 'TokenConditionalSwapCancel', 54);
toggleIx(ixGate, p, 'OpenbookV2CancelOrder', 55);
toggleIx(ixGate, p, 'OpenbookV2CloseOpenOrders', 56);
toggleIx(ixGate, p, 'OpenbookV2CreateOpenOrders', 57);
toggleIx(ixGate, p, 'OpenbookV2DeregisterMarket', 58);
toggleIx(ixGate, p, 'OpenbookV2EditMarket', 59);
toggleIx(ixGate, p, 'OpenbookV2LiqForceCancelOrders', 60);
toggleIx(ixGate, p, 'OpenbookV2PlaceOrder', 61);
toggleIx(ixGate, p, 'OpenbookV2PlaceTakeOrder', 62);
toggleIx(ixGate, p, 'OpenbookV2RegisterMarket', 63);
toggleIx(ixGate, p, 'OpenbookV2SettleFunds', 63);
toggleIx(ixGate, p, 'AdminTokenWithdrawFees', 65);
toggleIx(ixGate, p, 'AdminPerpWithdrawFees', 66);
return ixGate;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,205 @@
import { toNative } from '../utils';
const PREMIUM_LISTING = {
maxStalenessSlots: 120 as number | null,
oracleConfFilter: 0.1,
adjustmentFactor: 0.004,
util0: 0.5,
rate0: 0.052,
util1: 0.8,
rate1: 0.1446,
maxRate: 1.4456,
loanFeeRate: 0.005,
loanOriginationFeeRate: 0.001,
maintAssetWeight: 0.9,
initAssetWeight: 0.8,
maintLiabWeight: 1.1,
initLiabWeight: 1.2,
liquidationFee: 0.05,
minVaultToDepositsRatio: 0.2,
netBorrowLimitWindowSizeTs: 24 * 60 * 60,
netBorrowLimitPerWindowQuote: toNative(50000, 6).toNumber(),
insuranceFound: true,
borrowWeightScale: toNative(250000, 6).toNumber(),
depositWeightScale: toNative(250000, 6).toNumber(),
preset_name: 'Blue chip',
preset_key: 'PREMIUM',
preset_target_amount: 100000,
};
export type ListingPreset = typeof PREMIUM_LISTING;
export type LISTING_PRESETS_KEYS =
| 'PREMIUM'
| 'MID'
| 'MEME'
| 'SHIT'
| 'UNTRUSTED';
export const LISTING_PRESETS: {
[key in LISTING_PRESETS_KEYS]: ListingPreset | Record<string, never>;
} = {
//Price impact on $100,000 swap lower then 1%
PREMIUM: {
...PREMIUM_LISTING,
},
//Price impact on $20,000 swap lower then 1%
MID: {
...PREMIUM_LISTING,
maintAssetWeight: 0.75,
initAssetWeight: 0.5,
maintLiabWeight: 1.2,
initLiabWeight: 1.4,
liquidationFee: 0.1,
netBorrowLimitPerWindowQuote: toNative(20000, 6).toNumber(),
borrowWeightScale: toNative(50000, 6).toNumber(),
depositWeightScale: toNative(50000, 6).toNumber(),
insuranceFound: false,
preset_name: 'Midwit',
preset_key: 'MID',
preset_target_amount: 20000,
},
//Price impact on $5,000 swap lower then 1%
MEME: {
...PREMIUM_LISTING,
maxStalenessSlots: 800,
loanOriginationFeeRate: 0.002,
maintAssetWeight: 0,
initAssetWeight: 0,
maintLiabWeight: 1.25,
initLiabWeight: 1.5,
liquidationFee: 0.125,
netBorrowLimitPerWindowQuote: toNative(5000, 6).toNumber(),
borrowWeightScale: toNative(20000, 6).toNumber(),
depositWeightScale: toNative(20000, 6).toNumber(),
insuranceFound: false,
preset_name: 'Meme Coin',
preset_key: 'MEME',
preset_target_amount: 5000,
},
//Price impact on $1,000 swap lower then 1%
SHIT: {
...PREMIUM_LISTING,
maxStalenessSlots: 800,
loanOriginationFeeRate: 0.002,
maintAssetWeight: 0,
initAssetWeight: 0,
maintLiabWeight: 1.4,
initLiabWeight: 1.8,
liquidationFee: 0.2,
netBorrowLimitPerWindowQuote: toNative(1000, 6).toNumber(),
borrowWeightScale: toNative(5000, 6).toNumber(),
depositWeightScale: toNative(5000, 6).toNumber(),
insuranceFound: false,
preset_name: 'Shit Coin',
preset_key: 'SHIT',
preset_target_amount: 1000,
},
//should run untrusted instruction
UNTRUSTED: {},
};
export type MarketTradingParams = {
baseLots: number;
quoteLots: number;
minOrderValue: number;
baseLotExponent: number;
quoteLotExponent: number;
minOrderSize: number;
priceIncrement: number;
priceIncrementRelative: number;
};
// definitions:
// baseLots = 10 ^ baseLotExponent
// quoteLots = 10 ^ quoteLotExponent
// minOrderSize = 10^(baseLotExponent - baseDecimals)
// minOrderValue = basePrice * minOrderSize
// priceIncrement = 10^(quoteLotExponent + baseDecimals - baseLotExponent - quoteDecimals)
// priceIncrementRelative = priceIncrement * quotePrice / basePrice
// derive: baseLotExponent <= min[ basePrice * minOrderSize > 0.05]
// baseLotExponent = 10
// While (baseLotExponent < 10):
// minOrderSize = 10^(baseLotExponent - baseDecimals)
// minOrderValue = basePrice * minOrderSize
// if minOrderValue > 0.05:
// break;
// Derive: quoteLotExponent <= min[ priceIncrement * quotePrice / basePrice > 0.000025 ]
// quoteLotExponent = 0
// While (quoteLotExponent < 10):
// priceIncrement = 10^(quoteLotExponent + baseDecimals - baseLotExponent - quoteDecimals)
// priceIncrementRelative = priceIncrement * quotePrice / basePrice
// if priceIncrementRelative > 0.000025:
// break;
export const calculateMarketTradingParams = (
basePrice: number,
quotePrice: number,
baseDecimals: number,
quoteDecimals: number,
): MarketTradingParams => {
const MAX_MIN_ORDER_VALUE = 0.05;
const MIN_PRICE_INCREMENT_RELATIVE = 0.000025;
const EXPONENT_THRESHOLD = 10;
let minOrderSize = 0;
let priceIncrement = 0;
let baseLotExponent = 0;
let quoteLotExponent = 0;
let minOrderValue = 0;
let priceIncrementRelative = 0;
// Calculate minimum order size
do {
minOrderSize = Math.pow(10, baseLotExponent - baseDecimals);
minOrderValue = basePrice * minOrderSize;
if (minOrderValue > MAX_MIN_ORDER_VALUE) {
break;
}
baseLotExponent++;
} while (baseLotExponent < EXPONENT_THRESHOLD);
// Calculate price increment
do {
priceIncrement = Math.pow(
10,
quoteLotExponent + baseDecimals - baseLotExponent - quoteDecimals,
);
priceIncrementRelative = (priceIncrement * quotePrice) / basePrice;
if (priceIncrementRelative > MIN_PRICE_INCREMENT_RELATIVE) {
break;
}
quoteLotExponent++;
} while (quoteLotExponent < EXPONENT_THRESHOLD);
//exception override values in that case example eth/btc market
if (
quoteLotExponent === 0 &&
priceIncrementRelative > 0.001 &&
minOrderSize < 1
) {
baseLotExponent = baseLotExponent + 1;
minOrderSize = Math.pow(10, baseLotExponent - baseDecimals);
minOrderValue = basePrice * minOrderSize;
priceIncrement = Math.pow(
10,
quoteLotExponent + baseDecimals - baseLotExponent - quoteDecimals,
);
priceIncrementRelative = (priceIncrement * quotePrice) / basePrice;
}
return {
baseLots: Math.pow(10, baseLotExponent),
quoteLots: Math.pow(10, quoteLotExponent),
minOrderValue: minOrderValue,
baseLotExponent: baseLotExponent,
quoteLotExponent: quoteLotExponent,
minOrderSize: minOrderSize,
priceIncrement: priceIncrement,
priceIncrementRelative: priceIncrementRelative,
};
};

View File

@ -2071,9 +2071,10 @@ node-addon-api@^2.0.0:
resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz"
integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==
node-fetch@2, node-fetch@2.6.7, node-fetch@^2.6.1, node-fetch@^2.6.7, "node-fetch@https://github.com/blockworks-foundation/node-fetch.git#v2.6.11-fixed":
version "2.6.7"
resolved "https://github.com/blockworks-foundation/node-fetch.git#2a2dffa8828cc4fa3d45f480dd40d0a790c132c3"
node-fetch@2, node-fetch@2.6.7, node-fetch@^2.6.1, node-fetch@^2.6.7, "node-fetch@npm:@blockworks-foundation/node-fetch@2.6.11":
version "2.6.11"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/node-fetch/-/node-fetch-2.6.11.tgz#fb536ef0e6a960e7b7993f3c1d3b3bba9bdfbc56"
integrity sha512-HeDTxpIypSR4qCoqgUXGr8YL4OG1z7BbV4VhQ9iQs+pt2wV3MtqO+sQk2vXK3WDKu5C6BsbGmWE22BmIrcuOOw==
dependencies:
whatwg-url "^5.0.0"