Merge branch 'main' into deploy

This commit is contained in:
microwavedcola1 2024-03-10 14:26:29 +01:00
commit f2442428b6
99 changed files with 5307 additions and 1361 deletions

View File

@ -23,7 +23,7 @@ jobs:
- name: Verifiable Build
run: |
anchor build --verifiable --docker-image backpackapp/build:v0.28.0 --solana-version 1.16.14 -- --features enable-gpl
anchor build --verifiable --docker-image backpackapp/build:v0.28.0 --solana-version 1.16.14 --env GITHUB_SHA --env GITHUB_REF_NAME -- --features enable-gpl
- name: Generate Checksum
run: |

View File

@ -4,6 +4,7 @@ Update this for each program release and mainnet deployment.
## not on mainnet
<<<<<<< HEAD
### v0.22.0, 2024-2-
- Perp: Allow reusing your own perp order slots immediately (#817)
@ -32,6 +33,61 @@ Update this for each program release and mainnet deployment.
## mainnet
=======
### v0.23.0, 2024-3-
- Allow disabling asset liquidations for tokens (#867)
This allows listing tokens that have no reliable oracle. Those tokens could be
traded through mango but can't be borrowed, can't have asset weight and can't
even be liquidated.
- Add configurable collateral fees for tokens (#868, #880, #894)
Collateral fees allow the DAO to regularly charge users for using particular
types of collateral to back their liabilities.
- Add force_withdraw token state (#884)
There already is a force_close_borrows state, but for a full delisting user
deposits need to be removed too. In force_withdraw, user deposits can be
permissionlessly withdrawn to their owners' token accounts.
- Flash loan: Add a "swap without flash loan fees" option (#882)
- Cleanup, tests and minor (#878, #875, #854, #838, #895)
## mainnet
### v0.22.0, 2024-3-3
Deployment: Mar 3, 2024 at 23:52:08 Central European Standard Time, https://explorer.solana.com/tx/3MpEMU12Pv7RpSnwfShoM9sbyr41KAEeJFCVx9ypkq8nuK8Q5vm7CRLkdhH3u91yQ4k44a32armZHaoYguX6NqsY
- Perp: Allow reusing your own perp order slots immediately (#817)
Previously users who placed a lot of perp orders and used time-in-force needed
to wait for out-event cranking if their perp order before reusing an order
slot. Now perp order slots can be reused even when the out-event is still on
the event queue.
- Introduce fallback oracles (#790, #813)
Fallback oracles can be used when the primary oracle is stale or not confident.
These oracles need to configured by the DAO to be usable by clients.
Fallback oracles may be based on Orca in addition to the other supported types.
- Add serum3_cancel_by_client_order_id instruction (#798)
Can now cancel by client order id and not just the order id.
- Add configurable platform liquidation fees for tokens and perps (#849, #858)
- Delegates can now withdraw small token amounts to the owner's ata (#820)
- Custom allocator to allow larger heap use if needed (#801)
- Optimize compute use in token_deposit instruction (#786)
- Disable support for v1 and v2 mango accounts (#783)
- Cleanups, logging and tests (#819, #799, #818, #823, #834, #828, #833)
>>>>>>> main
### v0.21.3, 2024-2-9
Deployment: Feb 9, 2024 at 11:21:58 Central European Standard Time, https://explorer.solana.com/tx/44f2wcLyLiic1aycdaPTdfwXJBMeGeuA984kvCByg4L5iGprH6xW3D35gd3bvZ6kU3SipEtoY3kDuexJghbxL89T

8
Cargo.lock generated
View File

@ -3367,7 +3367,11 @@ dependencies = [
[[package]]
name = "mango-v4"
<<<<<<< HEAD
version = "0.22.0"
=======
version = "0.23.0"
>>>>>>> main
dependencies = [
"anchor-lang",
"anchor-spl",
@ -3445,6 +3449,7 @@ dependencies = [
"atty",
"base64 0.13.1",
"bincode",
"clap 3.2.25",
"derive_builder",
"fixed 1.11.0 (git+https://github.com/blockworks-foundation/fixed.git?branch=v1.11.0-borsh0_10-mango)",
"futures 0.3.28",
@ -3464,10 +3469,12 @@ dependencies = [
"solana-client",
"solana-rpc",
"solana-sdk",
"solana-transaction-status",
"spl-associated-token-account 1.1.3",
"thiserror",
"tokio",
"tokio-stream",
"tokio-tungstenite 0.17.2",
"tracing",
"tracing-subscriber",
]
@ -3528,6 +3535,7 @@ dependencies = [
"once_cell",
"pyth-sdk-solana",
"rand 0.7.3",
"regex",
"serde",
"serde_derive",
"serde_json",

View File

@ -24,6 +24,7 @@ solana-program = "~1.16.7"
solana-program-test = "~1.16.7"
solana-rpc = "~1.16.7"
solana-sdk = { version = "~1.16.7", default-features = false }
solana-transaction-status = { version = "~1.16.7" }
[profile.release]
overflow-checks = true

View File

@ -23,7 +23,9 @@
- Do a verifiable build
anchor build --verifiable --solana-version 1.14.13 -- --features enable-gpl
Set GITHUB_SHA and GITHUB_REF_NAME to the release sha1 and tag name.
anchor build --verifiable --docker-image backpackapp/build:v0.28.0 --solana-version 1.16.14 --env GITHUB_SHA --env GITHUB_REF_NAME -- --features enable-gpl
(or wait for github to finish and create the release)

View File

@ -1,10 +1,14 @@
use clap::clap_derive::ArgEnum;
use clap::{Args, Parser, Subcommand};
use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
use mango_v4::state::{PlaceOrderType, SelfTradeBehavior, Side};
use mango_v4_client::{
keypair_from_cli, pubkey_from_cli, Client, MangoClient, TransactionBuilderConfig,
};
use solana_sdk::pubkey::Pubkey;
use std::str::FromStr;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
mod save_snapshot;
mod test_oracles;
@ -88,6 +92,98 @@ struct JupiterSwap {
rpc: Rpc,
}
#[derive(ArgEnum, Clone, Debug)]
#[repr(u8)]
pub enum CliSide {
Bid = 0,
Ask = 1,
}
#[derive(Args, Debug, Clone)]
struct PerpPlaceOrder {
#[clap(long)]
account: String,
/// also pays for everything
#[clap(short, long)]
owner: String,
#[clap(long)]
market_name: String,
#[clap(long, value_enum)]
side: CliSide,
#[clap(short, long)]
price: f64,
#[clap(long)]
quantity: f64,
#[clap(long)]
expiry: u64,
#[clap(flatten)]
rpc: Rpc,
}
#[derive(Args, Debug, Clone)]
struct Serum3CreateOpenOrders {
#[clap(long)]
account: String,
/// also pays for everything
#[clap(short, long)]
owner: String,
#[clap(long)]
market_name: String,
#[clap(flatten)]
rpc: Rpc,
}
#[derive(Args, Debug, Clone)]
struct Serum3CloseOpenOrders {
#[clap(long)]
account: String,
/// also pays for everything
#[clap(short, long)]
owner: String,
#[clap(long)]
market_name: String,
#[clap(flatten)]
rpc: Rpc,
}
#[derive(Args, Debug, Clone)]
struct Serum3PlaceOrder {
#[clap(long)]
account: String,
/// also pays for everything
#[clap(short, long)]
owner: String,
#[clap(long)]
market_name: String,
#[clap(long, value_enum)]
side: CliSide,
#[clap(short, long)]
price: f64,
#[clap(long)]
quantity: f64,
#[clap(flatten)]
rpc: Rpc,
}
#[derive(Subcommand, Debug, Clone)]
enum Command {
CreateAccount(CreateAccount),
@ -128,21 +224,28 @@ enum Command {
#[clap(short, long)]
output: String,
},
PerpPlaceOrder(PerpPlaceOrder),
Serum3CloseOpenOrders(Serum3CloseOpenOrders),
Serum3CreateOpenOrders(Serum3CreateOpenOrders),
Serum3PlaceOrder(Serum3PlaceOrder),
}
impl Rpc {
fn client(&self, override_fee_payer: Option<&str>) -> anyhow::Result<Client> {
let fee_payer = keypair_from_cli(override_fee_payer.unwrap_or(&self.fee_payer));
Ok(Client::new(
anchor_client::Cluster::from_str(&self.url)?,
solana_sdk::commitment_config::CommitmentConfig::confirmed(),
Arc::new(fee_payer),
None,
TransactionBuilderConfig {
prioritization_micro_lamports: Some(5),
compute_budget_per_instruction: Some(250_000),
},
))
Ok(Client::builder()
.cluster(anchor_client::Cluster::from_str(&self.url)?)
.commitment(solana_sdk::commitment_config::CommitmentConfig::confirmed())
.fee_payer(Some(Arc::new(fee_payer)))
.transaction_builder_config(
TransactionBuilderConfig::builder()
.prioritization_micro_lamports(Some(5))
.compute_budget_per_instruction(Some(250_000))
.build()
.unwrap(),
)
.build()
.unwrap())
}
}
@ -204,15 +307,8 @@ async fn main() -> Result<(), anyhow::Error> {
let output_mint = pubkey_from_cli(&cmd.output_mint);
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
let txsig = client
.jupiter_v4()
.swap(
input_mint,
output_mint,
cmd.amount,
cmd.slippage_bps,
mango_v4_client::JupiterSwapMode::ExactIn,
false,
)
.jupiter_v6()
.swap(input_mint, output_mint, cmd.amount, cmd.slippage_bps, false)
.await?;
println!("{}", txsig);
}
@ -245,6 +341,111 @@ async fn main() -> Result<(), anyhow::Error> {
let client = rpc.client(None)?;
save_snapshot::save_snapshot(mango_group, client, output).await?
}
Command::PerpPlaceOrder(cmd) => {
let client = cmd.rpc.client(Some(&cmd.owner))?;
let account = pubkey_from_cli(&cmd.account);
let owner = Arc::new(keypair_from_cli(&cmd.owner));
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
let market = client
.context
.perp_markets
.iter()
.find(|p| p.1.name == cmd.market_name)
.unwrap()
.1;
fn native(x: f64, b: u32) -> i64 {
(x * (10_i64.pow(b)) as f64) as i64
}
let price_lots = native(cmd.price, 6) * market.base_lot_size
/ (market.quote_lot_size * 10_i64.pow(market.base_decimals.into()));
let max_base_lots =
native(cmd.quantity, market.base_decimals.into()) / market.base_lot_size;
let txsig = client
.perp_place_order(
market.perp_market_index,
match cmd.side {
CliSide::Bid => Side::Bid,
CliSide::Ask => Side::Ask,
},
price_lots,
max_base_lots,
i64::max_value(),
SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
PlaceOrderType::Limit,
false,
if cmd.expiry > 0 {
SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() + cmd.expiry
} else {
0
},
10,
SelfTradeBehavior::AbortTransaction,
)
.await?;
println!("{}", txsig);
}
Command::Serum3CreateOpenOrders(cmd) => {
let client = cmd.rpc.client(Some(&cmd.owner))?;
let account = pubkey_from_cli(&cmd.account);
let owner = Arc::new(keypair_from_cli(&cmd.owner));
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
let txsig = client.serum3_create_open_orders(&cmd.market_name).await?;
println!("{}", txsig);
}
Command::Serum3CloseOpenOrders(cmd) => {
let client = cmd.rpc.client(Some(&cmd.owner))?;
let account = pubkey_from_cli(&cmd.account);
let owner = Arc::new(keypair_from_cli(&cmd.owner));
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
let txsig = client.serum3_close_open_orders(&cmd.market_name).await?;
println!("{}", txsig);
}
Command::Serum3PlaceOrder(cmd) => {
let client = cmd.rpc.client(Some(&cmd.owner))?;
let account = pubkey_from_cli(&cmd.account);
let owner = Arc::new(keypair_from_cli(&cmd.owner));
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
let market_index = client.context.serum3_market_index(&cmd.market_name);
let market = client.context.serum3(market_index);
let base_token = client.context.token(market.base_token_index);
let quote_token = client.context.token(market.quote_token_index);
fn native(x: f64, b: u32) -> u64 {
(x * (10_i64.pow(b)) as f64) as u64
}
// coin_lot_size = base lot size ?
// cf priceNumberToLots
let price_lots = native(cmd.price, quote_token.decimals as u32) * market.coin_lot_size
/ (native(1.0, base_token.decimals as u32) * market.pc_lot_size);
// cf baseSizeNumberToLots
let max_base_lots =
native(cmd.quantity, base_token.decimals as u32) / market.coin_lot_size;
let txsig = client
.serum3_place_order(
&cmd.market_name,
match cmd.side {
CliSide::Bid => Serum3Side::Bid,
CliSide::Ask => Serum3Side::Ask,
},
price_lots,
max_base_lots as u64,
((price_lots * max_base_lots) as f64 * 1.01) as u64,
Serum3SelfTradeBehavior::AbortTransaction,
Serum3OrderType::Limit,
SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
10,
)
.await?;
println!("{}", txsig);
}
};
Ok(())

View File

@ -23,10 +23,10 @@ pub async fn save_snapshot(
}
fs::create_dir_all(out_path).unwrap();
let rpc_url = client.cluster.url().to_string();
let ws_url = client.cluster.ws_url().to_string();
let rpc_url = client.config().cluster.url().to_string();
let ws_url = client.config().cluster.ws_url().to_string();
let group_context = MangoGroupContext::new_from_rpc(&client.rpc_async(), mango_group).await?;
let group_context = MangoGroupContext::new_from_rpc(client.rpc_async(), mango_group).await?;
let oracles_and_vaults = group_context
.tokens

View File

@ -1,17 +1,34 @@
use std::{collections::HashSet, sync::Arc, time::Duration, time::Instant};
use std::{
collections::HashSet,
sync::Arc,
time::Instant,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use crate::MangoClient;
use anyhow::Context;
use itertools::Itertools;
use anchor_lang::{__private::bytemuck::cast_ref, solana_program};
use anchor_lang::{__private::bytemuck::cast_ref, solana_program, Discriminator};
use futures::Future;
use mango_v4::state::{EventQueue, EventType, FillEvent, OutEvent, TokenIndex};
use mango_v4_client::PerpMarketContext;
use mango_v4::{
accounts_zerocopy::AccountReader,
state::{
EventQueue, EventType, FillEvent, Group, MangoAccount, MangoAccountValue, OutEvent,
TokenIndex,
},
};
use mango_v4_client::{
account_fetcher_fetch_anchor_account, AccountFetcher, PerpMarketContext, PreparedInstructions,
RpcAccountFetcher, TransactionBuilder,
};
use prometheus::{register_histogram, Encoder, Histogram, IntCounter, Registry};
use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
signature::Signature,
};
use tokio::task::JoinHandle;
use tracing::*;
use warp::Filter;
@ -80,6 +97,9 @@ pub async fn runner(
interval_consume_events: u64,
interval_update_funding: u64,
interval_check_for_changes_and_abort: u64,
interval_charge_collateral_fees: u64,
max_cu_when_batching: u32,
extra_jobs: Vec<JoinHandle<()>>,
) -> Result<(), anyhow::Error> {
let handles1 = mango_client
.context
@ -138,12 +158,18 @@ pub async fn runner(
futures::future::join_all(handles1),
futures::future::join_all(handles2),
futures::future::join_all(handles3),
loop_charge_collateral_fees(
mango_client.clone(),
interval_charge_collateral_fees,
max_cu_when_batching
),
MangoClient::loop_check_for_context_changes_and_abort(
mango_client.clone(),
Duration::from_secs(interval_check_for_changes_and_abort),
),
serve_metrics(),
debugging_handle,
futures::future::join_all(extra_jobs),
);
Ok(())
@ -409,3 +435,146 @@ pub async fn loop_update_funding(
}
}
}
pub async fn loop_charge_collateral_fees(
mango_client: Arc<MangoClient>,
interval: u64,
max_cu_when_batching: u32,
) {
if interval == 0 {
return;
}
// Make a new one separate from the mango_client.account_fetcher,
// because we don't want cached responses
let fetcher = RpcAccountFetcher {
rpc: mango_client.client.new_rpc_async(),
};
let group: Group = account_fetcher_fetch_anchor_account(&fetcher, &mango_client.context.group)
.await
.unwrap();
let collateral_fee_interval = group.collateral_fee_interval;
let mut interval = mango_v4_client::delay_interval(Duration::from_secs(interval));
loop {
interval.tick().await;
match charge_collateral_fees_inner(
&mango_client,
&fetcher,
collateral_fee_interval,
max_cu_when_batching,
)
.await
{
Ok(()) => {}
Err(err) => {
error!("charge_collateral_fees error: {err:?}");
}
}
}
}
async fn charge_collateral_fees_inner(
client: &MangoClient,
fetcher: &RpcAccountFetcher,
collateral_fee_interval: u64,
max_cu_when_batching: u32,
) -> anyhow::Result<()> {
let mango_accounts = fetcher
.fetch_program_accounts(&mango_v4::id(), MangoAccount::DISCRIMINATOR)
.await
.context("fetching mango accounts")?
.into_iter()
.filter_map(
|(pk, data)| match MangoAccountValue::from_bytes(&data.data()[8..]) {
Ok(acc) => Some((pk, acc)),
Err(err) => {
error!(pk=%pk, "charge_collateral_fees could not parse account: {err:?}");
None
}
},
);
let mut ix_to_send = Vec::new();
let now_ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as u64;
for (pk, account) in mango_accounts {
let should_reset =
collateral_fee_interval == 0 && account.fixed.last_collateral_fee_charge > 0;
let should_charge = collateral_fee_interval > 0
&& now_ts > account.fixed.last_collateral_fee_charge + collateral_fee_interval;
if !(should_reset || should_charge) {
continue;
}
let ixs = match client
.token_charge_collateral_fees_instruction((&pk, &account))
.await
{
Ok(ixs) => ixs,
Err(err) => {
error!(pk=%pk, "charge_collateral_fees could not build instruction: {err:?}");
continue;
}
};
ix_to_send.push(ixs);
}
let txsigs = send_batched_log_errors_no_confirm(
client.transaction_builder().await?,
&client.client,
&ix_to_send,
max_cu_when_batching,
)
.await;
info!("charge collateral fees: {:?}", txsigs);
Ok(())
}
/// Try to batch the instructions into transactions and send them
async fn send_batched_log_errors_no_confirm(
mut tx_builder: TransactionBuilder,
client: &mango_v4_client::Client,
ixs_list: &[PreparedInstructions],
max_cu: u32,
) -> Vec<Signature> {
let mut txsigs = Vec::new();
let mut current_batch = PreparedInstructions::new();
for ixs in ixs_list {
let previous_batch = current_batch.clone();
current_batch.append(ixs.clone());
tx_builder.instructions = current_batch.clone().to_instructions();
if tx_builder
.transaction_size()
.map(|ts| !ts.is_within_limit())
.unwrap_or(true)
|| current_batch.cu > max_cu
{
tx_builder.instructions = previous_batch.to_instructions();
match tx_builder.send(client).await {
Err(err) => error!("could not send transaction: {err:?}"),
Ok(txsig) => txsigs.push(txsig),
}
current_batch = ixs.clone();
}
}
if !current_batch.is_empty() {
tx_builder.instructions = current_batch.to_instructions();
match tx_builder.send(client).await {
Err(err) => error!("could not send transaction: {err:?}"),
Ok(txsig) => txsigs.push(txsig),
}
}
txsigs
}

View File

@ -7,7 +7,10 @@ use std::time::Duration;
use anchor_client::Cluster;
use clap::{Parser, Subcommand};
use mango_v4_client::{keypair_from_cli, Client, MangoClient, TransactionBuilderConfig};
use mango_v4_client::{
keypair_from_cli, priority_fees_cli, Client, FallbackOracleConfig, MangoClient,
TransactionBuilderConfig,
};
use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::pubkey::Pubkey;
use tokio::time;
@ -58,12 +61,23 @@ struct Cli {
#[clap(long, env, default_value_t = 120)]
interval_check_new_listings_and_abort: u64,
#[clap(long, env, default_value_t = 300)]
interval_charge_collateral_fees: u64,
#[clap(long, env, default_value_t = 10)]
timeout: u64,
/// prioritize each transaction with this many microlamports/cu
#[clap(long, env, default_value = "0")]
prioritization_micro_lamports: u64,
#[clap(flatten)]
prioritization_fee_cli: priority_fees_cli::PriorityFeeArgs,
/// url to the lite-rpc websocket, optional
#[clap(long, env, default_value = "")]
lite_rpc_url: String,
/// When batching multiple instructions into a transaction, don't exceed
/// this compute unit limit.
#[clap(long, env, default_value_t = 1_000_000)]
max_cu_when_batching: u32,
}
#[derive(Subcommand, Debug, Clone)]
@ -85,6 +99,10 @@ async fn main() -> Result<(), anyhow::Error> {
};
let cli = Cli::parse_from(args);
let (prio_provider, prio_jobs) = cli
.prioritization_fee_cli
.make_prio_provider(cli.lite_rpc_url.clone())?;
let owner = Arc::new(keypair_from_cli(&cli.owner));
let rpc_url = cli.rpc_url;
@ -98,19 +116,23 @@ async fn main() -> Result<(), anyhow::Error> {
let mango_client = Arc::new(
MangoClient::new_for_existing_account(
Client::new(
cluster,
commitment,
owner.clone(),
Some(Duration::from_secs(cli.timeout)),
TransactionBuilderConfig {
prioritization_micro_lamports: (cli.prioritization_micro_lamports > 0)
.then_some(cli.prioritization_micro_lamports),
compute_budget_per_instruction: None,
},
),
Client::builder()
.cluster(cluster)
.commitment(commitment)
.fee_payer(Some(owner.clone()))
.timeout(Duration::from_secs(cli.timeout))
.transaction_builder_config(
TransactionBuilderConfig::builder()
.priority_fee_provider(prio_provider)
.compute_budget_per_instruction(None)
.build()
.unwrap(),
)
.fallback_oracle_config(FallbackOracleConfig::Never)
.build()
.unwrap(),
cli.mango_account,
owner.clone(),
owner,
)
.await?,
);
@ -139,12 +161,15 @@ async fn main() -> Result<(), anyhow::Error> {
cli.interval_consume_events,
cli.interval_update_funding,
cli.interval_check_new_listings_and_abort,
cli.interval_charge_collateral_fees,
cli.max_cu_when_batching,
prio_jobs,
)
.await
}
Command::Taker { .. } => {
let client = mango_client.clone();
taker::runner(client, debugging_handle).await
taker::runner(client, debugging_handle, prio_jobs).await
}
}
}

View File

@ -10,13 +10,15 @@ use mango_v4::{
accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side},
state::TokenIndex,
};
use tokio::task::JoinHandle;
use tracing::*;
use crate::MangoClient;
pub async fn runner(
mango_client: Arc<MangoClient>,
_debugging_handle: impl Future,
debugging_handle: impl Future,
extra_jobs: Vec<JoinHandle<()>>,
) -> Result<(), anyhow::Error> {
ensure_deposit(&mango_client).await?;
ensure_oo(&mango_client).await?;
@ -53,7 +55,9 @@ pub async fn runner(
futures::join!(
futures::future::join_all(handles1),
futures::future::join_all(handles2)
futures::future::join_all(handles2),
debugging_handle,
futures::future::join_all(extra_jobs),
);
Ok(())

View File

@ -48,3 +48,4 @@ tokio = { version = "1", features = ["full"] }
tokio-stream = { version = "0.1.9"}
tokio-tungstenite = "0.16.1"
tracing = "0.1"
regex = "1.9.5"

View File

@ -0,0 +1,211 @@
use crate::trigger_tcs;
use anchor_lang::prelude::Pubkey;
use clap::Parser;
use mango_v4_client::{jupiter, priority_fees_cli};
use std::collections::HashSet;
#[derive(Parser, Debug)]
#[clap()]
pub(crate) struct CliDotenv {
// When --dotenv <file> is passed, read the specified dotenv file before parsing args
#[clap(long)]
pub(crate) dotenv: std::path::PathBuf,
pub(crate) 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)]
pub(crate) enum BoolArg {
True,
False,
}
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum JupiterVersionArg {
Mock,
V6,
}
impl From<JupiterVersionArg> for jupiter::Version {
fn from(a: JupiterVersionArg) -> Self {
match a {
JupiterVersionArg::Mock => jupiter::Version::Mock,
JupiterVersionArg::V6 => jupiter::Version::V6,
}
}
}
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum TcsMode {
BorrowBuy,
SwapSellIntoBuy,
SwapCollateralIntoBuy,
}
impl From<TcsMode> for trigger_tcs::Mode {
fn from(a: TcsMode) -> Self {
match a {
TcsMode::BorrowBuy => trigger_tcs::Mode::BorrowBuyToken,
TcsMode::SwapSellIntoBuy => trigger_tcs::Mode::SwapSellIntoBuy,
TcsMode::SwapCollateralIntoBuy => trigger_tcs::Mode::SwapCollateralIntoBuy,
}
}
}
pub(crate) fn cli_to_hashset<T: Eq + std::hash::Hash + From<u16>>(
str_list: Option<Vec<u16>>,
) -> HashSet<T> {
return str_list
.map(|v| v.iter().map(|x| T::from(*x)).collect::<HashSet<T>>())
.unwrap_or_default();
}
#[derive(Parser)]
#[clap()]
pub struct Cli {
#[clap(short, long, env)]
pub(crate) rpc_url: String,
#[clap(long, env, value_delimiter = ';')]
pub(crate) override_send_transaction_url: Option<Vec<String>>,
#[clap(long, env)]
pub(crate) liqor_mango_account: Pubkey,
#[clap(long, env)]
pub(crate) liqor_owner: String,
#[clap(long, env, default_value = "1000")]
pub(crate) check_interval_ms: u64,
#[clap(long, env, default_value = "300")]
pub(crate) snapshot_interval_secs: u64,
// how often do we refresh token swap route/prices
#[clap(long, env, default_value = "30")]
pub(crate) token_swap_refresh_interval_secs: u64,
/// how many getMultipleAccounts requests to send in parallel
#[clap(long, env, default_value = "10")]
pub(crate) parallel_rpc_requests: usize,
/// typically 100 is the max number of accounts getMultipleAccounts will retrieve at once
#[clap(long, env, default_value = "100")]
pub(crate) get_multiple_accounts_count: usize,
/// liquidator health ratio should not fall below this value
#[clap(long, env, default_value = "50")]
pub(crate) 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")]
pub(crate) rebalance: BoolArg,
/// max slippage to request on swaps to rebalance spot tokens
#[clap(long, env, default_value = "100")]
pub(crate) rebalance_slippage_bps: u64,
/// tokens to not rebalance (in addition to USDC=0); use a comma separated list of token index
#[clap(long, env, value_parser, value_delimiter = ',')]
pub(crate) rebalance_skip_tokens: Option<Vec<u16>>,
/// When closing borrows, the rebalancer can't close token positions exactly.
/// Instead it purchases too much and then gets rid of the excess in a second step.
/// If this is 0.05, then it'll swap borrow_value * (1 + 0.05) quote token into borrow token.
#[clap(long, env, default_value = "0.05")]
pub(crate) rebalance_borrow_settle_excess: f64,
#[clap(long, env, default_value = "30")]
pub(crate) rebalance_refresh_timeout_secs: u64,
/// if taking tcs orders is enabled
///
/// typically only disabled for tests where swaps are unavailable
#[clap(long, env, value_enum, default_value = "true")]
pub(crate) take_tcs: BoolArg,
/// profit margin at which to take tcs orders
#[clap(long, env, default_value = "0.0005")]
pub(crate) tcs_profit_fraction: f64,
/// control how tcs triggering provides buy tokens
#[clap(long, env, value_enum, default_value = "swap-sell-into-buy")]
pub(crate) tcs_mode: TcsMode,
/// largest tcs amount to trigger in one transaction, in dollar
#[clap(long, env, default_value = "1000.0")]
pub(crate) tcs_max_trigger_amount: f64,
/// Minimum fraction of max_buy to buy for success when triggering,
/// useful in conjunction with jupiter swaps in same tx to avoid over-buying.
///
/// Can be set to 0 to allow executions of any size.
#[clap(long, env, default_value = "0.7")]
pub(crate) tcs_min_buy_fraction: f64,
#[clap(flatten)]
pub(crate) prioritization_fee_cli: priority_fees_cli::PriorityFeeArgs,
/// url to the lite-rpc websocket, optional
#[clap(long, env, default_value = "")]
pub(crate) lite_rpc_url: String,
/// compute limit requested for liquidation instructions
#[clap(long, env, default_value = "250000")]
pub(crate) compute_limit_for_liquidation: u32,
/// compute limit requested for tcs trigger instructions
#[clap(long, env, default_value = "300000")]
pub(crate) compute_limit_for_tcs: u32,
/// control which version of jupiter to use
#[clap(long, env, value_enum, default_value = "v6")]
pub(crate) jupiter_version: JupiterVersionArg,
/// override the url to jupiter v6
#[clap(long, env, default_value = "https://quote-api.jup.ag/v6")]
pub(crate) jupiter_v6_url: String,
/// provide a jupiter token, currently only for jup v6
#[clap(long, env, default_value = "")]
pub(crate) jupiter_token: String,
/// size of the swap to quote via jupiter to get slippage info, in dollar
/// should be larger than tcs_max_trigger_amount
#[clap(long, env, default_value = "1000.0")]
pub(crate) jupiter_swap_info_amount: f64,
/// report liquidator's existence and pubkey
#[clap(long, env, value_enum, default_value = "true")]
pub(crate) telemetry: BoolArg,
/// liquidation refresh timeout in secs
#[clap(long, env, default_value = "30")]
pub(crate) liquidation_refresh_timeout_secs: u8,
/// tokens to exclude for liquidation/tcs (never liquidate any pair where base or quote is in this list)
#[clap(long, env, value_parser, value_delimiter = ' ')]
pub(crate) forbidden_tokens: Option<Vec<u16>>,
/// tokens to allow for liquidation/tcs (only liquidate a pair if base or quote is in this list)
/// when empty, allows all pairs
#[clap(long, env, value_parser, value_delimiter = ' ')]
pub(crate) only_allow_tokens: Option<Vec<u16>>,
/// perp market to exclude for liquidation
#[clap(long, env, value_parser, value_delimiter = ' ')]
pub(crate) liquidation_forbidden_perp_markets: Option<Vec<u16>>,
/// perp market to allow for liquidation (only liquidate if is in this list)
/// when empty, allows all pairs
#[clap(long, env, value_parser, value_delimiter = ' ')]
pub(crate) liquidation_only_allow_perp_markets: Option<Vec<u16>>,
/// how long should it wait before logging an oracle error again (for the same token)
#[clap(long, env, default_value = "30")]
pub(crate) skip_oracle_error_in_logs_duration_secs: u64,
}

View File

@ -1,10 +1,11 @@
use std::cmp::Reverse;
use std::collections::HashSet;
use std::time::Duration;
use itertools::Itertools;
use mango_v4::health::{HealthCache, HealthType};
use mango_v4::state::{MangoAccountValue, PerpMarketIndex, Side, TokenIndex, QUOTE_TOKEN_INDEX};
use mango_v4_client::{chain_data, health_cache, MangoClient};
use mango_v4_client::{chain_data, MangoClient, PreparedInstructions};
use solana_sdk::signature::Signature;
use futures::{stream, StreamExt, TryStreamExt};
@ -19,6 +20,16 @@ pub struct Config {
pub min_health_ratio: f64,
pub refresh_timeout: Duration,
pub compute_limit_for_liq_ix: u32,
pub only_allowed_tokens: HashSet<TokenIndex>,
pub forbidden_tokens: HashSet<TokenIndex>,
pub only_allowed_perp_markets: HashSet<PerpMarketIndex>,
pub forbidden_perp_markets: HashSet<PerpMarketIndex>,
/// If we cram multiple ix into a transaction, don't exceed this level
/// of expected-cu.
pub max_cu_per_transaction: u32,
}
struct LiquidateHelper<'a> {
@ -29,8 +40,6 @@ struct LiquidateHelper<'a> {
health_cache: &'a HealthCache,
maint_health: I80F48,
liqor_min_health_ratio: I80F48,
allowed_asset_tokens: HashSet<Pubkey>,
allowed_liab_tokens: HashSet<Pubkey>,
config: Config,
}
@ -46,7 +55,7 @@ impl<'a> LiquidateHelper<'a> {
Ok((*orders, *open_orders))
})
.try_collect();
let serum_force_cancels = serum_oos?
let mut serum_force_cancels = serum_oos?
.into_iter()
.filter_map(|(orders, open_orders)| {
let can_force_cancel = open_orders.native_coin_total > 0
@ -62,18 +71,42 @@ impl<'a> LiquidateHelper<'a> {
if serum_force_cancels.is_empty() {
return Ok(None);
}
// Cancel all orders on a random serum market
let serum_orders = serum_force_cancels.choose(&mut rand::thread_rng()).unwrap();
let txsig = self
.client
.serum3_liq_force_cancel_orders(
(self.pubkey, self.liqee),
serum_orders.market_index,
&serum_orders.open_orders,
)
.await?;
serum_force_cancels.shuffle(&mut rand::thread_rng());
let mut ixs = PreparedInstructions::new();
let mut cancelled_markets = vec![];
let mut tx_builder = self.client.transaction_builder().await?;
for force_cancel in serum_force_cancels {
let mut new_ixs = ixs.clone();
new_ixs.append(
self.client
.serum3_liq_force_cancel_orders_instruction(
(self.pubkey, self.liqee),
force_cancel.market_index,
&force_cancel.open_orders,
)
.await?,
);
let exceeds_cu_limit = new_ixs.cu > self.config.max_cu_per_transaction;
let exceeds_size_limit = {
tx_builder.instructions = new_ixs.clone().to_instructions();
!tx_builder.transaction_size()?.is_within_limit()
};
if exceeds_cu_limit || exceeds_size_limit {
break;
}
ixs = new_ixs;
cancelled_markets.push(force_cancel.market_index);
}
tx_builder.instructions = ixs.to_instructions();
let txsig = tx_builder.send_and_confirm(&self.client.client).await?;
info!(
market_index = serum_orders.market_index,
market_indexes = ?cancelled_markets,
%txsig,
"Force cancelled serum orders",
);
@ -108,6 +141,25 @@ impl<'a> LiquidateHelper<'a> {
let all_perp_base_positions: anyhow::Result<
Vec<Option<(PerpMarketIndex, i64, I80F48, I80F48)>>,
> = stream::iter(self.liqee.active_perp_positions())
.filter(|pp| async {
if self
.config
.forbidden_perp_markets
.contains(&pp.market_index)
{
return false;
}
if !self.config.only_allowed_perp_markets.is_empty()
&& !self
.config
.only_allowed_perp_markets
.contains(&pp.market_index)
{
return false;
}
true
})
.then(|pp| async {
let base_lots = pp.base_position_lots();
if (base_lots == 0 && pp.quote_position_native() <= 0) || pp.has_open_taker_fills()
@ -155,10 +207,7 @@ impl<'a> LiquidateHelper<'a> {
.await
.context("getting liquidator account")?;
liqor.ensure_perp_position(*perp_market_index, QUOTE_TOKEN_INDEX)?;
let mut health_cache =
health_cache::new(&self.client.context, self.account_fetcher, &liqor)
.await
.context("health cache")?;
let mut health_cache = self.client.health_cache(&liqor).await.expect("always ok");
let quote_bank = self
.client
.first_bank(QUOTE_TOKEN_INDEX)
@ -328,6 +377,7 @@ impl<'a> LiquidateHelper<'a> {
.health_cache
.token_infos
.iter()
.filter(|p| !self.config.forbidden_tokens.contains(&p.token_index))
.zip(
self.health_cache
.effective_token_balances(HealthType::LiquidationEnd)
@ -345,34 +395,17 @@ impl<'a> LiquidateHelper<'a> {
.filter_map(|(ti, effective)| {
// check constraints for liquidatable assets, see also has_possible_spot_liquidations()
let tokens = ti.balance_spot.min(effective.spot_and_perp);
let is_valid_asset = tokens >= 1;
let is_valid_asset = tokens >= 1 && ti.allow_asset_liquidation;
let quote_value = tokens * ti.prices.oracle;
// prefer to liquidate tokens with asset weight that have >$1 liquidatable
let is_preferred =
ti.init_asset_weight > 0 && quote_value > I80F48::from(1_000_000);
ti.maint_asset_weight > 0 && quote_value > I80F48::from(1_000_000);
is_valid_asset.then_some((ti.token_index, is_preferred, quote_value))
})
.collect_vec();
// sort such that preferred tokens are at the end, and the one with the larget quote value is
// at the very end
potential_assets.sort_by_key(|(_, is_preferred, amount)| (*is_preferred, *amount));
// filter only allowed assets
let potential_allowed_assets = potential_assets.iter().filter_map(|(ti, _, _)| {
let is_allowed = self
.allowed_asset_tokens
.contains(&self.client.context.token(*ti).mint);
is_allowed.then_some(*ti)
});
let asset_token_index = match potential_allowed_assets.last() {
Some(token_index) => token_index,
None => anyhow::bail!(
"mango account {}, has no allowed asset tokens that are liquidatable: {:?}",
self.pubkey,
potential_assets,
),
};
// sort such that preferred tokens are at the start, and the one with the larget quote value is
// at 0
potential_assets.sort_by_key(|(_, is_preferred, amount)| Reverse((*is_preferred, *amount)));
//
// find a good liab, same as for assets
@ -385,29 +418,69 @@ impl<'a> LiquidateHelper<'a> {
let tokens = (-ti.balance_spot).min(-effective.spot_and_perp);
let is_valid_liab = tokens > 0;
let quote_value = tokens * ti.prices.oracle;
is_valid_liab.then_some((ti.token_index, quote_value))
is_valid_liab.then_some((ti.token_index, false, quote_value))
})
.collect_vec();
// largest liquidatable liability at the end
potential_liabs.sort_by_key(|(_, amount)| *amount);
// largest liquidatable liability at the start
potential_liabs.sort_by_key(|(_, is_preferred, amount)| Reverse((*is_preferred, *amount)));
// filter only allowed liabs
let potential_allowed_liabs = potential_liabs.iter().filter_map(|(ti, _)| {
let is_allowed = self
.allowed_liab_tokens
.contains(&self.client.context.token(*ti).mint);
is_allowed.then_some(*ti)
});
//
// Find a pair
//
let liab_token_index = match potential_allowed_liabs.last() {
Some(token_index) => token_index,
None => anyhow::bail!(
"mango account {}, has no liab tokens that are liquidatable: {:?}",
fn find_best_token(
lh: &LiquidateHelper,
token_list: &Vec<(TokenIndex, bool, I80F48)>,
) -> (Option<TokenIndex>, Option<TokenIndex>) {
let mut best_whitelisted = None;
let mut best = None;
let allowed_token_list = token_list
.iter()
.filter_map(|(ti, _, _)| (!lh.config.forbidden_tokens.contains(ti)).then_some(ti));
for ti in allowed_token_list {
let whitelisted = lh.config.only_allowed_tokens.is_empty()
|| lh.config.only_allowed_tokens.contains(ti);
if best.is_none() {
best = Some(*ti);
}
if best_whitelisted.is_none() && whitelisted {
best_whitelisted = Some(*ti);
break;
}
}
return (best, best_whitelisted);
}
let (best_asset, best_whitelisted_asset) = find_best_token(self, &potential_assets);
let (best_liab, best_whitelisted_liab) = find_best_token(self, &potential_liabs);
let best_pair_opt = [
(best_whitelisted_asset, best_liab),
(best_asset, best_whitelisted_liab),
]
.iter()
.filter_map(|(a, l)| (a.is_some() && l.is_some()).then_some((a.unwrap(), l.unwrap())))
.next();
if best_pair_opt.is_none() {
anyhow::bail!(
"mango account {}, has no allowed asset/liab tokens pair that are liquidatable: assets={:?}; liabs={:?}",
self.pubkey,
potential_assets,
potential_liabs,
),
)
};
let (asset_token_index, liab_token_index) = best_pair_opt.unwrap();
//
// Compute max transfer size
//
let max_liab_transfer = self
.max_token_liab_transfer(liab_token_index, asset_token_index)
.await
@ -459,9 +532,7 @@ impl<'a> LiquidateHelper<'a> {
.iter()
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
liab_usdc_equivalent.is_negative()
&& self
.allowed_liab_tokens
.contains(&self.client.context.token(*liab_token_index).mint)
&& !self.config.forbidden_tokens.contains(liab_token_index)
})
.ok_or_else(|| {
anyhow::anyhow!(
@ -589,7 +660,8 @@ pub async fn maybe_liquidate_account(
let liqor_min_health_ratio = I80F48::from_num(config.min_health_ratio);
let account = account_fetcher.fetch_mango_account(pubkey)?;
let health_cache = health_cache::new(&mango_client.context, account_fetcher, &account)
let health_cache = mango_client
.health_cache(&account)
.await
.context("creating health cache 1")?;
let maint_health = health_cache.health(HealthType::Maint);
@ -607,7 +679,8 @@ pub async fn maybe_liquidate_account(
// This is -- unfortunately -- needed because the websocket streams seem to not
// be great at providing timely updates to the account data.
let account = account_fetcher.fetch_fresh_mango_account(pubkey).await?;
let health_cache = health_cache::new(&mango_client.context, account_fetcher, &account)
let health_cache = mango_client
.health_cache(&account)
.await
.context("creating health cache 2")?;
if !health_cache.is_liquidatable() {
@ -616,8 +689,6 @@ pub async fn maybe_liquidate_account(
let maint_health = health_cache.health(HealthType::Maint);
let all_token_mints = HashSet::from_iter(mango_client.context.tokens.values().map(|c| c.mint));
// try liquidating
let maybe_txsig = LiquidateHelper {
client: mango_client,
@ -627,8 +698,6 @@ pub async fn maybe_liquidate_account(
health_cache: &health_cache,
maint_health,
liqor_min_health_ratio,
allowed_asset_tokens: all_token_mints.clone(),
allowed_liab_tokens: all_token_mints,
config: config.clone(),
}
.send_liq_tx()

View File

@ -9,7 +9,7 @@ use clap::Parser;
use mango_v4::state::{PerpMarketIndex, TokenIndex};
use mango_v4_client::AsyncChannelSendUnlessFull;
use mango_v4_client::{
account_update_stream, chain_data, error_tracking::ErrorTracking, jupiter, keypair_from_cli,
account_update_stream, chain_data, error_tracking::ErrorTracking, keypair_from_cli,
snapshot_source, websocket_source, Client, MangoClient, MangoClientError, MangoGroupContext,
TransactionBuilderConfig,
};
@ -20,14 +20,17 @@ use solana_sdk::pubkey::Pubkey;
use solana_sdk::signer::Signer;
use tracing::*;
pub mod cli_args;
pub mod liquidate;
pub mod metrics;
pub mod rebalance;
pub mod telemetry;
pub mod token_swap_info;
pub mod trigger_tcs;
mod unwrappable_oracle_error;
pub mod util;
use crate::unwrappable_oracle_error::UnwrappableOracleError;
use crate::util::{is_mango_account, is_mint_info, is_perp_market};
// jemalloc seems to be better at keeping the memory footprint reasonable over
@ -35,149 +38,6 @@ use crate::util::{is_mango_account, is_mint_info, is_perp_market};
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
#[derive(Parser, Debug)]
#[clap()]
struct CliDotenv {
// When --dotenv <file> is passed, read the specified dotenv file before parsing args
#[clap(long)]
dotenv: std::path::PathBuf,
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(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
enum JupiterVersionArg {
Mock,
V4,
V6,
}
impl From<JupiterVersionArg> for jupiter::Version {
fn from(a: JupiterVersionArg) -> Self {
match a {
JupiterVersionArg::Mock => jupiter::Version::Mock,
JupiterVersionArg::V4 => jupiter::Version::V4,
JupiterVersionArg::V6 => jupiter::Version::V6,
}
}
}
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
enum TcsMode {
BorrowBuy,
SwapSellIntoBuy,
SwapCollateralIntoBuy,
}
impl From<TcsMode> for trigger_tcs::Mode {
fn from(a: TcsMode) -> Self {
match a {
TcsMode::BorrowBuy => trigger_tcs::Mode::BorrowBuyToken,
TcsMode::SwapSellIntoBuy => trigger_tcs::Mode::SwapSellIntoBuy,
TcsMode::SwapCollateralIntoBuy => trigger_tcs::Mode::SwapCollateralIntoBuy,
}
}
}
#[derive(Parser)]
#[clap()]
struct Cli {
#[clap(short, long, env)]
rpc_url: String,
#[clap(long, env)]
liqor_mango_account: Pubkey,
#[clap(long, env)]
liqor_owner: String,
#[clap(long, env, default_value = "1000")]
check_interval_ms: u64,
#[clap(long, env, default_value = "300")]
snapshot_interval_secs: u64,
/// how many getMultipleAccounts requests to send in parallel
#[clap(long, env, default_value = "10")]
parallel_rpc_requests: usize,
/// typically 100 is the max number of accounts getMultipleAccounts will retrieve at once
#[clap(long, env, default_value = "100")]
get_multiple_accounts_count: usize,
/// liquidator health ratio should not fall below this value
#[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,
/// tokens to not rebalance (in addition to USDC); use a comma separated list of names
#[clap(long, env, default_value = "")]
rebalance_skip_tokens: String,
/// if taking tcs orders is enabled
///
/// typically only disabled for tests where swaps are unavailable
#[clap(long, env, value_enum, default_value = "true")]
take_tcs: BoolArg,
/// profit margin at which to take tcs orders
#[clap(long, env, default_value = "0.0005")]
tcs_profit_fraction: f64,
/// control how tcs triggering provides buy tokens
#[clap(long, env, value_enum, default_value = "swap-sell-into-buy")]
tcs_mode: TcsMode,
/// prioritize each transaction with this many microlamports/cu
#[clap(long, env, default_value = "0")]
prioritization_micro_lamports: u64,
/// compute limit requested for liquidation instructions
#[clap(long, env, default_value = "250000")]
compute_limit_for_liquidation: u32,
/// compute limit requested for tcs trigger instructions
#[clap(long, env, default_value = "300000")]
compute_limit_for_tcs: u32,
/// control which version of jupiter to use
#[clap(long, env, value_enum, default_value = "v6")]
jupiter_version: JupiterVersionArg,
/// override the url to jupiter v4
#[clap(long, env, default_value = "https://quote-api.jup.ag/v4")]
jupiter_v4_url: String,
/// override the url to jupiter v6
#[clap(long, env, default_value = "https://quote-api.jup.ag/v6")]
jupiter_v6_url: String,
/// provide a jupiter token, currently only for jup v6
#[clap(long, env, default_value = "")]
jupiter_token: String,
/// report liquidator's existence and pubkey
#[clap(long, env, value_enum, default_value = "true")]
telemetry: BoolArg,
}
pub fn encode_address(addr: &Pubkey) -> String {
bs58::encode(&addr.to_bytes()).into_string()
}
@ -186,20 +46,31 @@ pub fn encode_address(addr: &Pubkey) -> String {
async fn main() -> anyhow::Result<()> {
mango_v4_client::tracing_subscriber_init();
let args = if let Ok(cli_dotenv) = CliDotenv::try_parse() {
let args: Vec<std::ffi::OsString> = if let Ok(cli_dotenv) = CliDotenv::try_parse() {
dotenv::from_path(cli_dotenv.dotenv)?;
cli_dotenv.remaining_args
std::env::args_os()
.take(1)
.chain(cli_dotenv.remaining_args.into_iter())
.collect()
} else {
dotenv::dotenv().ok();
std::env::args_os().collect()
};
let cli = Cli::parse_from(args);
let liqor_owner = Arc::new(keypair_from_cli(&cli.liqor_owner));
//
// Priority fee setup
//
let (prio_provider, prio_jobs) = cli
.prioritization_fee_cli
.make_prio_provider(cli.lite_rpc_url.clone())?;
//
// Client setup
//
let liqor_owner = Arc::new(keypair_from_cli(&cli.liqor_owner));
let rpc_url = cli.rpc_url;
let ws_url = rpc_url.replace("https", "wss");
let rpc_timeout = Duration::from_secs(10);
let cluster = Cluster::Custom(rpc_url.clone(), ws_url.clone());
let commitment = CommitmentConfig::processed();
@ -207,16 +78,18 @@ async fn main() -> anyhow::Result<()> {
.cluster(cluster.clone())
.commitment(commitment)
.fee_payer(Some(liqor_owner.clone()))
.timeout(Some(rpc_timeout))
.jupiter_v4_url(cli.jupiter_v4_url)
.timeout(rpc_timeout)
.jupiter_v6_url(cli.jupiter_v6_url)
.jupiter_token(cli.jupiter_token)
.transaction_builder_config(TransactionBuilderConfig {
prioritization_micro_lamports: (cli.prioritization_micro_lamports > 0)
.then_some(cli.prioritization_micro_lamports),
// Liquidation and tcs triggers set their own budgets, this is a default for other tx
compute_budget_per_instruction: Some(250_000),
})
.transaction_builder_config(
TransactionBuilderConfig::builder()
.priority_fee_provider(prio_provider)
// Liquidation and tcs triggers set their own budgets, this is a default for other tx
.compute_budget_per_instruction(Some(250_000))
.build()
.unwrap(),
)
.override_send_transaction_urls(cli.override_send_transaction_url)
.build()
.unwrap();
@ -225,7 +98,7 @@ async fn main() -> anyhow::Result<()> {
// Reading accounts from chain_data
let account_fetcher = Arc::new(chain_data::AccountFetcher {
chain_data: chain_data.clone(),
rpc: client.rpc_async(),
rpc: client.new_rpc_async(),
});
let mango_account = account_fetcher
@ -238,7 +111,7 @@ async fn main() -> anyhow::Result<()> {
warn!("rebalancing on delegated accounts will be unable to free token positions reliably, withdraw dust manually");
}
let group_context = MangoGroupContext::new_from_rpc(&client.rpc_async(), mango_group).await?;
let group_context = MangoGroupContext::new_from_rpc(client.rpc_async(), mango_group).await?;
let mango_oracles = group_context
.tokens
@ -319,8 +192,8 @@ 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
quote_index: 0, // USDC
quote_amount: (cli.jupiter_swap_info_amount * 1e6) as u64,
jupiter_version: cli.jupiter_version.into(),
};
@ -332,24 +205,33 @@ async fn main() -> anyhow::Result<()> {
let liq_config = liquidate::Config {
min_health_ratio: cli.min_health_ratio,
compute_limit_for_liq_ix: cli.compute_limit_for_liquidation,
// TODO: config
refresh_timeout: Duration::from_secs(30),
max_cu_per_transaction: 1_000_000,
refresh_timeout: Duration::from_secs(cli.liquidation_refresh_timeout_secs as u64),
only_allowed_tokens: cli_args::cli_to_hashset::<TokenIndex>(cli.only_allow_tokens),
forbidden_tokens: cli_args::cli_to_hashset::<TokenIndex>(cli.forbidden_tokens),
only_allowed_perp_markets: cli_args::cli_to_hashset::<PerpMarketIndex>(
cli.liquidation_only_allow_perp_markets,
),
forbidden_perp_markets: cli_args::cli_to_hashset::<PerpMarketIndex>(
cli.liquidation_forbidden_perp_markets,
),
};
let tcs_config = trigger_tcs::Config {
min_health_ratio: cli.min_health_ratio,
max_trigger_quote_amount: 1_000_000_000, // TODO: config, $1000
max_trigger_quote_amount: (cli.tcs_max_trigger_amount * 1e6) as u64,
compute_limit_for_trigger: cli.compute_limit_for_tcs,
profit_fraction: cli.tcs_profit_fraction,
collateral_token_index: 0, // USDC
// TODO: config
refresh_timeout: Duration::from_secs(30),
jupiter_version: cli.jupiter_version.into(),
jupiter_slippage_bps: cli.rebalance_slippage_bps,
mode: cli.tcs_mode.into(),
min_buy_fraction: 0.7,
min_buy_fraction: cli.tcs_min_buy_fraction,
only_allowed_tokens: liq_config.only_allowed_tokens.clone(),
forbidden_tokens: liq_config.forbidden_tokens.clone(),
};
let mut rebalance_interval = tokio::time::interval(Duration::from_secs(30));
@ -357,16 +239,10 @@ async fn main() -> anyhow::Result<()> {
let rebalance_config = rebalance::Config {
enabled: cli.rebalance == BoolArg::True,
slippage_bps: cli.rebalance_slippage_bps,
// TODO: config
borrow_settle_excess: 1.05,
refresh_timeout: Duration::from_secs(30),
borrow_settle_excess: (1f64 + cli.rebalance_borrow_settle_excess).max(1f64),
refresh_timeout: Duration::from_secs(cli.rebalance_refresh_timeout_secs),
jupiter_version: cli.jupiter_version.into(),
skip_tokens: cli
.rebalance_skip_tokens
.split(',')
.filter(|v| !v.is_empty())
.map(|name| mango_client.context.token_by_name(name).token_index)
.collect(),
skip_tokens: cli.rebalance_skip_tokens.unwrap_or(Vec::new()),
allow_withdraws: signer_is_owner,
};
@ -388,6 +264,12 @@ async fn main() -> anyhow::Result<()> {
.skip_threshold_for_type(LiqErrorType::Liq, 5)
.skip_duration(Duration::from_secs(120))
.build()?,
oracle_errors: ErrorTracking::builder()
.skip_threshold(1)
.skip_duration(Duration::from_secs(
cli.skip_oracle_error_in_logs_duration_secs,
))
.build()?,
});
info!("main loop");
@ -501,6 +383,7 @@ async fn main() -> anyhow::Result<()> {
};
liquidation.errors.update();
liquidation.oracle_errors.update();
let liquidated = liquidation
.maybe_liquidate_one(account_addresses.iter())
@ -508,16 +391,13 @@ async fn main() -> anyhow::Result<()> {
let mut took_tcs = false;
if !liquidated && cli.take_tcs == BoolArg::True {
took_tcs = match liquidation
took_tcs = liquidation
.maybe_take_token_conditional_swap(account_addresses.iter())
.await
{
Ok(v) => v,
Err(err) => {
.unwrap_or_else(|err| {
error!("error during maybe_take_token_conditional_swap: {err}");
false
}
}
})
}
if liquidated || took_tcs {
@ -528,14 +408,15 @@ async fn main() -> anyhow::Result<()> {
});
let token_swap_info_job = tokio::spawn({
// TODO: configurable interval
let mut interval = mango_v4_client::delay_interval(Duration::from_secs(60));
let mut interval = mango_v4_client::delay_interval(Duration::from_secs(
cli.token_swap_refresh_interval_secs,
));
let mut startup_wait = mango_v4_client::delay_interval(Duration::from_secs(1));
let shared_state = shared_state.clone();
async move {
loop {
startup_wait.tick().await;
if !shared_state.read().unwrap().one_snapshot_done {
startup_wait.tick().await;
continue;
}
@ -570,6 +451,7 @@ async fn main() -> anyhow::Result<()> {
));
}
use cli_args::{BoolArg, Cli, CliDotenv};
use futures::StreamExt;
let mut jobs: futures::stream::FuturesUnordered<_> = vec![
data_job,
@ -579,6 +461,7 @@ async fn main() -> anyhow::Result<()> {
check_changes_for_abort_job,
]
.into_iter()
.chain(prio_jobs.into_iter())
.collect();
jobs.next().await;
@ -625,6 +508,7 @@ struct LiquidationState {
trigger_tcs_config: trigger_tcs::Config,
errors: ErrorTracking<Pubkey, LiqErrorType>,
oracle_errors: ErrorTracking<TokenIndex, LiqErrorType>,
}
impl LiquidationState {
@ -678,6 +562,25 @@ impl LiquidationState {
.await;
if let Err(err) = result.as_ref() {
if let Some((ti, ti_name)) = err.try_unwrap_oracle_error() {
if self
.oracle_errors
.had_too_many_errors(LiqErrorType::Liq, &ti, Instant::now())
.is_none()
{
warn!(
"{:?} recording oracle error for token {} {}",
chrono::offset::Utc::now(),
ti_name,
ti
);
}
self.oracle_errors
.record(LiqErrorType::Liq, &ti, err.to_string());
return result;
}
// Keep track of pubkeys that had errors
error_tracking.record(LiqErrorType::Liq, pubkey, err.to_string());

View File

@ -151,18 +151,7 @@ impl Rebalancer {
let direct_sol_route_job =
self.jupiter_quote(sol_mint, output_mint, in_amount_sol, true, jupiter_version);
let mut jobs = vec![full_route_job, direct_quote_route_job, direct_sol_route_job];
// for v6, add a v4 fallback
if self.config.jupiter_version == jupiter::Version::V6 {
jobs.push(self.jupiter_quote(
quote_mint,
output_mint,
in_amount_quote,
false,
jupiter::Version::V4,
));
}
let jobs = vec![full_route_job, direct_quote_route_job, direct_sol_route_job];
let mut results = futures::future::join_all(jobs).await;
let full_route = results.remove(0)?;
@ -211,18 +200,7 @@ impl Rebalancer {
let direct_sol_route_job =
self.jupiter_quote(input_mint, sol_mint, in_amount, true, jupiter_version);
let mut jobs = vec![full_route_job, direct_quote_route_job, direct_sol_route_job];
// for v6, add a v4 fallback
if self.config.jupiter_version == jupiter::Version::V6 {
jobs.push(self.jupiter_quote(
input_mint,
quote_mint,
in_amount,
false,
jupiter::Version::V4,
));
}
let jobs = vec![full_route_job, direct_quote_route_job, direct_sol_route_job];
let mut results = futures::future::join_all(jobs).await;
let full_route = results.remove(0)?;
@ -253,7 +231,7 @@ impl Rebalancer {
.prepare_swap_transaction(full)
.await?;
let tx_size = builder.transaction_size()?;
if tx_size.is_ok() {
if tx_size.is_within_limit() {
return Ok((builder, full.clone()));
}
trace!(
@ -520,6 +498,7 @@ impl Rebalancer {
};
let counters = perp_pnl::fetch_top(
&self.mango_client.context,
&self.mango_client.client.config().fallback_oracle_config,
self.account_fetcher.as_ref(),
perp_position.market_index,
direction,

View File

@ -11,7 +11,10 @@ use mango_v4_client::MangoClient;
pub struct Config {
pub quote_index: TokenIndex,
/// Size in quote_index-token native tokens to quote.
pub quote_amount: u64,
pub jupiter_version: jupiter::Version,
}

View File

@ -1,8 +1,9 @@
use std::collections::HashSet;
use std::{
collections::HashMap,
pin::Pin,
sync::{Arc, RwLock},
time::{Duration, Instant},
time::Instant,
};
use futures_core::Future;
@ -11,10 +12,10 @@ use mango_v4::{
i80f48::ClampToInt,
state::{Bank, MangoAccountValue, TokenConditionalSwap, TokenIndex},
};
use mango_v4_client::{chain_data, health_cache, jupiter, MangoClient, TransactionBuilder};
use mango_v4_client::{chain_data, jupiter, MangoClient, TransactionBuilder};
use anyhow::Context as AnyhowContext;
use solana_sdk::{signature::Signature, signer::Signer};
use solana_sdk::signature::Signature;
use tracing::*;
use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
@ -56,7 +57,6 @@ pub enum Mode {
pub struct Config {
pub min_health_ratio: f64,
pub max_trigger_quote_amount: u64,
pub refresh_timeout: Duration,
pub compute_limit_for_trigger: u32,
pub collateral_token_index: TokenIndex,
@ -73,6 +73,9 @@ pub struct Config {
pub jupiter_version: jupiter::Version,
pub jupiter_slippage_bps: u64,
pub mode: Mode,
pub only_allowed_tokens: HashSet<TokenIndex>,
pub forbidden_tokens: HashSet<TokenIndex>,
}
pub enum JupiterQuoteCacheResult<T> {
@ -401,11 +404,43 @@ impl Context {
Ok(taker_price >= base_price * cost_over_oracle * (1.0 + self.config.profit_fraction))
}
// excluded by config
fn tcs_pair_is_allowed(
&self,
buy_token_index: TokenIndex,
sell_token_index: TokenIndex,
) -> bool {
if self.config.forbidden_tokens.contains(&buy_token_index) {
return false;
}
if self.config.forbidden_tokens.contains(&sell_token_index) {
return false;
}
if self.config.only_allowed_tokens.is_empty() {
return true;
}
if self.config.only_allowed_tokens.contains(&buy_token_index) {
return true;
}
if self.config.only_allowed_tokens.contains(&sell_token_index) {
return true;
}
return false;
}
// Either expired or triggerable with ok-looking price.
fn tcs_is_interesting(&self, tcs: &TokenConditionalSwap) -> anyhow::Result<bool> {
if tcs.is_expired(self.now_ts) {
return Ok(true);
}
if !self.tcs_pair_is_allowed(tcs.buy_token_index, tcs.buy_token_index) {
return Ok(false);
}
let (_, buy_token_price, _) = self.token_bank_price_mint(tcs.buy_token_index)?;
let (_, sell_token_price, _) = self.token_bank_price_mint(tcs.sell_token_index)?;
@ -665,8 +700,9 @@ impl Context {
liqee_old: &MangoAccountValue,
tcs_id: u64,
) -> anyhow::Result<Option<PreparedExecution>> {
let fetcher = self.account_fetcher.as_ref();
let health_cache = health_cache::new(&self.mango_client.context, fetcher, liqee_old)
let health_cache = self
.mango_client
.health_cache(liqee_old)
.await
.context("creating health cache 1")?;
if health_cache.is_liquidatable() {
@ -685,7 +721,9 @@ impl Context {
return Ok(None);
}
let health_cache = health_cache::new(&self.mango_client.context, fetcher, &liqee)
let health_cache = self
.mango_client
.health_cache(&liqee)
.await
.context("creating health cache 2")?;
if health_cache.is_liquidatable() {
@ -1165,10 +1203,8 @@ impl Context {
let fee_payer = self.mango_client.client.fee_payer();
TransactionBuilder {
instructions: vec![compute_ix],
address_lookup_tables: vec![],
payer: fee_payer.pubkey(),
signers: vec![self.mango_client.owner.clone(), fee_payer],
config: self.mango_client.client.transaction_builder_config,
..self.mango_client.transaction_builder().await?
}
};

View File

@ -0,0 +1,126 @@
use anchor_lang::error::Error::AnchorError;
use mango_v4::error::MangoError;
use mango_v4::state::TokenIndex;
use regex::Regex;
pub trait UnwrappableOracleError {
fn try_unwrap_oracle_error(&self) -> Option<(TokenIndex, String)>;
}
impl UnwrappableOracleError for anyhow::Error {
fn try_unwrap_oracle_error(&self) -> Option<(TokenIndex, String)> {
let root_cause = self
.root_cause()
.downcast_ref::<anchor_lang::error::Error>();
if root_cause.is_none() {
return None;
}
if let AnchorError(ae) = root_cause.unwrap() {
let is_oracle_error = ae.error_code_number == MangoError::OracleConfidence.error_code()
|| ae.error_code_number == MangoError::OracleStale.error_code();
if !is_oracle_error {
return None;
}
let error_str = ae.to_string();
return parse_oracle_error_string(&error_str);
}
None
}
}
fn parse_oracle_error_string(error_str: &str) -> Option<(TokenIndex, String)> {
let token_name_regex = Regex::new(r#"name: (\w+)"#).unwrap();
let token_index_regex = Regex::new(r#"token index (\d+)"#).unwrap();
let token_name = token_name_regex
.captures(error_str)
.map(|c| c[1].to_string())
.unwrap_or_default();
let token_index = token_index_regex
.captures(error_str)
.map(|c| c[1].parse::<u16>().ok())
.unwrap_or_default();
if token_index.is_some() {
return Some((TokenIndex::from(token_index.unwrap()), token_name));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use anchor_lang::error;
use anyhow::Context;
use mango_v4::error::Contextable;
use mango_v4::error::MangoError;
use mango_v4::state::{oracle_log_context, OracleConfig, OracleState, OracleType};
fn generate_errored_res() -> std::result::Result<u8, error::Error> {
return Err(MangoError::OracleConfidence.into());
}
fn generate_errored_res_with_context() -> anyhow::Result<u8> {
let value = Contextable::with_context(
Contextable::with_context(generate_errored_res(), || {
oracle_log_context(
"SOL",
&OracleState {
price: Default::default(),
deviation: Default::default(),
last_update_slot: 0,
oracle_type: OracleType::Pyth,
},
&OracleConfig {
conf_filter: Default::default(),
max_staleness_slots: 0,
reserved: [0; 72],
},
None,
)
}),
|| {
format!(
"getting oracle for bank with health account index {} and token index {}, passed account {}",
10,
11,
12,
)
},
)?;
Ok(value)
}
#[test]
fn should_extract_oracle_error_and_token_infos() {
let error = generate_errored_res_with_context()
.context("Something")
.unwrap_err();
println!("{}", error);
println!("{}", error.root_cause());
let oracle_error_opt = error.try_unwrap_oracle_error();
assert!(oracle_error_opt.is_some());
assert_eq!(
oracle_error_opt.unwrap(),
(TokenIndex::from(11u16), "SOL".to_string())
);
}
#[test]
fn should_parse_oracle_error_message() {
assert!(parse_oracle_error_string("").is_none());
assert!(parse_oracle_error_string("Something went wrong").is_none());
assert_eq!(
parse_oracle_error_string("Something went wrong token index 4, name: SOL, Stale")
.unwrap(),
(TokenIndex::from(4u16), "SOL".to_string())
);
}
}

View File

@ -78,7 +78,7 @@ async fn main() -> anyhow::Result<()> {
);
let group_pk = Pubkey::from_str(&config.mango_group).unwrap();
let group_context =
Arc::new(MangoGroupContext::new_from_rpc(&client.rpc_async(), group_pk).await?);
Arc::new(MangoGroupContext::new_from_rpc(client.rpc_async(), group_pk).await?);
let perp_queue_pks: Vec<_> = group_context
.perp_markets

View File

@ -373,7 +373,7 @@ async fn main() -> anyhow::Result<()> {
);
let group_context = Arc::new(
MangoGroupContext::new_from_rpc(
&client.rpc_async(),
client.rpc_async(),
Pubkey::from_str(&config.mango_group).unwrap(),
)
.await?,

View File

@ -357,7 +357,7 @@ async fn main() -> anyhow::Result<()> {
);
let group_context = Arc::new(
MangoGroupContext::new_from_rpc(
&client.rpc_async(),
client.rpc_async(),
Pubkey::from_str(&config.mango_group).unwrap(),
)
.await?,

View File

@ -21,7 +21,8 @@ use fixed::types::I80F48;
use mango_feeds_connector::metrics::*;
use mango_v4::state::{MangoAccount, MangoAccountValue, PerpMarketIndex};
use mango_v4_client::{
chain_data, health_cache, AccountFetcher, Client, MangoGroupContext, TransactionBuilderConfig,
chain_data, health_cache, AccountFetcher, Client, FallbackOracleConfig, MangoGroupContext,
TransactionBuilderConfig,
};
use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::{account::ReadableAccount, signature::Keypair};
@ -52,7 +53,13 @@ async fn compute_pnl(
account_fetcher: Arc<impl AccountFetcher>,
account: &MangoAccountValue,
) -> anyhow::Result<Vec<(PerpMarketIndex, I80F48)>> {
let health_cache = health_cache::new(&context, account_fetcher.as_ref(), account).await?;
let health_cache = health_cache::new(
&context,
&FallbackOracleConfig::Dynamic,
account_fetcher.as_ref(),
account,
)
.await?;
let pnls = account
.active_perp_positions()
@ -265,7 +272,7 @@ async fn main() -> anyhow::Result<()> {
);
let group_context = Arc::new(
MangoGroupContext::new_from_rpc(
&client.rpc_async(),
client.rpc_async(),
Pubkey::from_str(&config.pnl.mango_group).unwrap(),
)
.await?,
@ -273,7 +280,7 @@ async fn main() -> anyhow::Result<()> {
let chain_data = Arc::new(RwLock::new(chain_data::ChainData::new()));
let account_fetcher = Arc::new(chain_data::AccountFetcher {
chain_data: chain_data.clone(),
rpc: client.rpc_async(),
rpc: client.new_rpc_async(),
});
let metrics_tx = metrics::start(config.metrics, "pnl".into());

View File

@ -6,8 +6,8 @@ use anchor_client::Cluster;
use clap::Parser;
use mango_v4::state::{PerpMarketIndex, TokenIndex};
use mango_v4_client::{
account_update_stream, chain_data, keypair_from_cli, snapshot_source, websocket_source, Client,
MangoClient, MangoGroupContext, TransactionBuilderConfig,
account_update_stream, chain_data, keypair_from_cli, priority_fees_cli, snapshot_source,
websocket_source, Client, MangoClient, MangoGroupContext, TransactionBuilderConfig,
};
use tracing::*;
@ -61,9 +61,12 @@ struct Cli {
#[clap(long, env, default_value = "100")]
get_multiple_accounts_count: usize,
/// prioritize each transaction with this many microlamports/cu
#[clap(long, env, default_value = "0")]
prioritization_micro_lamports: u64,
#[clap(flatten)]
prioritization_fee_cli: priority_fees_cli::PriorityFeeArgs,
/// url to the lite-rpc websocket, optional
#[clap(long, env, default_value = "")]
lite_rpc_url: String,
/// compute budget for each instruction
#[clap(long, env, default_value = "250000")]
@ -87,6 +90,10 @@ async fn main() -> anyhow::Result<()> {
};
let cli = Cli::parse_from(args);
let (prio_provider, prio_jobs) = cli
.prioritization_fee_cli
.make_prio_provider(cli.lite_rpc_url.clone())?;
let settler_owner = Arc::new(keypair_from_cli(&cli.settler_owner));
let rpc_url = cli.rpc_url;
@ -100,11 +107,11 @@ async fn main() -> anyhow::Result<()> {
commitment,
settler_owner.clone(),
Some(rpc_timeout),
TransactionBuilderConfig {
prioritization_micro_lamports: (cli.prioritization_micro_lamports > 0)
.then_some(cli.prioritization_micro_lamports),
compute_budget_per_instruction: Some(cli.compute_budget_per_instruction),
},
TransactionBuilderConfig::builder()
.compute_budget_per_instruction(Some(cli.compute_budget_per_instruction))
.priority_fee_provider(prio_provider)
.build()
.unwrap(),
);
// The representation of current on-chain account data
@ -112,7 +119,7 @@ async fn main() -> anyhow::Result<()> {
// Reading accounts from chain_data
let account_fetcher = Arc::new(chain_data::AccountFetcher {
chain_data: chain_data.clone(),
rpc: client.rpc_async(),
rpc: client.new_rpc_async(),
});
let mango_account = account_fetcher
@ -120,7 +127,7 @@ async fn main() -> anyhow::Result<()> {
.await?;
let mango_group = mango_account.fixed.group;
let group_context = MangoGroupContext::new_from_rpc(&client.rpc_async(), mango_group).await?;
let group_context = MangoGroupContext::new_from_rpc(client.rpc_async(), mango_group).await?;
let mango_oracles = group_context
.tokens
@ -352,6 +359,7 @@ async fn main() -> anyhow::Result<()> {
check_changes_for_abort_job,
]
.into_iter()
.chain(prio_jobs.into_iter())
.collect();
jobs.next().await;

View File

@ -5,10 +5,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
use mango_v4::health::HealthType;
use mango_v4::state::{OracleAccountInfos, PerpMarket, PerpMarketIndex};
use mango_v4_client::{
chain_data, health_cache, prettify_solana_client_error, MangoClient, PreparedInstructions,
TransactionBuilder,
};
use mango_v4_client::{chain_data, MangoClient, PreparedInstructions, TransactionBuilder};
use solana_sdk::address_lookup_table_account::AddressLookupTableAccount;
use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::signature::Signature;
@ -120,11 +117,10 @@ impl SettlementState {
continue;
}
let health_cache =
match health_cache::new(&mango_client.context, account_fetcher, &account).await {
Ok(hc) => hc,
Err(_) => continue, // Skip for stale/unconfident oracles
};
let health_cache = match mango_client.health_cache(&account).await {
Ok(hc) => hc,
Err(_) => continue, // Skip for stale/unconfident oracles
};
let liq_end_health = health_cache.health(HealthType::LiquidationEnd);
for perp_market_index in perp_indexes {
@ -288,7 +284,7 @@ impl<'a> SettleBatchProcessor<'a> {
address_lookup_tables: self.address_lookup_tables.clone(),
payer: fee_payer.pubkey(),
signers: vec![fee_payer],
config: client.transaction_builder_config,
config: client.config().transaction_builder_config.clone(),
}
.transaction_with_blockhash(self.blockhash)
}
@ -301,13 +297,7 @@ impl<'a> SettleBatchProcessor<'a> {
let tx = self.transaction()?;
self.instructions.clear();
let send_result = self
.mango_client
.client
.rpc_async()
.send_transaction_with_config(&tx, self.mango_client.client.rpc_send_transaction_config)
.await
.map_err(prettify_solana_client_error);
let send_result = self.mango_client.client.send_transaction(&tx).await;
match send_result {
Ok(txsig) => {
@ -328,11 +318,14 @@ impl<'a> SettleBatchProcessor<'a> {
) -> anyhow::Result<Option<Signature>> {
let a_value = self.account_fetcher.fetch_mango_account(&account_a)?;
let b_value = self.account_fetcher.fetch_mango_account(&account_b)?;
let new_ixs = self.mango_client.perp_settle_pnl_instruction(
self.perp_market_index,
(&account_a, &a_value),
(&account_b, &b_value),
)?;
let new_ixs = self
.mango_client
.perp_settle_pnl_instruction(
self.perp_market_index,
(&account_a, &a_value),
(&account_b, &b_value),
)
.await?;
let previous = self.instructions.clone();
self.instructions.append(new_ixs.clone());

View File

@ -123,14 +123,17 @@ impl State {
}
// Clear newly created token positions, so the liqor account is mostly empty
for token_index in startable_chunk.iter().map(|(_, _, ti)| *ti).unique() {
let new_token_pos_indices = startable_chunk
.iter()
.map(|(_, _, ti)| *ti)
.unique()
.collect_vec();
for token_index in new_token_pos_indices {
let mint = mango_client.context.token(token_index).mint;
let ix = match mango_client.token_withdraw_instructions(
&liqor_account,
mint,
u64::MAX,
false,
) {
let ix = match mango_client
.token_withdraw_instructions(&liqor_account, mint, u64::MAX, false)
.await
{
Ok(ix) => ix,
Err(_) => continue,
};

View File

@ -15,6 +15,7 @@ async-channel = "1.6"
async-once-cell = { version = "0.4.2", features = ["unpin"] }
async-trait = "0.1.52"
atty = "0.2"
clap = { version = "3.1.8", features = ["derive", "env"] }
derive_builder = "0.12.0"
fixed = { workspace = true, features = ["serde", "borsh"] }
futures = "0.3.25"
@ -30,6 +31,7 @@ solana-client = { workspace = true }
solana-rpc = { workspace = true }
solana-sdk = { workspace = true }
solana-address-lookup-table-program = { workspace = true }
solana-transaction-status = { workspace = true }
mango-feeds-connector = { workspace = true }
spl-associated-token-account = "1.0.3"
thiserror = "1.0.31"
@ -37,6 +39,7 @@ thiserror = "1.0.31"
reqwest = "0.11.17"
tokio = { version = "1", features = ["full"] }
tokio-stream = { version = "0.1.9"}
tokio-tungstenite = "0.17.0"
serde = "1.0.141"
serde_json = "1.0.82"
base64 = "0.13.0"

View File

@ -11,10 +11,14 @@ use anchor_lang::AccountDeserialize;
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
use solana_sdk::account::{AccountSharedData, ReadableAccount};
use solana_sdk::hash::Hash;
use solana_sdk::hash::Hasher;
use solana_sdk::pubkey::Pubkey;
use mango_v4::state::MangoAccountValue;
use crate::gpa;
#[async_trait::async_trait]
pub trait AccountFetcher: Sync + Send {
async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData>;
@ -29,6 +33,13 @@ pub trait AccountFetcher: Sync + Send {
program: &Pubkey,
discriminator: [u8; 8],
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>>;
async fn fetch_multiple_accounts(
&self,
keys: &[Pubkey],
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>>;
async fn get_slot(&self) -> anyhow::Result<u64>;
}
// Can't be in the trait, since then it would no longer be object-safe...
@ -100,6 +111,17 @@ impl AccountFetcher for RpcAccountFetcher {
.map(|(pk, acc)| (pk, acc.into()))
.collect::<Vec<_>>())
}
async fn fetch_multiple_accounts(
&self,
keys: &[Pubkey],
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>> {
gpa::fetch_multiple_accounts(&self.rpc, keys).await
}
async fn get_slot(&self) -> anyhow::Result<u64> {
Ok(self.rpc.get_slot().await?)
}
}
struct CoalescedAsyncJob<Key, Output> {
@ -138,6 +160,8 @@ struct AccountCache {
keys_for_program_and_discriminator: HashMap<(Pubkey, [u8; 8]), Vec<Pubkey>>,
account_jobs: CoalescedAsyncJob<Pubkey, anyhow::Result<AccountSharedData>>,
multiple_accounts_jobs:
CoalescedAsyncJob<Hash, anyhow::Result<Vec<(Pubkey, AccountSharedData)>>>,
program_accounts_jobs:
CoalescedAsyncJob<(Pubkey, [u8; 8]), anyhow::Result<Vec<(Pubkey, AccountSharedData)>>>,
}
@ -261,4 +285,62 @@ impl<T: AccountFetcher + 'static> AccountFetcher for CachedAccountFetcher<T> {
)),
}
}
async fn fetch_multiple_accounts(
&self,
keys: &[Pubkey],
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>> {
let fetch_job = {
let mut cache = self.cache.lock().unwrap();
let mut missing_keys: Vec<Pubkey> = keys
.iter()
.filter(|k| !cache.accounts.contains_key(k))
.cloned()
.collect();
if missing_keys.len() == 0 {
return Ok(keys
.iter()
.map(|pk| (*pk, cache.accounts.get(&pk).unwrap().clone()))
.collect::<Vec<_>>());
}
let self_copy = self.clone();
missing_keys.sort();
let mut hasher = Hasher::default();
for key in missing_keys.iter() {
hasher.hash(key.as_ref());
}
let job_key = hasher.result();
cache
.multiple_accounts_jobs
.run_coalesced(job_key.clone(), async move {
let result = self_copy
.fetcher
.fetch_multiple_accounts(&missing_keys)
.await;
let mut cache = self_copy.cache.lock().unwrap();
cache.multiple_accounts_jobs.remove(&job_key);
if let Ok(results) = result.as_ref() {
for (key, account) in results {
cache.accounts.insert(*key, account.clone());
}
}
result
})
};
match fetch_job.get().await {
Ok(v) => Ok(v.clone()),
// Can't clone the stored error, so need to stringize it
Err(err) => Err(anyhow::format_err!(
"fetch error in CachedAccountFetcher: {:?}",
err
)),
}
}
async fn get_slot(&self) -> anyhow::Result<u64> {
self.fetcher.get_slot().await
}
}

View File

@ -8,7 +8,10 @@ use anchor_lang::Discriminator;
use fixed::types::I80F48;
use mango_v4::accounts_zerocopy::{KeyedAccountSharedData, LoadZeroCopy};
use mango_v4::state::{Bank, MangoAccount, MangoAccountValue, OracleAccountInfos};
use mango_v4::state::{
pyth_mainnet_sol_oracle, pyth_mainnet_usdc_oracle, Bank, MangoAccount, MangoAccountValue,
OracleAccountInfos,
};
use anyhow::Context;
@ -64,12 +67,34 @@ impl AccountFetcher {
pub fn fetch_bank_and_price(&self, bank: &Pubkey) -> anyhow::Result<(Bank, I80F48)> {
let bank: Bank = self.fetch(bank)?;
let oracle = self.fetch_raw(&bank.oracle)?;
let oracle_acc = &KeyedAccountSharedData::new(bank.oracle, oracle.into());
let price = bank.oracle_price(&OracleAccountInfos::from_reader(oracle_acc), None)?;
let oracle_data = self.fetch_raw(&bank.oracle)?;
let oracle = &KeyedAccountSharedData::new(bank.oracle, oracle_data.into());
let fallback_opt = self.fetch_keyed_account_data(bank.fallback_oracle)?;
let sol_opt = self.fetch_keyed_account_data(pyth_mainnet_sol_oracle::ID)?;
let usdc_opt = self.fetch_keyed_account_data(pyth_mainnet_usdc_oracle::ID)?;
let oracle_acc_infos = OracleAccountInfos {
oracle,
fallback_opt: fallback_opt.as_ref(),
usdc_opt: usdc_opt.as_ref(),
sol_opt: sol_opt.as_ref(),
};
let price = bank.oracle_price(&oracle_acc_infos, None)?;
Ok((bank, price))
}
#[inline(always)]
fn fetch_keyed_account_data(
&self,
key: Pubkey,
) -> anyhow::Result<Option<KeyedAccountSharedData>> {
Ok(self
.fetch_raw(&key)
.ok()
.map(|data| KeyedAccountSharedData::new(key, data)))
}
pub fn fetch_bank_price(&self, bank: &Pubkey) -> anyhow::Result<I80F48> {
self.fetch_bank_and_price(bank).map(|(_, p)| p)
}
@ -217,4 +242,20 @@ impl crate::AccountFetcher for AccountFetcher {
})
.collect::<Vec<_>>())
}
async fn fetch_multiple_accounts(
&self,
keys: &[Pubkey],
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>> {
let chain_data = self.chain_data.read().unwrap();
Ok(keys
.iter()
.map(|pk| (*pk, chain_data.account(pk).unwrap().account.clone()))
.collect::<Vec<_>>())
}
async fn get_slot(&self) -> anyhow::Result<u64> {
let chain_data = self.chain_data.read().unwrap();
Ok(chain_data.newest_processed_slot())
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,117 @@
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
use solana_client::rpc_request::RpcError;
use solana_sdk::{commitment_config::CommitmentConfig, signature::Signature};
use solana_transaction_status::TransactionStatus;
use crate::util::delay_interval;
use std::time::Duration;
#[derive(thiserror::Error, Debug)]
pub enum WaitForTransactionConfirmationError {
#[error("blockhash has expired")]
BlockhashExpired,
#[error("timeout expired")]
Timeout,
#[error("client error: {0:?}")]
ClientError(#[from] solana_client::client_error::ClientError),
}
#[derive(Clone, Debug, Builder)]
#[builder(default)]
pub struct RpcConfirmTransactionConfig {
/// If none, defaults to the RpcClient's configured default commitment.
pub commitment: Option<CommitmentConfig>,
/// Time after which to start checking for blockhash expiry.
pub recent_blockhash_initial_timeout: Duration,
/// Interval between signature status queries.
pub signature_status_interval: Duration,
/// If none, there's no timeout. The confirmation will still abort eventually
/// when the blockhash expires.
pub timeout: Option<Duration>,
}
impl Default for RpcConfirmTransactionConfig {
fn default() -> Self {
Self {
commitment: None,
recent_blockhash_initial_timeout: Duration::from_secs(5),
signature_status_interval: Duration::from_millis(500),
timeout: None,
}
}
}
impl RpcConfirmTransactionConfig {
pub fn builder() -> RpcConfirmTransactionConfigBuilder {
RpcConfirmTransactionConfigBuilder::default()
}
}
/// Wait for `signature` to be confirmed at `commitment` or until either
/// - `recent_blockhash` is so old that the tx can't be confirmed _and_
/// `blockhash_initial_timeout` is reached
/// - the `signature_status_timeout` is reached
/// While waiting, query for confirmation every `signature_status_interval`
///
/// NOTE: RpcClient::config contains confirm_transaction_initial_timeout which is the
/// same as blockhash_initial_timeout. Unfortunately the former is private.
///
/// Returns:
/// - blockhash and blockhash_initial_timeout expired -> BlockhashExpired error
/// - signature_status_timeout expired -> Timeout error (possibly just didn't reach commitment in time?)
/// - any rpc error -> ClientError error
/// - confirmed at commitment -> ok(slot, opt<tx_error>)
pub async fn wait_for_transaction_confirmation(
rpc_client: &RpcClientAsync,
signature: &Signature,
recent_blockhash: &solana_sdk::hash::Hash,
config: &RpcConfirmTransactionConfig,
) -> Result<TransactionStatus, WaitForTransactionConfirmationError> {
let mut signature_status_interval = delay_interval(config.signature_status_interval);
let commitment = config.commitment.unwrap_or(rpc_client.commitment());
let start = std::time::Instant::now();
let is_timed_out = || config.timeout.map(|t| start.elapsed() > t).unwrap_or(false);
loop {
signature_status_interval.tick().await;
if is_timed_out() {
return Err(WaitForTransactionConfirmationError::Timeout);
}
let statuses = rpc_client
.get_signature_statuses(&[signature.clone()])
.await?;
let status_opt = match statuses.value.into_iter().next() {
Some(v) => v,
None => {
return Err(WaitForTransactionConfirmationError::ClientError(
RpcError::ParseError(
"must contain an entry for each requested signature".into(),
)
.into(),
));
}
};
// If the tx isn't seen at all (not even processed), check blockhash expiry
if status_opt.is_none() {
if start.elapsed() > config.recent_blockhash_initial_timeout {
let blockhash_is_valid = rpc_client
.is_blockhash_valid(recent_blockhash, CommitmentConfig::processed())
.await?;
if !blockhash_is_valid {
return Err(WaitForTransactionConfirmationError::BlockhashExpired);
}
}
continue;
}
let status = status_opt.unwrap();
if status.satisfies_commitment(commitment) {
return Ok(status);
}
}
}

View File

@ -4,15 +4,20 @@ use anchor_client::ClientError;
use anchor_lang::__private::bytemuck;
use mango_v4::state::{
Group, MangoAccountValue, PerpMarketIndex, Serum3MarketIndex, TokenIndex, MAX_BANKS,
use mango_v4::{
accounts_zerocopy::{KeyedAccountReader, KeyedAccountSharedData},
state::{
determine_oracle_type, load_whirlpool_state, oracle_state_unchecked, Group,
MangoAccountValue, OracleAccountInfos, OracleConfig, OracleConfigParams, OracleType,
PerpMarketIndex, Serum3MarketIndex, TokenIndex, MAX_BANKS,
},
};
use fixed::types::I80F48;
use futures::{stream, StreamExt, TryStreamExt};
use itertools::Itertools;
use crate::gpa::*;
use crate::{gpa::*, AccountFetcher, FallbackOracleConfig};
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
use solana_sdk::account::Account;
@ -28,9 +33,10 @@ pub struct TokenContext {
pub oracle: Pubkey,
pub banks: [Pubkey; MAX_BANKS],
pub vaults: [Pubkey; MAX_BANKS],
pub fallback_oracle: Pubkey,
pub fallback_context: FallbackOracleContext,
pub mint_info_address: Pubkey,
pub decimals: u8,
pub oracle_config: OracleConfig,
}
impl TokenContext {
@ -56,6 +62,18 @@ impl TokenContext {
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct FallbackOracleContext {
pub key: Pubkey,
// only used for CLMM fallback oracles, otherwise Pubkey::default
pub quote_key: Pubkey,
}
impl FallbackOracleContext {
pub fn keys(&self) -> Vec<Pubkey> {
vec![self.key, self.quote_key]
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct Serum3MarketContext {
pub address: Pubkey,
@ -101,6 +119,9 @@ pub struct ComputeEstimates {
pub cu_per_serum3_order_cancel: u32,
pub cu_per_perp_order_match: u32,
pub cu_per_perp_order_cancel: u32,
pub cu_per_oracle_fallback: u32,
pub cu_per_charge_collateral_fees: u32,
pub cu_per_charge_collateral_fees_token: u32,
}
impl Default for ComputeEstimates {
@ -118,25 +139,40 @@ impl Default for ComputeEstimates {
cu_per_perp_order_match: 7_000,
// measured around 3.5k, see test_perp_compute
cu_per_perp_order_cancel: 7_000,
// measured around 2k, see test_health_compute_tokens_fallback_oracles
cu_per_oracle_fallback: 2000,
// the base cost is mostly the division
cu_per_charge_collateral_fees: 20_000,
// per-chargable-token cost
cu_per_charge_collateral_fees_token: 15_000,
}
}
}
impl ComputeEstimates {
pub fn health_for_counts(&self, tokens: usize, perps: usize, serums: usize) -> u32 {
pub fn health_for_counts(
&self,
tokens: usize,
perps: usize,
serums: usize,
fallbacks: usize,
) -> u32 {
let tokens: u32 = tokens.try_into().unwrap();
let perps: u32 = perps.try_into().unwrap();
let serums: u32 = serums.try_into().unwrap();
let fallbacks: u32 = fallbacks.try_into().unwrap();
tokens * self.health_cu_per_token
+ perps * self.health_cu_per_perp
+ serums * self.health_cu_per_serum
+ fallbacks * self.cu_per_oracle_fallback
}
pub fn health_for_account(&self, account: &MangoAccountValue) -> u32 {
pub fn health_for_account(&self, account: &MangoAccountValue, num_fallbacks: usize) -> u32 {
self.health_for_counts(
account.active_token_positions().count(),
account.active_perp_positions().count(),
account.active_serum3_orders().count(),
num_fallbacks,
)
}
}
@ -227,8 +263,12 @@ impl MangoGroupContext {
decimals: u8::MAX,
banks: mi.banks,
vaults: mi.vaults,
fallback_oracle: mi.fallback_oracle,
oracle: mi.oracle,
fallback_context: FallbackOracleContext {
key: mi.fallback_oracle,
quote_key: Pubkey::default(),
},
oracle_config: OracleConfigParams::default().to_oracle_config(),
group: mi.group,
mint: mi.mint,
},
@ -236,14 +276,23 @@ impl MangoGroupContext {
})
.collect::<HashMap<_, _>>();
// reading the banks is only needed for the token names and decimals
// reading the banks is only needed for the token names, decimals and oracle configs
// FUTURE: either store the names on MintInfo as well, or maybe don't store them at all
// because they are in metaplex?
let bank_tuples = fetch_banks(rpc, program, group).await?;
for (_, bank) in bank_tuples {
let fallback_keys: Vec<Pubkey> = bank_tuples
.iter()
.map(|tup| tup.1.fallback_oracle)
.collect();
let fallback_oracle_accounts = fetch_multiple_accounts(rpc, &fallback_keys[..]).await?;
for (index, (_, bank)) in bank_tuples.iter().enumerate() {
let token = tokens.get_mut(&bank.token_index).unwrap();
token.name = bank.name().into();
token.decimals = bank.mint_decimals;
token.oracle_config = bank.oracle_config;
let (key, acc_info) = fallback_oracle_accounts[index].clone();
token.fallback_context.quote_key =
get_fallback_quote_key(&KeyedAccountSharedData::new(key, acc_info));
}
assert!(tokens.values().all(|t| t.decimals != u8::MAX));
@ -357,6 +406,7 @@ impl MangoGroupContext {
affected_tokens: Vec<TokenIndex>,
writable_banks: Vec<TokenIndex>,
affected_perp_markets: Vec<PerpMarketIndex>,
fallback_contexts: HashMap<Pubkey, FallbackOracleContext>,
) -> anyhow::Result<(Vec<AccountMeta>, u32)> {
let mut account = account.clone();
for affected_token_index in affected_tokens.iter().chain(writable_banks.iter()) {
@ -370,6 +420,7 @@ impl MangoGroupContext {
// figure out all the banks/oracles that need to be passed for the health check
let mut banks = vec![];
let mut oracles = vec![];
let mut fallbacks = vec![];
for position in account.active_token_positions() {
let token = self.token(position.token_index);
banks.push((
@ -377,6 +428,9 @@ impl MangoGroupContext {
writable_banks.iter().any(|&ti| ti == position.token_index),
));
oracles.push(token.oracle);
if let Some(fallback_context) = fallback_contexts.get(&token.oracle) {
fallbacks.extend(fallback_context.keys());
}
}
let serum_oos = account.active_serum3_orders().map(|&s| s.open_orders);
@ -386,6 +440,14 @@ impl MangoGroupContext {
let perp_oracles = account
.active_perp_positions()
.map(|&pa| self.perp(pa.market_index).oracle);
// FUTURE: implement fallback oracles for perps
let fallback_oracles: Vec<Pubkey> = fallbacks
.into_iter()
.unique()
.filter(|key| !oracles.contains(key) && key != &Pubkey::default())
.collect();
let fallbacks_len = fallback_oracles.len();
let to_account_meta = |pubkey| AccountMeta {
pubkey,
@ -404,9 +466,12 @@ impl MangoGroupContext {
.chain(perp_markets.map(to_account_meta))
.chain(perp_oracles.map(to_account_meta))
.chain(serum_oos.map(to_account_meta))
.chain(fallback_oracles.into_iter().map(to_account_meta))
.collect();
let cu = self.compute_estimates.health_for_account(&account);
let cu = self
.compute_estimates
.health_for_account(&account, fallbacks_len);
Ok((accounts, cu))
}
@ -417,10 +482,12 @@ impl MangoGroupContext {
account2: &MangoAccountValue,
affected_tokens: &[TokenIndex],
writable_banks: &[TokenIndex],
fallback_contexts: HashMap<Pubkey, FallbackOracleContext>,
) -> anyhow::Result<(Vec<AccountMeta>, u32)> {
// figure out all the banks/oracles that need to be passed for the health check
let mut banks = vec![];
let mut oracles = vec![];
let mut fallbacks = vec![];
let token_indexes = account2
.active_token_positions()
@ -434,6 +501,9 @@ impl MangoGroupContext {
let writable_bank = writable_banks.iter().contains(&token_index);
banks.push((token.first_bank(), writable_bank));
oracles.push(token.oracle);
if let Some(fallback_context) = fallback_contexts.get(&token.oracle) {
fallbacks.extend(fallback_context.keys());
}
}
let serum_oos = account2
@ -452,6 +522,14 @@ impl MangoGroupContext {
let perp_oracles = perp_market_indexes
.iter()
.map(|&index| self.perp(index).oracle);
// FUTURE: implement fallback oracles for perps
let fallback_oracles: Vec<Pubkey> = fallbacks
.into_iter()
.unique()
.filter(|key| !oracles.contains(key) && key != &Pubkey::default())
.collect();
let fallbacks_len = fallback_oracles.len();
let to_account_meta = |pubkey| AccountMeta {
pubkey,
@ -470,6 +548,7 @@ impl MangoGroupContext {
.chain(perp_markets.map(to_account_meta))
.chain(perp_oracles.map(to_account_meta))
.chain(serum_oos.map(to_account_meta))
.chain(fallback_oracles.into_iter().map(to_account_meta))
.collect();
// Since health is likely to be computed separately for both accounts, we don't use the
@ -490,10 +569,12 @@ impl MangoGroupContext {
account1_token_count,
account1.active_perp_positions().count(),
account1.active_serum3_orders().count(),
fallbacks_len,
) + self.compute_estimates.health_for_counts(
account2_token_count,
account2.active_perp_positions().count(),
account2.active_serum3_orders().count(),
fallbacks_len,
);
Ok((accounts, cu))
@ -554,6 +635,61 @@ impl MangoGroupContext {
let new_perp_markets = fetch_perp_markets(rpc, mango_v4::id(), self.group).await?;
Ok(new_perp_markets.len() > self.perp_markets.len())
}
/// Returns a map of oracle pubkey -> FallbackOracleContext
pub async fn derive_fallback_oracle_keys(
&self,
fallback_oracle_config: &FallbackOracleConfig,
account_fetcher: &dyn AccountFetcher,
) -> anyhow::Result<HashMap<Pubkey, FallbackOracleContext>> {
// FUTURE: implement for perp oracles as well
let fallbacks_by_oracle = match fallback_oracle_config {
FallbackOracleConfig::Never => HashMap::new(),
FallbackOracleConfig::Fixed(keys) => self
.tokens
.iter()
.filter(|token| {
token.1.fallback_context.key != Pubkey::default()
&& keys.contains(&token.1.fallback_context.key)
})
.map(|t| (t.1.oracle, t.1.fallback_context.clone()))
.collect(),
FallbackOracleConfig::All => self
.tokens
.iter()
.filter(|token| token.1.fallback_context.key != Pubkey::default())
.map(|t| (t.1.oracle, t.1.fallback_context.clone()))
.collect(),
FallbackOracleConfig::Dynamic => {
let tokens_by_oracle: HashMap<Pubkey, &TokenContext> =
self.tokens.iter().map(|t| (t.1.oracle, t.1)).collect();
let oracle_keys: Vec<Pubkey> =
tokens_by_oracle.values().map(|b| b.oracle).collect();
let oracle_accounts = account_fetcher
.fetch_multiple_accounts(&oracle_keys)
.await?;
let now_slot = account_fetcher.get_slot().await?;
let mut stale_oracles_with_fallbacks = vec![];
for (key, acc) in oracle_accounts {
let token = tokens_by_oracle.get(&key).unwrap();
let state = oracle_state_unchecked(
&OracleAccountInfos::from_reader(&KeyedAccountSharedData::new(key, acc)),
token.decimals,
)?;
let oracle_is_valid = state
.check_confidence_and_maybe_staleness(&token.oracle_config, Some(now_slot));
if oracle_is_valid.is_err() && token.fallback_context.key != Pubkey::default() {
stale_oracles_with_fallbacks
.push((token.oracle, token.fallback_context.clone()));
}
}
stale_oracles_with_fallbacks.into_iter().collect()
}
};
Ok(fallbacks_by_oracle)
}
}
fn from_serum_style_pubkey(d: [u64; 4]) -> Pubkey {
@ -567,3 +703,22 @@ async fn fetch_raw_account(rpc: &RpcClientAsync, address: Pubkey) -> Result<Acco
.value
.ok_or(ClientError::AccountNotFound)
}
/// Fetch the quote key for a fallback oracle account info.
/// Returns Pubkey::default if no quote key is found or there are any
/// errors occur when trying to fetch the quote oracle.
/// This function will only return a non-default key when a CLMM oracle is used
fn get_fallback_quote_key(acc_info: &impl KeyedAccountReader) -> Pubkey {
let maybe_key = match determine_oracle_type(acc_info).ok() {
Some(oracle_type) => match oracle_type {
OracleType::OrcaCLMM => match load_whirlpool_state(acc_info).ok() {
Some(whirlpool) => whirlpool.get_quote_oracle().ok(),
None => None,
},
_ => None,
},
None => None,
};
maybe_key.unwrap_or_else(|| Pubkey::default())
}

View File

@ -32,7 +32,7 @@ impl<Key> Default for ErrorTypeState<Key> {
#[derive(Builder)]
pub struct ErrorTracking<Key, ErrorType> {
#[builder(setter(custom))]
#[builder(default, setter(custom))]
errors_by_type: HashMap<ErrorType, ErrorTypeState<Key>>,
/// number of errors of a type after which had_too_many_errors returns true

View File

@ -1,11 +1,11 @@
use anchor_lang::{AccountDeserialize, Discriminator};
use mango_v4::state::{Bank, MangoAccount, MangoAccountValue, MintInfo, PerpMarket, Serum3Market};
use solana_account_decoder::UiAccountEncoding;
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig};
use solana_client::rpc_filter::{Memcmp, RpcFilterType};
use solana_sdk::account::AccountSharedData;
use solana_sdk::pubkey::Pubkey;
pub async fn fetch_mango_accounts(
@ -129,3 +129,22 @@ pub async fn fetch_perp_markets(
)
.await
}
pub async fn fetch_multiple_accounts(
rpc: &RpcClientAsync,
keys: &[Pubkey],
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>> {
let config = RpcAccountInfoConfig {
encoding: Some(UiAccountEncoding::Base64),
..RpcAccountInfoConfig::default()
};
Ok(rpc
.get_multiple_accounts_with_config(keys, config)
.await?
.value
.into_iter()
.zip(keys.iter())
.filter(|(maybe_acc, _)| maybe_acc.is_some())
.map(|(acc, key)| (*key, acc.unwrap().into()))
.collect())
}

View File

@ -1,22 +1,32 @@
use crate::{AccountFetcher, MangoGroupContext};
use crate::{AccountFetcher, FallbackOracleConfig, MangoGroupContext};
use anyhow::Context;
use futures::{stream, StreamExt, TryStreamExt};
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
use mango_v4::health::{FixedOrderAccountRetriever, HealthCache};
use mango_v4::state::MangoAccountValue;
use mango_v4::state::{pyth_mainnet_sol_oracle, pyth_mainnet_usdc_oracle, MangoAccountValue};
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
pub async fn new(
context: &MangoGroupContext,
account_fetcher: &impl AccountFetcher,
fallback_config: &FallbackOracleConfig,
account_fetcher: &dyn AccountFetcher,
account: &MangoAccountValue,
) -> anyhow::Result<HealthCache> {
let active_token_len = account.active_token_positions().count();
let active_perp_len = account.active_perp_positions().count();
let (metas, _health_cu) =
context.derive_health_check_remaining_account_metas(account, vec![], vec![], vec![])?;
let fallback_keys = context
.derive_fallback_oracle_keys(fallback_config, account_fetcher)
.await?;
let (metas, _health_cu) = context.derive_health_check_remaining_account_metas(
account,
vec![],
vec![],
vec![],
fallback_keys,
)?;
let accounts: anyhow::Result<Vec<KeyedAccountSharedData>> = stream::iter(metas.iter())
.then(|meta| async {
Ok(KeyedAccountSharedData::new(
@ -34,9 +44,13 @@ pub async fn new(
begin_perp: active_token_len * 2,
begin_serum3: active_token_len * 2 + active_perp_len * 2,
staleness_slot: None,
begin_fallback_oracles: metas.len(), // TODO: add support for fallback oracle accounts
usd_oracle_index: None,
sol_oracle_index: None,
begin_fallback_oracles: metas.len(),
usdc_oracle_index: metas
.iter()
.position(|m| m.pubkey == pyth_mainnet_usdc_oracle::ID),
sol_oracle_index: metas
.iter()
.position(|m| m.pubkey == pyth_mainnet_sol_oracle::ID),
};
let now_ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
mango_v4::health::new_health_cache(&account.borrow(), &retriever, now_ts)
@ -51,8 +65,13 @@ pub fn new_sync(
let active_token_len = account.active_token_positions().count();
let active_perp_len = account.active_perp_positions().count();
let (metas, _health_cu) =
context.derive_health_check_remaining_account_metas(account, vec![], vec![], vec![])?;
let (metas, _health_cu) = context.derive_health_check_remaining_account_metas(
account,
vec![],
vec![],
vec![],
HashMap::new(),
)?;
let accounts = metas
.iter()
.map(|meta| {
@ -70,8 +89,8 @@ pub fn new_sync(
begin_perp: active_token_len * 2,
begin_serum3: active_token_len * 2 + active_perp_len * 2,
staleness_slot: None,
begin_fallback_oracles: metas.len(), // TODO: add support for fallback oracle accounts
usd_oracle_index: None,
begin_fallback_oracles: metas.len(),
usdc_oracle_index: None,
sol_oracle_index: None,
};
let now_ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();

View File

@ -1,23 +1,21 @@
pub mod v4;
pub mod v6;
use anchor_lang::prelude::*;
use std::str::FromStr;
use crate::{JupiterSwapMode, MangoClient, TransactionBuilder};
use crate::{MangoClient, TransactionBuilder};
use fixed::types::I80F48;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Version {
Mock,
V4,
V6,
}
#[derive(Clone)]
#[allow(clippy::large_enum_variant)]
pub enum RawQuote {
Mock,
V4(v4::QueryRoute),
V6(v6::QuoteResponse),
}
@ -32,21 +30,6 @@ pub struct Quote {
}
impl Quote {
pub fn try_from_v4(
input_mint: Pubkey,
output_mint: Pubkey,
route: v4::QueryRoute,
) -> anyhow::Result<Self> {
Ok(Quote {
input_mint,
output_mint,
price_impact_pct: route.price_impact_pct,
in_amount: route.in_amount.parse()?,
out_amount: route.out_amount.parse()?,
raw: RawQuote::V4(route),
})
}
pub fn try_from_v6(query: v6::QuoteResponse) -> anyhow::Result<Self> {
Ok(Quote {
input_mint: Pubkey::from_str(&query.input_mint)?,
@ -65,7 +48,6 @@ impl Quote {
pub fn first_route_label(&self) -> String {
let label_maybe = match &self.raw {
RawQuote::Mock => Some("mock".into()),
RawQuote::V4(raw) => raw.market_infos.first().map(|v| v.label.clone()),
RawQuote::V6(raw) => raw
.route_plan
.first()
@ -129,21 +111,6 @@ impl<'a> Jupiter<'a> {
) -> anyhow::Result<Quote> {
Ok(match version {
Version::Mock => self.quote_mock(input_mint, output_mint, amount).await?,
Version::V4 => Quote::try_from_v4(
input_mint,
output_mint,
self.mango_client
.jupiter_v4()
.quote(
input_mint,
output_mint,
amount,
slippage_bps,
JupiterSwapMode::ExactIn,
only_direct_routes,
)
.await?,
)?,
Version::V6 => Quote::try_from_v6(
self.mango_client
.jupiter_v6()
@ -165,12 +132,6 @@ impl<'a> Jupiter<'a> {
) -> anyhow::Result<TransactionBuilder> {
match &quote.raw {
RawQuote::Mock => anyhow::bail!("can't prepare jupiter swap for the mock"),
RawQuote::V4(raw) => {
self.mango_client
.jupiter_v4()
.prepare_swap_transaction(quote.input_mint, quote.output_mint, raw)
.await
}
RawQuote::V6(raw) => {
self.mango_client
.jupiter_v6()

View File

@ -1,363 +0,0 @@
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use anchor_lang::Id;
use anchor_spl::token::Token;
use bincode::Options;
use crate::{util, TransactionBuilder};
use crate::{JupiterSwapMode, MangoClient};
use anyhow::Context;
use solana_sdk::instruction::Instruction;
use solana_sdk::signature::Signature;
use solana_sdk::{pubkey::Pubkey, signer::Signer};
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct QueryResult {
pub data: Vec<QueryRoute>,
pub time_taken: f64,
pub context_slot: u64,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct QueryRoute {
pub in_amount: String,
pub out_amount: String,
pub price_impact_pct: f64,
pub market_infos: Vec<QueryMarketInfo>,
pub amount: String,
pub slippage_bps: u64,
pub other_amount_threshold: String,
pub swap_mode: String,
pub fees: Option<QueryRouteFees>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct QueryMarketInfo {
pub id: String,
pub label: String,
pub input_mint: String,
pub output_mint: String,
pub not_enough_liquidity: bool,
pub in_amount: String,
pub out_amount: String,
pub min_in_amount: Option<String>,
pub min_out_amount: Option<String>,
pub price_impact_pct: Option<f64>,
pub lp_fee: QueryFee,
pub platform_fee: QueryFee,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct QueryFee {
pub amount: String,
pub mint: String,
pub pct: Option<f64>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct QueryRouteFees {
pub signature_fee: f64,
pub open_orders_deposits: Vec<f64>,
pub ata_deposits: Vec<f64>,
pub total_fee_and_deposits: f64,
#[serde(rename = "minimalSOLForTransaction")]
pub minimal_sol_for_transaction: f64,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SwapRequest {
pub route: QueryRoute,
pub user_public_key: String,
#[serde(rename = "wrapUnwrapSOL")]
pub wrap_unwrap_sol: bool,
pub compute_unit_price_micro_lamports: Option<u64>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SwapResponse {
pub setup_transaction: Option<String>,
pub swap_transaction: String,
pub cleanup_transaction: Option<String>,
}
pub struct JupiterV4<'a> {
pub mango_client: &'a MangoClient,
}
impl<'a> JupiterV4<'a> {
pub async fn quote(
&self,
input_mint: Pubkey,
output_mint: Pubkey,
amount: u64,
slippage_bps: u64,
swap_mode: JupiterSwapMode,
only_direct_routes: bool,
) -> anyhow::Result<QueryRoute> {
let response = self
.mango_client
.http_client
.get(format!("{}/quote", self.mango_client.client.jupiter_v4_url))
.query(&[
("inputMint", input_mint.to_string()),
("outputMint", output_mint.to_string()),
("amount", format!("{}", amount)),
("onlyDirectRoutes", only_direct_routes.to_string()),
("enforceSingleTx", "true".into()),
("filterTopNResult", "10".into()),
("slippageBps", format!("{}", slippage_bps)),
(
"swapMode",
match swap_mode {
JupiterSwapMode::ExactIn => "ExactIn",
JupiterSwapMode::ExactOut => "ExactOut",
}
.into(),
),
])
.send()
.await
.context("quote request to jupiter")?;
let quote: QueryResult = util::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())
}
/// Find the instructions and account lookup tables for a jupiter swap through mango
///
/// It would be nice if we didn't have to pass input_mint/output_mint - the data is
/// definitely in QueryRoute - but it's unclear how.
pub async fn prepare_swap_transaction(
&self,
input_mint: Pubkey,
output_mint: Pubkey,
route: &QueryRoute,
) -> anyhow::Result<TransactionBuilder> {
let source_token = self.mango_client.context.token_by_mint(&input_mint)?;
let target_token = self.mango_client.context.token_by_mint(&output_mint)?;
let swap_response = self
.mango_client
.http_client
.post(format!("{}/swap", self.mango_client.client.jupiter_v4_url))
.json(&SwapRequest {
route: route.clone(),
user_public_key: self.mango_client.owner.pubkey().to_string(),
wrap_unwrap_sol: false,
compute_unit_price_micro_lamports: None, // we already prioritize
})
.send()
.await
.context("swap transaction request to jupiter")?;
let swap: SwapResponse = util::http_error_handling(swap_response)
.await
.context("error requesting jupiter swap")?;
if swap.setup_transaction.is_some() || swap.cleanup_transaction.is_some() {
anyhow::bail!(
"chosen jupiter route requires setup or cleanup transactions, can't execute"
);
}
let jup_tx = bincode::options()
.with_fixint_encoding()
.reject_trailing_bytes()
.deserialize::<solana_sdk::transaction::VersionedTransaction>(
&base64::decode(&swap.swap_transaction)
.context("base64 decoding jupiter transaction")?,
)
.context("parsing jupiter transaction")?;
let ata_program = anchor_spl::associated_token::ID;
let token_program = anchor_spl::token::ID;
let compute_budget_program = solana_sdk::compute_budget::ID;
// these setup instructions should be placed outside of flashloan begin-end
let is_setup_ix = |k: Pubkey| -> bool {
k == ata_program || k == token_program || k == compute_budget_program
};
let (jup_ixs, jup_alts) = self
.mango_client
.deserialize_instructions_and_alts(&jup_tx.message)
.await?;
let jup_action_ix_begin = jup_ixs
.iter()
.position(|ix| !is_setup_ix(ix.program_id))
.ok_or_else(|| {
anyhow::anyhow!("jupiter swap response only had setup-like instructions")
})?;
let jup_action_ix_end = jup_ixs.len()
- jup_ixs
.iter()
.rev()
.position(|ix| !is_setup_ix(ix.program_id))
.unwrap();
let bank_ams = [source_token.first_bank(), target_token.first_bank()]
.into_iter()
.map(util::to_writable_account_meta)
.collect::<Vec<_>>();
let vault_ams = [source_token.first_vault(), target_token.first_vault()]
.into_iter()
.map(util::to_writable_account_meta)
.collect::<Vec<_>>();
let owner = self.mango_client.owner();
let token_ams = [source_token.mint, target_token.mint]
.into_iter()
.map(|mint| {
util::to_writable_account_meta(
anchor_spl::associated_token::get_associated_token_address(&owner, &mint),
)
})
.collect::<Vec<_>>();
let source_loan = if route.swap_mode == "ExactIn" {
u64::from_str(&route.amount).unwrap()
} else if route.swap_mode == "ExactOut" {
u64::from_str(&route.other_amount_threshold).unwrap()
} else {
anyhow::bail!("unknown swap mode: {}", route.swap_mode);
};
let loan_amounts = vec![source_loan, 0u64];
let num_loans: u8 = loan_amounts.len().try_into().unwrap();
// This relies on the fact that health account banks will be identical to the first_bank above!
let (health_ams, _health_cu) = self
.mango_client
.derive_health_check_remaining_account_metas(
vec![source_token.token_index, target_token.token_index],
vec![source_token.token_index, target_token.token_index],
vec![],
)
.await
.context("building health accounts")?;
let mut instructions = Vec::new();
for ix in &jup_ixs[..jup_action_ix_begin] {
instructions.push(ix.clone());
}
// Ensure the source token account is created (jupiter takes care of the output account)
instructions.push(
spl_associated_token_account::instruction::create_associated_token_account_idempotent(
&owner,
&owner,
&source_token.mint,
&Token::id(),
),
);
instructions.push(Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::FlashLoanBegin {
account: self.mango_client.mango_account_address,
owner,
token_program: Token::id(),
instructions: solana_sdk::sysvar::instructions::id(),
},
None,
);
ams.extend(bank_ams);
ams.extend(vault_ams.clone());
ams.extend(token_ams.clone());
ams.push(util::to_readonly_account_meta(self.mango_client.group()));
ams
},
data: anchor_lang::InstructionData::data(&mango_v4::instruction::FlashLoanBegin {
loan_amounts,
}),
});
for ix in &jup_ixs[jup_action_ix_begin..jup_action_ix_end] {
instructions.push(ix.clone());
}
instructions.push(Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::FlashLoanEnd {
account: self.mango_client.mango_account_address,
owner,
token_program: Token::id(),
},
None,
);
ams.extend(health_ams);
ams.extend(vault_ams);
ams.extend(token_ams);
ams.push(util::to_readonly_account_meta(self.mango_client.group()));
ams
},
data: anchor_lang::InstructionData::data(&mango_v4::instruction::FlashLoanEndV2 {
num_loans,
flash_loan_type: mango_v4::accounts_ix::FlashLoanType::Swap,
}),
});
for ix in &jup_ixs[jup_action_ix_end..] {
instructions.push(ix.clone());
}
let mut address_lookup_tables = self.mango_client.mango_address_lookup_tables().await?;
address_lookup_tables.extend(jup_alts.into_iter());
let payer = owner; // maybe use fee_payer? but usually it's the same
Ok(TransactionBuilder {
instructions,
address_lookup_tables,
payer,
signers: vec![self.mango_client.owner.clone()],
config: self.mango_client.client.transaction_builder_config,
})
}
pub async fn swap(
&self,
input_mint: Pubkey,
output_mint: Pubkey,
amount: u64,
slippage_bps: u64,
swap_mode: JupiterSwapMode,
only_direct_routes: bool,
) -> anyhow::Result<Signature> {
let route = self
.quote(
input_mint,
output_mint,
amount,
slippage_bps,
swap_mode,
only_direct_routes,
)
.await?;
let tx_builder = self
.prepare_swap_transaction(input_mint, output_mint, &route)
.await?;
tx_builder.send_and_confirm(&self.mango_client.client).await
}
}

View File

@ -194,15 +194,15 @@ impl<'a> JupiterV6<'a> {
),
),
];
let client = &self.mango_client.client;
if !client.jupiter_token.is_empty() {
query_args.push(("token", client.jupiter_token.clone()));
let config = self.mango_client.client.config();
if !config.jupiter_token.is_empty() {
query_args.push(("token", config.jupiter_token.clone()));
}
let response = self
.mango_client
.http_client
.get(format!("{}/quote", client.jupiter_v6_url))
.get(format!("{}/quote", config.jupiter_v6_url))
.query(&query_args)
.send()
.await
@ -237,6 +237,7 @@ impl<'a> JupiterV6<'a> {
.collect::<Vec<_>>();
let owner = self.mango_client.owner();
let account = &self.mango_client.mango_account().await?;
let token_ams = [source_token.mint, target_token.mint]
.into_iter()
@ -259,6 +260,7 @@ impl<'a> JupiterV6<'a> {
let (health_ams, _health_cu) = self
.mango_client
.derive_health_check_remaining_account_metas(
account,
vec![source_token.token_index, target_token.token_index],
vec![source_token.token_index, target_token.token_index],
vec![],
@ -267,15 +269,15 @@ impl<'a> JupiterV6<'a> {
.context("building health accounts")?;
let mut query_args = vec![];
let client = &self.mango_client.client;
if !client.jupiter_token.is_empty() {
query_args.push(("token", client.jupiter_token.clone()));
let config = self.mango_client.client.config();
if !config.jupiter_token.is_empty() {
query_args.push(("token", config.jupiter_token.clone()));
}
let swap_response = self
.mango_client
.http_client
.post(format!("{}/swap-instructions", client.jupiter_v6_url))
.post(format!("{}/swap-instructions", config.jupiter_v6_url))
.query(&query_args)
.json(&SwapRequest {
user_public_key: owner.to_string(),
@ -386,7 +388,12 @@ impl<'a> JupiterV6<'a> {
address_lookup_tables,
payer,
signers: vec![self.mango_client.owner.clone()],
config: self.mango_client.client.transaction_builder_config,
config: self
.mango_client
.client
.config()
.transaction_builder_config
.clone(),
})
}

View File

@ -8,12 +8,15 @@ pub mod account_update_stream;
pub mod chain_data;
mod chain_data_fetcher;
mod client;
pub mod confirm_transaction;
mod context;
pub mod error_tracking;
pub mod gpa;
pub mod health_cache;
pub mod jupiter;
pub mod perp_pnl;
pub mod priority_fees;
pub mod priority_fees_cli;
pub mod snapshot_source;
mod util;
pub mod websocket_source;

View File

@ -17,6 +17,7 @@ pub enum Direction {
/// Note: keep in sync with perp.ts:getSettlePnlCandidates
pub async fn fetch_top(
context: &crate::context::MangoGroupContext,
fallback_config: &FallbackOracleConfig,
account_fetcher: &impl AccountFetcher,
perp_market_index: PerpMarketIndex,
direction: Direction,
@ -91,9 +92,10 @@ pub async fn fetch_top(
} else {
I80F48::ZERO
};
let perp_max_settle = crate::health_cache::new(context, account_fetcher, &acc)
.await?
.perp_max_settle(perp_market.settle_token_index)?;
let perp_max_settle =
crate::health_cache::new(context, fallback_config, account_fetcher, &acc)
.await?
.perp_max_settle(perp_market.settle_token_index)?;
let settleable_pnl = if perp_max_settle > 0 {
(*pnl).max(-perp_max_settle)
} else {

View File

@ -0,0 +1,240 @@
use futures::{SinkExt, StreamExt};
use jsonrpc_core::{MethodCall, Notification, Params, Version};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
use tokio::sync::broadcast;
use tokio::task::JoinHandle;
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::protocol::Message;
use tracing::*;
pub trait PriorityFeeProvider: Sync + Send {
fn compute_unit_fee_microlamports(&self) -> u64;
}
pub struct FixedPriorityFeeProvider {
pub compute_unit_fee_microlamports: u64,
}
impl FixedPriorityFeeProvider {
pub fn new(fee_microlamports: u64) -> Self {
Self {
compute_unit_fee_microlamports: fee_microlamports,
}
}
}
impl PriorityFeeProvider for FixedPriorityFeeProvider {
fn compute_unit_fee_microlamports(&self) -> u64 {
self.compute_unit_fee_microlamports
}
}
#[derive(Builder)]
pub struct EmaPriorityFeeProviderConfig {
pub percentile: u8,
#[builder(default = "0.2")]
pub alpha: f64,
pub fallback_prio: u64,
#[builder(default = "Duration::from_secs(15)")]
pub max_age: Duration,
}
impl EmaPriorityFeeProviderConfig {
pub fn builder() -> EmaPriorityFeeProviderConfigBuilder {
EmaPriorityFeeProviderConfigBuilder::default()
}
}
#[derive(Default)]
struct CuPercentileEmaPriorityFeeProviderData {
ema: f64,
last_update: Option<Instant>,
}
pub struct CuPercentileEmaPriorityFeeProvider {
data: RwLock<CuPercentileEmaPriorityFeeProviderData>,
config: EmaPriorityFeeProviderConfig,
}
impl PriorityFeeProvider for CuPercentileEmaPriorityFeeProvider {
fn compute_unit_fee_microlamports(&self) -> u64 {
let data = self.data.read().unwrap();
if let Some(last_update) = data.last_update {
if Instant::now().duration_since(last_update) > self.config.max_age {
return self.config.fallback_prio;
}
} else {
return self.config.fallback_prio;
}
data.ema as u64
}
}
impl CuPercentileEmaPriorityFeeProvider {
pub fn run(
config: EmaPriorityFeeProviderConfig,
sender: &broadcast::Sender<BlockPrioFees>,
) -> (Arc<Self>, JoinHandle<()>) {
let this = Arc::new(Self {
data: Default::default(),
config,
});
let handle = tokio::spawn({
let this_c = this.clone();
let rx = sender.subscribe();
async move { Self::run_update_job(this_c, rx).await }
});
(this, handle)
}
async fn run_update_job(provider: Arc<Self>, mut rx: broadcast::Receiver<BlockPrioFees>) {
let config = &provider.config;
loop {
let block_prios = rx.recv().await.unwrap();
let prio = match block_prios.by_cu_percentile.get(&config.percentile) {
Some(v) => *v as f64,
None => {
error!("percentile not available: {}", config.percentile);
continue;
}
};
let mut data = provider.data.write().unwrap();
data.ema = data.ema * (1.0 - config.alpha) + config.alpha * prio;
data.last_update = Some(Instant::now());
}
}
}
#[derive(Clone, Default, Debug)]
pub struct BlockPrioFees {
pub slot: u64,
// prio fee percentile in percent -> prio fee
pub percentile: HashMap<u8, u64>,
// cu percentile in percent -> median prio fee of the group
pub by_cu_percentile: HashMap<u8, u64>,
}
#[derive(serde::Deserialize)]
struct BlockPrioritizationFeesNotificationContext {
slot: u64,
}
#[derive(serde::Deserialize)]
struct BlockPrioritizationFeesNotificationValue {
by_tx: Vec<u64>,
by_tx_percentiles: Vec<f64>,
by_cu: Vec<u64>,
by_cu_percentiles: Vec<f64>,
}
#[derive(serde::Deserialize)]
struct BlockPrioritizationFeesNotificationParams {
context: BlockPrioritizationFeesNotificationContext,
value: BlockPrioritizationFeesNotificationValue,
}
fn as_block_prioritization_fees_notification(
notification_str: &str,
) -> anyhow::Result<Option<BlockPrioFees>> {
let notification: Notification = match serde_json::from_str(&notification_str) {
Ok(v) => v,
Err(_) => return Ok(None), // not a notification at all
};
if notification.method != "blockPrioritizationFeesNotification" {
return Ok(None);
}
let map = match notification.params {
Params::Map(m) => m,
_ => anyhow::bail!("unexpected params, expected map"),
};
let result = map
.get("result")
.ok_or(anyhow::anyhow!("missing params.result"))?
.clone();
let mut data = BlockPrioFees::default();
let v: BlockPrioritizationFeesNotificationParams = serde_json::from_value(result)?;
data.slot = v.context.slot;
for (percentile, prio) in v.value.by_tx_percentiles.iter().zip(v.value.by_tx.iter()) {
let int_perc: u8 = ((percentile * 100.0) as u64).try_into()?;
data.percentile.insert(int_perc, *prio);
}
for (percentile, prio) in v.value.by_cu_percentiles.iter().zip(v.value.by_cu.iter()) {
let int_perc: u8 = ((percentile * 100.0) as u64).try_into()?;
data.by_cu_percentile.insert(int_perc, *prio);
}
Ok(Some(data))
}
async fn connect_and_broadcast(
url: &str,
sender: &broadcast::Sender<BlockPrioFees>,
) -> anyhow::Result<()> {
let (ws_stream, _) = connect_async(url).await?;
let (mut write, mut read) = ws_stream.split();
// Create a JSON-RPC request
let call = MethodCall {
jsonrpc: Some(Version::V2),
method: "blockPrioritizationFeesSubscribe".to_string(),
params: Params::None,
id: jsonrpc_core::Id::Num(1),
};
let request = serde_json::to_string(&call).unwrap();
write.send(Message::Text(request)).await?;
loop {
let timeout = tokio::time::sleep(Duration::from_secs(20));
tokio::select! {
message = read.next() => {
match message {
Some(Ok(Message::Text(text))) => {
if let Some(block_prio) = as_block_prioritization_fees_notification(&text)? {
// Failure might just mean there is no receiver right now
let _ = sender.send(block_prio);
}
}
Some(Ok(Message::Ping(..))) => {}
Some(Ok(Message::Pong(..))) => {}
Some(Ok(msg @ _)) => {
anyhow::bail!("received a non-text message: {:?}", msg);
},
Some(Err(e)) => {
anyhow::bail!("error receiving message: {}", e);
}
None => {
anyhow::bail!("websocket stream closed");
}
}
},
_ = timeout => {
anyhow::bail!("timeout");
}
}
}
}
async fn connect_and_broadcast_loop(url: &str, sender: broadcast::Sender<BlockPrioFees>) {
loop {
if let Err(err) = connect_and_broadcast(url, &sender).await {
info!("recent block prio feed error, restarting: {err:?}");
}
}
}
pub fn run_broadcast_from_websocket_feed(
url: String,
) -> (broadcast::Sender<BlockPrioFees>, JoinHandle<()>) {
let (sender, _) = broadcast::channel(10);
let sender_c = sender.clone();
let handle = tokio::spawn(async move { connect_and_broadcast_loop(&url, sender_c).await });
(sender, handle)
}

View File

@ -0,0 +1,80 @@
use std::sync::Arc;
use tokio::task::JoinHandle;
use tracing::*;
use crate::priority_fees::*;
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
enum PriorityFeeStyleArg {
None,
Fixed,
LiteRpcCuPercentileEma,
}
#[derive(clap::Args, Debug, Clone)]
pub struct PriorityFeeArgs {
/// choose prio fee style
#[clap(long, env, value_enum, default_value = "none")]
prioritization_style: PriorityFeeStyleArg,
/// prioritize each transaction with this many microlamports/cu
///
/// for dynamic prio styles, this is the fallback value
#[clap(long, env, default_value = "0")]
prioritization_micro_lamports: u64,
#[clap(long, env, default_value = "50")]
prioritization_ema_percentile: u8,
#[clap(long, env, default_value = "0.2")]
prioritization_ema_alpha: f64,
}
impl PriorityFeeArgs {
pub fn make_prio_provider(
&self,
lite_rpc_url: String,
) -> anyhow::Result<(Option<Arc<dyn PriorityFeeProvider>>, Vec<JoinHandle<()>>)> {
let prio_style;
if self.prioritization_micro_lamports > 0
&& self.prioritization_style == PriorityFeeStyleArg::None
{
info!("forcing prioritization-style to fixed, since prioritization-micro-lamports was set");
prio_style = PriorityFeeStyleArg::Fixed;
} else {
prio_style = self.prioritization_style;
}
Ok(match prio_style {
PriorityFeeStyleArg::None => (None, vec![]),
PriorityFeeStyleArg::Fixed => (
Some(Arc::new(FixedPriorityFeeProvider::new(
self.prioritization_micro_lamports,
))),
vec![],
),
PriorityFeeStyleArg::LiteRpcCuPercentileEma => {
if lite_rpc_url.is_empty() {
anyhow::bail!("cannot use recent-cu-percentile-ema prioritization style without a lite-rpc url");
}
let (block_prio_broadcaster, block_prio_job) =
run_broadcast_from_websocket_feed(lite_rpc_url);
let (prio_fee_provider, prio_fee_provider_job) =
CuPercentileEmaPriorityFeeProvider::run(
EmaPriorityFeeProviderConfig::builder()
.percentile(75)
.fallback_prio(self.prioritization_micro_lamports)
.alpha(self.prioritization_ema_alpha)
.percentile(self.prioritization_ema_percentile)
.build()
.unwrap(),
&block_prio_broadcaster,
);
(
Some(prio_fee_provider),
vec![block_prio_job, prio_fee_provider_job],
)
}
})
}
}

View File

@ -1,17 +1,8 @@
use solana_client::{
client_error::Result as ClientResult, rpc_client::RpcClient, rpc_request::RpcError,
};
use solana_sdk::compute_budget::ComputeBudgetInstruction;
use solana_sdk::instruction::Instruction;
use solana_sdk::transaction::Transaction;
use solana_sdk::{
clock::Slot, commitment_config::CommitmentConfig, signature::Signature,
transaction::uses_durable_nonce,
};
use anchor_lang::prelude::{AccountMeta, Pubkey};
use anyhow::Context;
use std::{thread, time};
/// Some Result<> types don't convert to anyhow::Result nicely. Force them through stringification.
pub trait AnyhowWrap {
@ -57,67 +48,6 @@ pub fn delay_interval(period: std::time::Duration) -> tokio::time::Interval {
interval
}
/// A copy of RpcClient::send_and_confirm_transaction that returns the slot the
/// transaction confirmed in.
pub fn send_and_confirm_transaction(
rpc_client: &RpcClient,
transaction: &Transaction,
) -> ClientResult<(Signature, Slot)> {
const SEND_RETRIES: usize = 1;
const GET_STATUS_RETRIES: usize = usize::MAX;
'sending: for _ in 0..SEND_RETRIES {
let signature = rpc_client.send_transaction(transaction)?;
let recent_blockhash = if uses_durable_nonce(transaction).is_some() {
let (recent_blockhash, ..) =
rpc_client.get_latest_blockhash_with_commitment(CommitmentConfig::processed())?;
recent_blockhash
} else {
transaction.message.recent_blockhash
};
for status_retry in 0..GET_STATUS_RETRIES {
let response = rpc_client.get_signature_statuses(&[signature])?.value;
match response[0]
.clone()
.filter(|result| result.satisfies_commitment(rpc_client.commitment()))
{
Some(tx_status) => {
return if let Some(e) = tx_status.err {
Err(e.into())
} else {
Ok((signature, tx_status.slot))
};
}
None => {
if !rpc_client
.is_blockhash_valid(&recent_blockhash, CommitmentConfig::processed())?
{
// Block hash is not found by some reason
break 'sending;
} else if cfg!(not(test))
// Ignore sleep at last step.
&& status_retry < GET_STATUS_RETRIES
{
// Retry twice a second
thread::sleep(time::Duration::from_millis(500));
continue;
}
}
}
}
}
Err(RpcError::ForUser(
"unable to confirm transaction. \
This can happen in situations such as transaction expiration \
and insufficient fee-payer funds"
.to_string(),
)
.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() {

View File

@ -1,5 +1,9 @@
{
<<<<<<< HEAD
"version": "0.22.0",
=======
"version": "0.23.0",
>>>>>>> main
"name": "mango_v4",
"instructions": [
{
@ -277,6 +281,12 @@
"type": {
"option": "u16"
}
},
{
"name": "collateralFeeIntervalOpt",
"type": {
"option": "u64"
}
}
]
},
@ -631,6 +641,17 @@
{
"name": "platformLiquidationFee",
"type": "f32"
<<<<<<< HEAD
=======
},
{
"name": "disableAssetLiquidation",
"type": "bool"
},
{
"name": "collateralFeePerDay",
"type": "f32"
>>>>>>> main
}
]
},
@ -1041,6 +1062,27 @@
"type": {
"option": "f32"
}
<<<<<<< HEAD
=======
},
{
"name": "disableAssetLiquidationOpt",
"type": {
"option": "bool"
}
},
{
"name": "collateralFeePerDayOpt",
"type": {
"option": "f32"
}
},
{
"name": "forceWithdrawOpt",
"type": {
"option": "bool"
}
>>>>>>> main
}
]
},
@ -3763,6 +3805,63 @@
}
]
},
{
"name": "tokenForceWithdraw",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group"
]
},
{
"name": "bank",
"isMut": true,
"isSigner": false,
"relations": [
"group",
"vault",
"oracle"
]
},
{
"name": "vault",
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
},
{
"name": "ownerAtaTokenAccount",
"isMut": true,
"isSigner": false
},
{
"name": "alternateOwnerTokenAccount",
"isMut": true,
"isSigner": false,
"docs": [
"Only for the unusual case where the owner_ata account is not owned by account.owner"
]
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": []
},
{
"name": "perpCreateMarket",
"docs": [
@ -5953,6 +6052,25 @@
}
]
},
{
"name": "tokenChargeCollateralFees",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group"
]
}
],
"args": []
},
{
"name": "altSet",
"accounts": [
@ -7373,12 +7491,24 @@
"name": "forceClose",
"type": "u8"
},
{
"name": "disableAssetLiquidation",
"docs": [
"If set to 1, deposits cannot be liquidated when an account is liquidatable.",
"That means bankrupt accounts may still have assets of this type deposited."
],
"type": "u8"
},
{
"name": "forceWithdraw",
"type": "u8"
},
{
"name": "padding",
"type": {
"array": [
"u8",
6
4
]
}
},
@ -7514,11 +7644,36 @@
}
},
{
<<<<<<< HEAD
=======
"name": "collectedCollateralFees",
"docs": [
"Collateral fees that have been collected (in native tokens)",
"",
"See also collected_fees_native and fees_withdrawn."
],
"type": {
"defined": "I80F48"
}
},
{
"name": "collateralFeePerDay",
"docs": [
"The daily collateral fees rate for fully utilized collateral."
],
"type": "f32"
},
{
>>>>>>> main
"name": "reserved",
"type": {
"array": [
"u8",
<<<<<<< HEAD
1920
=======
1900
>>>>>>> main
]
}
}
@ -7646,12 +7801,28 @@
],
"type": "u16"
},
{
"name": "padding2",
"type": {
"array": [
"u8",
4
]
}
},
{
"name": "collateralFeeInterval",
"docs": [
"Intervals in which collateral fee is applied"
],
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
1812
1800
]
}
}
@ -7773,12 +7944,27 @@
],
"type": "u64"
},
{
"name": "temporaryDelegate",
"type": "publicKey"
},
{
"name": "temporaryDelegateExpiry",
"type": "u64"
},
{
"name": "lastCollateralFeeCharge",
"docs": [
"Time at which the last collateral fee was charged"
],
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
200
152
]
}
},
@ -9548,12 +9734,16 @@
"name": "temporaryDelegateExpiry",
"type": "u64"
},
{
"name": "lastCollateralFeeCharge",
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
160
152
]
}
}
@ -10474,6 +10664,9 @@
},
{
"name": "Swap"
},
{
"name": "SwapWithoutFee"
}
]
}
@ -10829,6 +11022,9 @@
},
{
"name": "Serum3PlaceOrderV2"
},
{
"name": "TokenForceWithdraw"
}
]
}
@ -13746,6 +13942,76 @@
"index": false
}
]
},
{
"name": "TokenCollateralFeeLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mangoAccount",
"type": "publicKey",
"index": false
},
{
"name": "tokenIndex",
"type": "u16",
"index": false
},
{
"name": "assetUsageFraction",
"type": "i128",
"index": false
},
{
"name": "fee",
"type": "i128",
"index": false
},
{
"name": "price",
"type": "i128",
"index": false
}
]
},
{
"name": "ForceWithdrawLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mangoAccount",
"type": "publicKey",
"index": false
},
{
"name": "tokenIndex",
"type": "u16",
"index": false
},
{
"name": "quantity",
"type": "u64",
"index": false
},
{
"name": "price",
"type": "i128",
"index": false
},
{
"name": "toTokenAccount",
"type": "publicKey",
"index": false
}
]
}
],
"errors": [
@ -14093,6 +14359,11 @@
"code": 6068,
"name": "MissingFeedForCLMMOracle",
"msg": "Pyth USDC/USD or SOL/USD feed not found (required by CLMM oracle)"
},
{
"code": 6069,
"name": "TokenAssetLiquidationDisabled",
"msg": "the asset does not allow liquidation"
}
]
}

View File

@ -1,6 +1,6 @@
[package]
name = "mango-v4"
version = "0.22.0"
version = "0.23.0"
description = "Created with Anchor"
edition = "2021"
@ -32,7 +32,11 @@ borsh = { version = "0.10.3", features = ["const-generics"] }
bytemuck = { version = "^1.7.2", features = ["min_const_generics"] }
default-env = "0.1.1"
derivative = "2.2.0"
fixed = { workspace = true, features = ["serde", "borsh", "debug-assert-in-release"] }
fixed = { workspace = true, features = [
"serde",
"borsh",
"debug-assert-in-release",
] }
num_enum = "0.5.1"
pyth-sdk-solana = { workspace = true }
serde = "^1.0"
@ -48,7 +52,9 @@ switchboard-program = "0.2"
switchboard-v2 = { package = "switchboard-solana", version = "0.28" }
openbook-v2 = { git = "https://github.com/openbook-dex/openbook-v2.git", features = ["no-entrypoint"] }
openbook-v2 = { git = "https://github.com/openbook-dex/openbook-v2.git", features = [
"no-entrypoint",
] }
[dev-dependencies]
@ -56,7 +62,9 @@ solana-sdk = { workspace = true, default-features = false }
solana-program-test = { workspace = true }
solana-logger = { workspace = true }
spl-token = { version = "^3.0.0", features = ["no-entrypoint"] }
spl-associated-token-account = { version = "^1.0.3", features = ["no-entrypoint"] }
spl-associated-token-account = { version = "^1.0.3", features = [
"no-entrypoint",
] }
bincode = "^1.3.1"
log = "0.4.14"
env_logger = "0.9.0"

View File

@ -92,6 +92,12 @@ pub struct FlashLoanEnd<'info> {
#[derive(PartialEq, Copy, Clone, Debug, AnchorSerialize, AnchorDeserialize)]
#[repr(u8)]
pub enum FlashLoanType {
/// An arbitrary flash loan
Unknown,
/// A flash loan used for a swap where one token is exchanged for another.
///
/// Deposits in this type get charged the flash_loan_swap_fee_rate
Swap,
/// Like Swap, but without the flash_loan_swap_fee_rate
SwapWithoutFee,
}

View File

@ -59,6 +59,7 @@ pub use stub_oracle_close::*;
pub use stub_oracle_create::*;
pub use stub_oracle_set::*;
pub use token_add_bank::*;
pub use token_charge_collateral_fees::*;
pub use token_conditional_swap_cancel::*;
pub use token_conditional_swap_create::*;
pub use token_conditional_swap_start::*;
@ -67,6 +68,7 @@ pub use token_deposit::*;
pub use token_deregister::*;
pub use token_edit::*;
pub use token_force_close_borrows_with_token::*;
pub use token_force_withdraw::*;
pub use token_liq_bankruptcy::*;
pub use token_liq_with_token::*;
pub use token_register::*;
@ -135,6 +137,7 @@ mod stub_oracle_close;
mod stub_oracle_create;
mod stub_oracle_set;
mod token_add_bank;
mod token_charge_collateral_fees;
mod token_conditional_swap_cancel;
mod token_conditional_swap_create;
mod token_conditional_swap_start;
@ -143,6 +146,7 @@ mod token_deposit;
mod token_deregister;
mod token_edit;
mod token_force_close_borrows_with_token;
mod token_force_withdraw;
mod token_liq_bankruptcy;
mod token_liq_with_token;
mod token_register;

View File

@ -0,0 +1,16 @@
use crate::error::MangoError;
use crate::state::*;
use anchor_lang::prelude::*;
/// Charges collateral fees on an account
#[derive(Accounts)]
pub struct TokenChargeCollateralFees<'info> {
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = group,
constraint = account.load()?.is_operational() @ MangoError::AccountIsFrozen
)]
pub account: AccountLoader<'info, MangoAccountFixed>,
}

View File

@ -0,0 +1,54 @@
use anchor_lang::prelude::*;
use anchor_spl::associated_token::get_associated_token_address;
use anchor_spl::token::Token;
use anchor_spl::token::TokenAccount;
use crate::error::*;
use crate::state::*;
#[derive(Accounts)]
pub struct TokenForceWithdraw<'info> {
#[account(
constraint = group.load()?.is_ix_enabled(IxGate::TokenForceWithdraw) @ MangoError::IxIsDisabled,
)]
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = group,
constraint = account.load()?.is_operational() @ MangoError::AccountIsFrozen,
)]
pub account: AccountLoader<'info, MangoAccountFixed>,
#[account(
mut,
has_one = group,
has_one = vault,
has_one = oracle,
// the mints of bank/vault/token_accounts are implicitly the same because
// spl::token::transfer succeeds between token_account and vault
)]
pub bank: AccountLoader<'info, Bank>,
#[account(mut)]
pub vault: Box<Account<'info, TokenAccount>>,
/// CHECK: The oracle can be one of several different account types
pub oracle: UncheckedAccount<'info>,
#[account(
mut,
address = get_associated_token_address(&account.load()?.owner, &vault.mint),
// NOTE: the owner may have been changed (before immutable owner was a thing)
)]
pub owner_ata_token_account: Box<Account<'info, TokenAccount>>,
/// Only for the unusual case where the owner_ata account is not owned by account.owner
#[account(
mut,
constraint = alternate_owner_token_account.owner == account.load()?.owner,
)]
pub alternate_owner_token_account: Box<Account<'info, TokenAccount>>,
pub token_program: Program<'info, Token>,
}

View File

@ -143,6 +143,8 @@ pub enum MangoError {
InvalidFeedForCLMMOracle,
#[msg("Pyth USDC/USD or SOL/USD feed not found (required by CLMM oracle)")]
MissingFeedForCLMMOracle,
#[msg("the asset does not allow liquidation")]
TokenAssetLiquidationDisabled,
}
impl MangoError {

View File

@ -59,7 +59,7 @@ pub struct FixedOrderAccountRetriever<T: KeyedAccountReader> {
pub begin_serum3: usize,
pub staleness_slot: Option<u64>,
pub begin_fallback_oracles: usize,
pub usd_oracle_index: Option<usize>,
pub usdc_oracle_index: Option<usize>,
pub sol_oracle_index: Option<usize>,
}
@ -78,7 +78,7 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
ais.len(), expected_ais,
active_token_len, active_token_len, active_perp_len, active_perp_len, active_serum3_len
);
let usd_oracle_index = ais[..]
let usdc_oracle_index = ais[..]
.iter()
.position(|o| o.key == &pyth_mainnet_usdc_oracle::ID);
let sol_oracle_index = ais[..]
@ -93,7 +93,7 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
begin_serum3: active_token_len * 2 + active_perp_len * 2,
staleness_slot: Some(Clock::get()?.slot),
begin_fallback_oracles: expected_ais,
usd_oracle_index,
usdc_oracle_index,
sol_oracle_index,
})
}
@ -139,7 +139,7 @@ impl<T: KeyedAccountReader> FixedOrderAccountRetriever<T> {
OracleAccountInfos {
oracle,
fallback_opt,
usd_opt: self.usd_oracle_index.map(|i| &self.ais[i]),
usdc_opt: self.usdc_oracle_index.map(|i| &self.ais[i]),
sol_opt: self.sol_oracle_index.map(|i| &self.ais[i]),
}
}
@ -324,7 +324,7 @@ impl<'a, 'info> ScannedBanksAndOracles<'a, 'info> {
OracleAccountInfos {
oracle,
fallback_opt,
usd_opt: self.usd_oracle_index.map(|i| &self.fallback_oracles[i]),
usdc_opt: self.usd_oracle_index.map(|i| &self.fallback_oracles[i]),
sol_opt: self.sol_oracle_index.map(|i| &self.fallback_oracles[i]),
}
}

View File

@ -175,6 +175,8 @@ pub struct TokenInfo {
/// Includes TokenPosition and free Serum3OpenOrders balances.
/// Does not include perp upnl or Serum3 reserved amounts.
pub balance_spot: I80F48,
pub allow_asset_liquidation: bool,
}
/// Temporary value used during health computations
@ -907,6 +909,7 @@ impl HealthCache {
}
/// Liquidatable spot assets mean: actual token deposits and also a positive effective token balance
/// and is available for asset liquidation
pub fn has_liq_spot_assets(&self) -> bool {
let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd);
self.token_infos
@ -914,11 +917,11 @@ impl HealthCache {
.zip(health_token_balances.iter())
.any(|(ti, b)| {
// need 1 native token to use token_liq_with_token
ti.balance_spot >= 1 && b.spot_and_perp >= 1
ti.balance_spot >= 1 && b.spot_and_perp >= 1 && ti.allow_asset_liquidation
})
}
/// Liquidatable spot borrows mean: actual toen borrows plus a negative effective token balance
/// Liquidatable spot borrows mean: actual token borrows plus a negative effective token balance
pub fn has_liq_spot_borrows(&self) -> bool {
let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd);
self.token_infos
@ -932,7 +935,9 @@ impl HealthCache {
let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd);
let all_iter = || self.token_infos.iter().zip(health_token_balances.iter());
all_iter().any(|(ti, b)| ti.balance_spot < 0 && b.spot_and_perp < 0)
&& all_iter().any(|(ti, b)| ti.balance_spot >= 1 && b.spot_and_perp >= 1)
&& all_iter().any(|(ti, b)| {
ti.balance_spot >= 1 && b.spot_and_perp >= 1 && ti.allow_asset_liquidation
})
}
pub fn has_serum3_open_orders_funds(&self) -> bool {
@ -1286,6 +1291,7 @@ fn new_health_cache_impl(
init_scaled_liab_weight: bank.scaled_init_liab_weight(liab_price),
prices,
balance_spot: native,
allow_asset_liquidation: bank.allows_asset_liquidation(),
});
}

View File

@ -682,6 +682,7 @@ mod tests {
init_scaled_liab_weight: I80F48::from_num(1.0 + x),
prices: Prices::new_single_price(I80F48::from_num(price)),
balance_spot: I80F48::ZERO,
allow_asset_liquidation: true,
}
}
@ -1461,27 +1462,49 @@ mod tests {
I80F48::ZERO
);
let find_max_borrow = |c: &HealthCache, ratio: f64| {
let max_borrow = c
.max_borrow_for_health_ratio(&account, bank0_data, I80F48::from_num(ratio))
.unwrap();
// compute the health ratio we'd get when executing the trade
let actual_ratio = {
let mut c = c.clone();
c.token_infos[0].balance_spot -= max_borrow;
c.health_ratio(HealthType::Init).to_num::<f64>()
};
// the ratio for borrowing one native token extra
let plus_ratio = {
let mut c = c.clone();
c.token_infos[0].balance_spot -= max_borrow + I80F48::ONE;
c.health_ratio(HealthType::Init).to_num::<f64>()
};
(max_borrow, actual_ratio, plus_ratio)
let now_ts = system_epoch_secs();
let cache_after_borrow = |account: &MangoAccountValue,
c: &HealthCache,
bank: &Bank,
amount: I80F48|
-> Result<HealthCache> {
let mut position = account.token_position(bank.token_index)?.clone();
let mut bank = bank.clone();
bank.withdraw_with_fee(&mut position, amount, now_ts)?;
bank.check_net_borrows(c.token_info(bank.token_index)?.prices.oracle)?;
let mut resulting_cache = c.clone();
resulting_cache.adjust_token_balance(&bank, -amount)?;
Ok(resulting_cache)
};
let check_max_borrow = |c: &HealthCache, ratio: f64| -> f64 {
let find_max_borrow =
|account: &MangoAccountValue, c: &HealthCache, ratio: f64, bank: &Bank| {
let max_borrow = c
.max_borrow_for_health_ratio(account, bank, I80F48::from_num(ratio))
.unwrap();
// compute the health ratio we'd get when executing the trade
let actual_ratio = {
let c = cache_after_borrow(account, c, bank, max_borrow).unwrap();
c.health_ratio(HealthType::Init).to_num::<f64>()
};
// the ratio for borrowing one native token extra
let plus_ratio = {
let c = cache_after_borrow(account, c, bank, max_borrow + I80F48::ONE).unwrap();
c.health_ratio(HealthType::Init).to_num::<f64>()
};
(max_borrow, actual_ratio, plus_ratio)
};
let check_max_borrow = |account: &MangoAccountValue,
c: &HealthCache,
ratio: f64,
bank: &Bank|
-> f64 {
let initial_ratio = c.health_ratio(HealthType::Init).to_num::<f64>();
let (max_borrow, actual_ratio, plus_ratio) = find_max_borrow(c, ratio);
let (max_borrow, actual_ratio, plus_ratio) = find_max_borrow(account, c, ratio, bank);
println!(
"checking target ratio {ratio}: initial ratio: {initial_ratio}, actual ratio: {actual_ratio}, plus ratio: {plus_ratio}, borrow: {max_borrow}",
);
@ -1496,30 +1519,66 @@ mod tests {
{
let mut health_cache = health_cache.clone();
health_cache.token_infos[0].balance_spot = I80F48::from_num(100.0);
assert_eq!(check_max_borrow(&health_cache, 50.0), 100.0);
assert_eq!(
check_max_borrow(&account, &health_cache, 50.0, bank0_data),
100.0
);
}
{
let mut health_cache = health_cache.clone();
health_cache.token_infos[1].balance_spot = I80F48::from_num(50.0); // price 2, so 2*50*0.8 = 80 health
check_max_borrow(&health_cache, 100.0);
check_max_borrow(&health_cache, 50.0);
check_max_borrow(&health_cache, 0.0);
check_max_borrow(&account, &health_cache, 100.0, bank0_data);
check_max_borrow(&account, &health_cache, 50.0, bank0_data);
check_max_borrow(&account, &health_cache, 0.0, bank0_data);
}
{
let mut health_cache = health_cache.clone();
health_cache.token_infos[0].balance_spot = I80F48::from_num(50.0);
health_cache.token_infos[1].balance_spot = I80F48::from_num(50.0);
check_max_borrow(&health_cache, 100.0);
check_max_borrow(&health_cache, 50.0);
check_max_borrow(&health_cache, 0.0);
check_max_borrow(&account, &health_cache, 100.0, bank0_data);
check_max_borrow(&account, &health_cache, 50.0, bank0_data);
check_max_borrow(&account, &health_cache, 0.0, bank0_data);
}
{
let mut health_cache = health_cache.clone();
health_cache.token_infos[0].balance_spot = I80F48::from_num(-50.0);
health_cache.token_infos[1].balance_spot = I80F48::from_num(50.0);
check_max_borrow(&health_cache, 100.0);
check_max_borrow(&health_cache, 50.0);
check_max_borrow(&health_cache, 0.0);
check_max_borrow(&account, &health_cache, 100.0, bank0_data);
check_max_borrow(&account, &health_cache, 50.0, bank0_data);
check_max_borrow(&account, &health_cache, 0.0, bank0_data);
}
// A test that includes init weight scaling
{
let mut account = account.clone();
let mut bank0 = bank0_data.clone();
let mut health_cache = health_cache.clone();
let tok0_deposits = I80F48::from_num(500.0);
health_cache.token_infos[0].balance_spot = tok0_deposits;
health_cache.token_infos[1].balance_spot = I80F48::from_num(-100.0); // 2 * 100 * 1.2 = 240 liab
// This test case needs the bank to know about the deposits
let position = account.token_position_mut(bank0.token_index).unwrap().0;
bank0.deposit(position, tok0_deposits, now_ts).unwrap();
// Set up scaling such that token0 health contrib is 500 * 1.0 * 1.0 * (600 / (500 + 300)) = 375
bank0.deposit_weight_scale_start_quote = 600.0;
bank0.potential_serum_tokens = 300;
health_cache.token_infos[0].init_scaled_asset_weight =
bank0.scaled_init_asset_weight(I80F48::ONE);
check_max_borrow(&account, &health_cache, 100.0, &bank0);
check_max_borrow(&account, &health_cache, 50.0, &bank0);
let max_borrow = check_max_borrow(&account, &health_cache, 0.0, &bank0);
// that borrow leaves 240 tokens in the account and <600 total in bank
assert!((260.0 - max_borrow).abs() < 0.3);
bank0.deposit_weight_scale_start_quote = 500.0;
let max_borrow = check_max_borrow(&account, &health_cache, 0.0, &bank0);
// 500 - 222.6 = 277.4 remaining token 0 deposits
// 277.4 * 500 / (277.4 + 300) = 240.2 (compensating the -240 liab)
assert!((222.6 - max_borrow).abs() < 0.3);
}
}

View File

@ -337,7 +337,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
// Create the token position now, so we can compute the pre-health with fixed order health accounts
let (_, raw_token_index, _) = account.ensure_token_position(bank.token_index)?;
// Transfer any excess over the inital balance of the token account back
// Transfer any excess over the initial balance of the token account back
// into the vault. Compute the total change in the vault balance.
let mut change = -I80F48::from(bank.flash_loan_approved_amount);
if token_account.amount > bank.flash_loan_token_account_initial {
@ -378,10 +378,10 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
match flash_loan_type {
FlashLoanType::Unknown => {}
FlashLoanType::Swap => {
FlashLoanType::Swap | FlashLoanType::SwapWithoutFee => {
require_msg!(
changes.len() == 2,
"when flash_loan_type is Swap there must be exactly 2 token vault changes"
"when flash_loan_type is Swap or SwapWithoutFee there must be exactly 2 token vault changes"
)
}
}

View File

@ -19,6 +19,7 @@ pub fn group_edit(
mngo_token_index_opt: Option<TokenIndex>,
buyback_fees_expiry_interval_opt: Option<u64>,
allowed_fast_listings_per_interval_opt: Option<u16>,
collateral_fee_interval_opt: Option<u64>,
) -> Result<()> {
let mut group = ctx.accounts.group.load_mut()?;
@ -116,5 +117,14 @@ pub fn group_edit(
group.allowed_fast_listings_per_interval = allowed_fast_listings_per_interval;
}
if let Some(collateral_fee_interval) = collateral_fee_interval_opt {
msg!(
"Collateral fee interval old {:?}, new {:?}",
group.collateral_fee_interval,
collateral_fee_interval
);
group.collateral_fee_interval = collateral_fee_interval;
}
Ok(())
}

View File

@ -95,6 +95,7 @@ pub fn ix_gate_set(ctx: Context<IxGateSet>, ix_gate: u128) -> Result<()> {
IxGate::TokenConditionalSwapCreateLinearAuction,
);
log_if_changed(&group, ix_gate, IxGate::Serum3PlaceOrderV2);
log_if_changed(&group, ix_gate, IxGate::TokenForceWithdraw);
group.ix_gate = ix_gate;

View File

@ -50,6 +50,7 @@ pub use stub_oracle_close::*;
pub use stub_oracle_create::*;
pub use stub_oracle_set::*;
pub use token_add_bank::*;
pub use token_charge_collateral_fees::*;
pub use token_conditional_swap_cancel::*;
pub use token_conditional_swap_create::*;
pub use token_conditional_swap_start::*;
@ -58,6 +59,7 @@ pub use token_deposit::*;
pub use token_deregister::*;
pub use token_edit::*;
pub use token_force_close_borrows_with_token::*;
pub use token_force_withdraw::*;
pub use token_liq_bankruptcy::*;
pub use token_liq_with_token::*;
pub use token_register::*;
@ -117,6 +119,7 @@ mod stub_oracle_close;
mod stub_oracle_create;
mod stub_oracle_set;
mod token_add_bank;
mod token_charge_collateral_fees;
mod token_conditional_swap_cancel;
mod token_conditional_swap_create;
mod token_conditional_swap_start;
@ -125,6 +128,7 @@ mod token_deposit;
mod token_deregister;
mod token_edit;
mod token_force_close_borrows_with_token;
mod token_force_withdraw;
mod token_liq_bankruptcy;
mod token_liq_with_token;
mod token_register;

View File

@ -0,0 +1,129 @@
use crate::accounts_zerocopy::*;
use crate::health::*;
use crate::state::*;
use anchor_lang::prelude::*;
use fixed::types::I80F48;
use crate::accounts_ix::*;
use crate::logs::{emit_stack, TokenBalanceLog, TokenCollateralFeeLog};
pub fn token_charge_collateral_fees(ctx: Context<TokenChargeCollateralFees>) -> Result<()> {
let group = ctx.accounts.group.load()?;
let mut account = ctx.accounts.account.load_full_mut()?;
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
if group.collateral_fee_interval == 0 {
// By resetting, a new enabling of collateral fees will not immediately create a charge
account.fixed.last_collateral_fee_charge = 0;
return Ok(());
}
// When collateral fees are enabled the first time, don't immediately charge
if account.fixed.last_collateral_fee_charge == 0 {
account.fixed.last_collateral_fee_charge = now_ts;
return Ok(());
}
// Is the next fee-charging due?
let last_charge_ts = account.fixed.last_collateral_fee_charge;
if now_ts < last_charge_ts + group.collateral_fee_interval {
return Ok(());
}
account.fixed.last_collateral_fee_charge = now_ts;
// Charge the user at most for 2x the interval. So if no one calls this for a long time
// there won't be a huge charge based only on the end state.
let charge_seconds = (now_ts - last_charge_ts).min(2 * group.collateral_fee_interval);
// The fees are configured in "interest per day" so we need to get the fraction of days
// that has passed since the last update for scaling
let inv_seconds_per_day = I80F48::from_num(1.157407407407e-5); // 1 / (24 * 60 * 60)
let time_scaling = I80F48::from(charge_seconds) * inv_seconds_per_day;
let health_cache = {
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
new_health_cache(&account.borrow(), &retriever, now_ts)?
};
// We want to find the total asset health and total liab health, but don't want
// to treat borrows that moved into open orders accounts as realized. Hence we
// pretend all spot orders are closed and settled and add their funds back to
// the token positions.
let mut token_balances = health_cache.effective_token_balances(HealthType::Maint);
for s3info in health_cache.serum3_infos.iter() {
token_balances[s3info.base_info_index].spot_and_perp += s3info.reserved_base;
token_balances[s3info.quote_info_index].spot_and_perp += s3info.reserved_quote;
}
let mut total_liab_health = I80F48::ZERO;
let mut total_asset_health = I80F48::ZERO;
for (info, balance) in health_cache.token_infos.iter().zip(token_balances.iter()) {
let health = info.health_contribution(HealthType::Maint, balance.spot_and_perp);
if health.is_positive() {
total_asset_health += health;
} else {
total_liab_health -= health;
}
}
// If there's no assets or no liabs, we can't charge fees
if total_asset_health.is_zero() || total_liab_health.is_zero() {
return Ok(());
}
// Users only pay for assets that are actively used to cover their liabilities.
let asset_usage_scaling = (total_liab_health / total_asset_health)
.max(I80F48::ZERO)
.min(I80F48::ONE);
let scaling = asset_usage_scaling * time_scaling;
let token_position_count = account.active_token_positions().count();
for bank_ai in &ctx.remaining_accounts[0..token_position_count] {
let mut bank = bank_ai.load_mut::<Bank>()?;
if bank.collateral_fee_per_day <= 0.0 || bank.maint_asset_weight.is_zero() {
continue;
}
let (token_position, raw_token_index) = account.token_position_mut(bank.token_index)?;
let token_balance = token_position.native(&bank);
if token_balance <= 0 {
continue;
}
let fee = token_balance * scaling * I80F48::from_num(bank.collateral_fee_per_day);
assert!(fee <= token_balance);
let is_active = bank.withdraw_without_fee(token_position, fee, now_ts)?;
if !is_active {
account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key());
}
bank.collected_fees_native += fee;
bank.collected_collateral_fees += fee;
let token_info = health_cache.token_info(bank.token_index)?;
let token_position = account.token_position(bank.token_index)?;
emit_stack(TokenCollateralFeeLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
token_index: bank.token_index,
fee: fee.to_bits(),
asset_usage_fraction: asset_usage_scaling.to_bits(),
price: token_info.prices.oracle.to_bits(),
});
emit_stack(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
token_index: bank.token_index,
indexed_position: token_position.indexed_position.to_bits(),
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
})
}
Ok(())
}

View File

@ -53,6 +53,9 @@ pub fn token_edit(
deposit_limit_opt: Option<u64>,
zero_util_rate: Option<f32>,
platform_liquidation_fee: Option<f32>,
disable_asset_liquidation_opt: Option<bool>,
collateral_fee_per_day: Option<f32>,
force_withdraw_opt: Option<bool>,
) -> Result<()> {
let group = ctx.accounts.group.load()?;
@ -482,6 +485,43 @@ pub fn token_edit(
platform_liquidation_fee
);
bank.platform_liquidation_fee = I80F48::from_num(platform_liquidation_fee);
<<<<<<< HEAD
=======
if platform_liquidation_fee != 0.0 {
require_group_admin = true;
}
}
if let Some(collateral_fee_per_day) = collateral_fee_per_day {
msg!(
"Collateral fee per day old {:?}, new {:?}",
bank.collateral_fee_per_day,
collateral_fee_per_day
);
bank.collateral_fee_per_day = collateral_fee_per_day;
if collateral_fee_per_day != 0.0 {
require_group_admin = true;
}
}
if let Some(disable_asset_liquidation) = disable_asset_liquidation_opt {
msg!(
"Asset liquidation disabled old {:?}, new {:?}",
bank.disable_asset_liquidation,
disable_asset_liquidation
);
bank.disable_asset_liquidation = u8::from(disable_asset_liquidation);
require_group_admin = true;
}
if let Some(force_withdraw) = force_withdraw_opt {
msg!(
"Force withdraw old {:?}, new {:?}",
bank.force_withdraw,
force_withdraw
);
bank.force_withdraw = u8::from(force_withdraw);
>>>>>>> main
require_group_admin = true;
}
}

View File

@ -0,0 +1,100 @@
use crate::accounts_zerocopy::AccountInfoRef;
use crate::error::*;
use crate::state::*;
use anchor_lang::prelude::*;
use anchor_spl::token;
use fixed::types::I80F48;
use crate::accounts_ix::*;
use crate::logs::{emit_stack, ForceWithdrawLog, TokenBalanceLog};
pub fn token_force_withdraw(ctx: Context<TokenForceWithdraw>) -> Result<()> {
let group = ctx.accounts.group.load()?;
let token_index = ctx.accounts.bank.load()?.token_index;
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
let mut bank = ctx.accounts.bank.load_mut()?;
require!(bank.is_force_withdraw(), MangoError::SomeError);
let mut account = ctx.accounts.account.load_full_mut()?;
let withdraw_target = if ctx.accounts.owner_ata_token_account.owner == account.fixed.owner {
ctx.accounts.owner_ata_token_account.to_account_info()
} else {
ctx.accounts.alternate_owner_token_account.to_account_info()
};
let (position, raw_token_index) = account.token_position_mut(token_index)?;
let native_position = position.native(&bank);
// Check >= to allow calling this on 0 deposits to close the token position
require_gte!(native_position, I80F48::ZERO);
let amount = native_position.floor().to_num::<u64>();
let amount_i80f48 = I80F48::from(amount);
// Update the bank and position
let position_is_active = bank.withdraw_without_fee(position, amount_i80f48, now_ts)?;
// Provide a readable error message in case the vault doesn't have enough tokens
if ctx.accounts.vault.amount < amount {
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
format!(
"bank vault does not have enough tokens, need {} but have {}",
amount, ctx.accounts.vault.amount
)
});
}
// Transfer the actual tokens
let group_seeds = group_seeds!(group);
token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: ctx.accounts.vault.to_account_info(),
to: withdraw_target.clone(),
authority: ctx.accounts.group.to_account_info(),
},
)
.with_signer(&[group_seeds]),
amount,
)?;
emit_stack(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
token_index,
indexed_position: position.indexed_position.to_bits(),
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
});
// Get the oracle price, even if stale or unconfident: We want to allow force withdraws
// even if the oracle is bad.
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
let unsafe_oracle_state = oracle_state_unchecked(
&OracleAccountInfos::from_reader(oracle_ref),
bank.mint_decimals,
)?;
// Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms)
let amount_usd = (amount_i80f48 * unsafe_oracle_state.price).to_num::<i64>();
account.fixed.net_deposits -= amount_usd;
if !position_is_active {
account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key());
}
emit_stack(ForceWithdrawLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
token_index,
quantity: amount,
price: unsafe_oracle_state.price.to_bits(),
to_token_account: withdraw_target.key(),
});
bank.enforce_borrows_lte_deposits()?;
Ok(())
}

View File

@ -112,6 +112,10 @@ pub(crate) fn liquidation_action(
liqee.token_position_and_raw_index(asset_token_index)?;
let liqee_asset_native = liqee_asset_position.native(asset_bank);
require_gt!(liqee_asset_native, 0);
require!(
asset_bank.allows_asset_liquidation(),
MangoError::TokenAssetLiquidationDisabled
);
let (liqee_liab_position, liqee_liab_raw_index) =
liqee.token_position_and_raw_index(liab_token_index)?;

View File

@ -44,6 +44,8 @@ pub fn token_register(
deposit_limit: u64,
zero_util_rate: f32,
platform_liquidation_fee: f32,
disable_asset_liquidation: bool,
collateral_fee_per_day: f32,
) -> Result<()> {
// Require token 0 to be in the insurance token
if token_index == INSURANCE_TOKEN_INDEX {
@ -109,6 +111,8 @@ pub fn token_register(
deposit_weight_scale_start_quote,
reduce_only,
force_close: 0,
disable_asset_liquidation: u8::from(disable_asset_liquidation),
force_withdraw: 0,
padding: Default::default(),
fees_withdrawn: 0,
token_conditional_swap_taker_fee_rate,
@ -127,7 +131,9 @@ pub fn token_register(
zero_util_rate: I80F48::from_num(zero_util_rate),
platform_liquidation_fee: I80F48::from_num(platform_liquidation_fee),
collected_liquidation_fees: I80F48::ZERO,
reserved: [0; 1920],
collected_collateral_fees: I80F48::ZERO,
collateral_fee_per_day,
reserved: [0; 1900],
};
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;

View File

@ -90,6 +90,8 @@ pub fn token_register_trustless(
deposit_weight_scale_start_quote: 5_000_000_000.0, // $5k
reduce_only: 2, // deposit-only
force_close: 0,
disable_asset_liquidation: 1,
force_withdraw: 0,
padding: Default::default(),
fees_withdrawn: 0,
token_conditional_swap_taker_fee_rate: 0.0,
@ -107,7 +109,9 @@ pub fn token_register_trustless(
deposit_limit: 0,
zero_util_rate: I80F48::ZERO,
collected_liquidation_fees: I80F48::ZERO,
reserved: [0; 1920],
collected_collateral_fees: I80F48::ZERO,
collateral_fee_per_day: 0.0, // TODO
reserved: [0; 1900],
};
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
if let Ok(oracle_price) = bank.oracle_price(&OracleAccountInfos::from_reader(oracle_ref), None)

View File

@ -84,6 +84,7 @@ pub mod mango_v4 {
mngo_token_index_opt: Option<TokenIndex>,
buyback_fees_expiry_interval_opt: Option<u64>,
allowed_fast_listings_per_interval_opt: Option<u16>,
collateral_fee_interval_opt: Option<u64>,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::group_edit(
@ -100,6 +101,7 @@ pub mod mango_v4 {
mngo_token_index_opt,
buyback_fees_expiry_interval_opt,
allowed_fast_listings_per_interval_opt,
collateral_fee_interval_opt,
)?;
Ok(())
}
@ -157,6 +159,8 @@ pub mod mango_v4 {
deposit_limit: u64,
zero_util_rate: f32,
platform_liquidation_fee: f32,
disable_asset_liquidation: bool,
collateral_fee_per_day: f32,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::token_register(
@ -190,6 +194,8 @@ pub mod mango_v4 {
deposit_limit,
zero_util_rate,
platform_liquidation_fee,
disable_asset_liquidation,
collateral_fee_per_day,
)?;
Ok(())
}
@ -245,6 +251,9 @@ pub mod mango_v4 {
deposit_limit_opt: Option<u64>,
zero_util_rate_opt: Option<f32>,
platform_liquidation_fee_opt: Option<f32>,
disable_asset_liquidation_opt: Option<bool>,
collateral_fee_per_day_opt: Option<f32>,
force_withdraw_opt: Option<bool>,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::token_edit(
@ -287,6 +296,9 @@ pub mod mango_v4 {
deposit_limit_opt,
zero_util_rate_opt,
platform_liquidation_fee_opt,
disable_asset_liquidation_opt,
collateral_fee_per_day_opt,
force_withdraw_opt,
)?;
Ok(())
}
@ -807,6 +819,12 @@ pub mod mango_v4 {
Ok(())
}
pub fn token_force_withdraw(ctx: Context<TokenForceWithdraw>) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::token_force_withdraw(ctx)?;
Ok(())
}
///
/// Perps
///
@ -1605,6 +1623,12 @@ pub mod mango_v4 {
Ok(())
}
pub fn token_charge_collateral_fees(ctx: Context<TokenChargeCollateralFees>) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::token_charge_collateral_fees(ctx)?;
Ok(())
}
pub fn alt_set(ctx: Context<AltSet>, index: u8) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::alt_set(ctx, index)?;

View File

@ -795,3 +795,23 @@ pub struct TokenConditionalSwapStartLog {
pub incentive_token_index: u16,
pub incentive_amount: u64,
}
#[event]
pub struct TokenCollateralFeeLog {
pub mango_group: Pubkey,
pub mango_account: Pubkey,
pub token_index: u16,
pub asset_usage_fraction: i128,
pub fee: i128,
pub price: i128,
}
#[event]
pub struct ForceWithdrawLog {
pub mango_group: Pubkey,
pub mango_account: Pubkey,
pub token_index: u16,
pub quantity: u64,
pub price: i128, // I80F48
pub to_token_account: Pubkey,
}

View File

@ -158,8 +158,14 @@ pub struct Bank {
pub reduce_only: u8,
pub force_close: u8,
/// If set to 1, deposits cannot be liquidated when an account is liquidatable.
/// That means bankrupt accounts may still have assets of this type deposited.
pub disable_asset_liquidation: u8,
pub force_withdraw: u8,
#[derivative(Debug = "ignore")]
pub padding: [u8; 6],
pub padding: [u8; 4],
// Do separate bookkeping for how many tokens were withdrawn
// This ensures that collected_fees_native is strictly increasing for stats gathering purposes
@ -217,8 +223,16 @@ pub struct Bank {
/// See also collected_fees_native and fees_withdrawn.
pub collected_liquidation_fees: I80F48,
/// Collateral fees that have been collected (in native tokens)
///
/// See also collected_fees_native and fees_withdrawn.
pub collected_collateral_fees: I80F48,
/// The daily collateral fees rate for fully utilized collateral.
pub collateral_fee_per_day: f32,
#[derivative(Debug = "ignore")]
pub reserved: [u8; 1920],
pub reserved: [u8; 1900],
}
const_assert_eq!(
size_of::<Bank>(),
@ -255,8 +269,9 @@ const_assert_eq!(
+ 16 * 3
+ 32
+ 8
+ 16 * 3
+ 1920
+ 16 * 4
+ 4
+ 1900
);
const_assert_eq!(size_of::<Bank>(), 3064);
const_assert_eq!(size_of::<Bank>() % 8, 0);
@ -300,6 +315,7 @@ impl Bank {
indexed_borrows: I80F48::ZERO,
collected_fees_native: I80F48::ZERO,
collected_liquidation_fees: I80F48::ZERO,
collected_collateral_fees: I80F48::ZERO,
fees_withdrawn: 0,
dust: I80F48::ZERO,
flash_loan_approved_amount: 0,
@ -346,7 +362,9 @@ impl Bank {
deposit_weight_scale_start_quote: existing_bank.deposit_weight_scale_start_quote,
reduce_only: existing_bank.reduce_only,
force_close: existing_bank.force_close,
padding: [0; 6],
disable_asset_liquidation: existing_bank.disable_asset_liquidation,
force_withdraw: existing_bank.force_withdraw,
padding: [0; 4],
token_conditional_swap_taker_fee_rate: existing_bank
.token_conditional_swap_taker_fee_rate,
token_conditional_swap_maker_fee_rate: existing_bank
@ -363,7 +381,8 @@ impl Bank {
deposit_limit: existing_bank.deposit_limit,
zero_util_rate: existing_bank.zero_util_rate,
platform_liquidation_fee: existing_bank.platform_liquidation_fee,
reserved: [0; 1920],
collateral_fee_per_day: existing_bank.collateral_fee_per_day,
reserved: [0; 1900],
}
}
@ -375,14 +394,18 @@ impl Bank {
require_gte!(self.rate0, I80F48::ZERO);
require_gte!(self.rate1, I80F48::ZERO);
require_gte!(self.max_rate, I80F48::ZERO);
require_gte!(self.adjustment_factor, 0.0);
require_gte!(self.loan_fee_rate, 0.0);
require_gte!(self.loan_origination_fee_rate, 0.0);
require_gte!(self.maint_asset_weight, 0.0);
require_gte!(self.stable_price_model.delay_growth_limit, 0.0);
require_gte!(self.stable_price_model.stable_growth_limit, 0.0);
require_gte!(self.init_asset_weight, 0.0);
require_gte!(self.maint_asset_weight, self.init_asset_weight);
require_gte!(self.maint_liab_weight, 0.0);
require_gte!(self.init_liab_weight, 0.0);
require_gte!(self.init_liab_weight, self.maint_liab_weight);
require_gte!(self.liquidation_fee, 0.0);
require_gte!(self.min_vault_to_deposits_ratio, 0.0);
require_gte!(1.0, self.min_vault_to_deposits_ratio);
require_gte!(self.net_borrow_limit_per_window_quote, -1);
require_gt!(self.borrow_weight_scale_start_quote, 0.0);
require_gt!(self.deposit_weight_scale_start_quote, 0.0);
@ -392,11 +415,22 @@ impl Bank {
require_gte!(self.flash_loan_swap_fee_rate, 0.0);
require_gte!(self.interest_curve_scaling, 1.0);
require_gte!(self.interest_target_utilization, 0.0);
require_gte!(1.0, self.interest_target_utilization);
require_gte!(self.maint_weight_shift_duration_inv, 0.0);
require_gte!(self.maint_weight_shift_asset_target, 0.0);
require_gte!(self.maint_weight_shift_liab_target, 0.0);
require_gte!(self.zero_util_rate, I80F48::ZERO);
require_gte!(self.platform_liquidation_fee, 0.0);
if !self.allows_asset_liquidation() {
require!(self.are_borrows_reduce_only(), MangoError::SomeError);
require_eq!(self.maint_asset_weight, I80F48::ZERO);
}
require_gte!(self.collateral_fee_per_day, 0.0);
if self.is_force_withdraw() {
require!(self.are_deposits_reduce_only(), MangoError::SomeError);
require!(!self.allows_asset_liquidation(), MangoError::SomeError);
require_eq!(self.maint_asset_weight, I80F48::ZERO);
}
Ok(())
}
@ -418,6 +452,14 @@ impl Bank {
self.force_close == 1
}
pub fn is_force_withdraw(&self) -> bool {
self.force_withdraw == 1
}
pub fn allows_asset_liquidation(&self) -> bool {
self.disable_asset_liquidation == 0
}
#[inline(always)]
pub fn native_borrows(&self) -> I80F48 {
self.borrow_index * self.indexed_borrows
@ -732,7 +774,7 @@ impl Bank {
})
}
// withdraw the loan origination fee for a borrow that happenend earlier
// withdraw the loan origination fee for a borrow that happened earlier
pub fn withdraw_loan_origination_fee(
&mut self,
position: &mut TokenPosition,
@ -1052,7 +1094,7 @@ impl Bank {
)
}
/// calcualtor function that can be used to compute an interest
/// calculator function that can be used to compute an interest
/// rate based on the given parameters
#[inline(always)]
pub fn interest_rate_curve_calculator(

View File

@ -98,11 +98,32 @@ pub struct Group {
/// Number of fast listings that are allowed per interval
pub allowed_fast_listings_per_interval: u16,
pub reserved: [u8; 1812],
pub padding2: [u8; 4],
/// Intervals in which collateral fee is applied
pub collateral_fee_interval: u64,
pub reserved: [u8; 1800],
}
const_assert_eq!(
size_of::<Group>(),
32 + 4 + 32 * 2 + 4 + 32 * 2 + 4 + 4 + 20 * 32 + 32 + 8 + 16 + 32 + 8 + 8 + 2 * 2 + 1812
32 + 4
+ 32 * 2
+ 4
+ 32 * 2
+ 4
+ 4
+ 20 * 32
+ 32
+ 8
+ 16
+ 32
+ 8
+ 8
+ 2 * 2
+ 4
+ 8
+ 1800
);
const_assert_eq!(size_of::<Group>(), 2736);
const_assert_eq!(size_of::<Group>() % 8, 0);
@ -224,6 +245,7 @@ pub enum IxGate {
TokenConditionalSwapCreatePremiumAuction = 69,
TokenConditionalSwapCreateLinearAuction = 70,
Serum3PlaceOrderV2 = 71,
TokenForceWithdraw = 72,
// NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction.
}

View File

@ -86,7 +86,7 @@ impl MangoAccountPdaSeeds {
// When not reading via idl, MangoAccount binary data is backwards compatible: when ignoring trailing bytes,
// a v2 account can be read as a v1 account and a v3 account can be read as v1 or v2 etc.
#[account]
#[derive(Derivative)]
#[derive(Derivative, PartialEq)]
#[derivative(Debug)]
pub struct MangoAccount {
// fixed
@ -151,8 +151,14 @@ pub struct MangoAccount {
/// Next id to use when adding a token condition swap
pub next_token_conditional_swap_id: u64,
pub temporary_delegate: Pubkey,
pub temporary_delegate_expiry: u64,
/// Time at which the last collateral fee was charged
pub last_collateral_fee_charge: u64,
#[derivative(Debug = "ignore")]
pub reserved: [u8; 200],
pub reserved: [u8; 152],
// dynamic
pub header_version: u8,
@ -203,7 +209,10 @@ impl MangoAccount {
buyback_fees_accrued_previous: 0,
buyback_fees_expiry_timestamp: 0,
next_token_conditional_swap_id: 0,
reserved: [0; 200],
temporary_delegate: Pubkey::default(),
temporary_delegate_expiry: 0,
last_collateral_fee_charge: 0,
reserved: [0; 152],
header_version: DEFAULT_MANGO_ACCOUNT_VERSION,
padding3: Default::default(),
padding4: Default::default(),
@ -327,11 +336,12 @@ pub struct MangoAccountFixed {
pub next_token_conditional_swap_id: u64,
pub temporary_delegate: Pubkey,
pub temporary_delegate_expiry: u64,
pub reserved: [u8; 160],
pub last_collateral_fee_charge: u64,
pub reserved: [u8; 152],
}
const_assert_eq!(
size_of::<MangoAccountFixed>(),
32 * 4 + 8 + 8 * 8 + 32 + 8 + 160
32 * 4 + 8 + 8 * 8 + 32 + 8 + 8 + 152
);
const_assert_eq!(size_of::<MangoAccountFixed>(), 400);
const_assert_eq!(size_of::<MangoAccountFixed>() % 8, 0);
@ -737,6 +747,12 @@ impl<
self.dynamic.deref_or_borrow()
}
#[allow(dead_code)]
fn dynamic_reserved_bytes(&self) -> &[u8] {
let reserved_offset = self.header().reserved_bytes_offset();
&self.dynamic()[reserved_offset..reserved_offset + DYNAMIC_RESERVED_BYTES]
}
/// Returns
/// - the position
/// - the raw index into the token positions list (for use with get_raw/deactivate)
@ -1155,6 +1171,7 @@ impl<
}
}
// Only used in unit tests
pub fn deactivate_perp_position(
&mut self,
perp_market_index: PerpMarketIndex,
@ -1196,6 +1213,19 @@ impl<
Ok(())
}
pub fn find_first_active_unused_perp_position(&self) -> Option<&PerpPosition> {
let first_unused_position_opt = self.all_perp_positions().find(|p| {
p.is_active()
&& p.base_position_lots == 0
&& p.quote_position_native == 0
&& p.bids_base_lots == 0
&& p.asks_base_lots == 0
&& p.taker_base_lots == 0
&& p.taker_quote_lots == 0
});
first_unused_position_opt
}
pub fn add_perp_order(
&mut self,
perp_market_index: PerpMarketIndex,
@ -1852,6 +1882,7 @@ impl<'a, 'info: 'a> MangoAccountLoader<'a> for &'a AccountLoader<'info, MangoAcc
mod tests {
use bytemuck::Zeroable;
use itertools::Itertools;
use std::path::PathBuf;
use crate::state::PostOrderType;
@ -2378,12 +2409,7 @@ mod tests {
);
}
let reserved_offset = account.header.reserved_bytes_offset();
assert!(
account.dynamic[reserved_offset..reserved_offset + DYNAMIC_RESERVED_BYTES]
.iter()
.all(|&v| v == 0)
);
assert!(account.dynamic_reserved_bytes().iter().all(|&v| v == 0));
Ok(())
}
@ -2808,4 +2834,118 @@ mod tests {
Ok(())
}
#[test]
fn test_perp_auto_close_first_unused() {
let mut account = make_test_account();
// Fill all perp slots
assert_eq!(account.header.perp_count, 4);
account.ensure_perp_position(1, 0).unwrap();
account.ensure_perp_position(2, 0).unwrap();
account.ensure_perp_position(3, 0).unwrap();
account.ensure_perp_position(4, 0).unwrap();
assert_eq!(account.active_perp_positions().count(), 4);
// Force usage of some perp slot (leaves 3 unused)
account.perp_position_mut(1).unwrap().taker_base_lots = 10;
account.perp_position_mut(2).unwrap().base_position_lots = 10;
account.perp_position_mut(4).unwrap().quote_position_native = I80F48::from_num(10);
assert!(account.perp_position(3).ok().is_some());
// Should not succeed anymore
{
let e = account.ensure_perp_position(5, 0);
assert!(e.is_anchor_error_with_code(MangoError::NoFreePerpPositionIndex.error_code()));
}
// Act
let to_be_closed_account_opt = account.find_first_active_unused_perp_position();
assert_eq!(to_be_closed_account_opt.unwrap().market_index, 3)
}
// Attempts reading old mango account data with borsh and with zerocopy
#[test]
fn test_mango_account_backwards_compatibility() -> Result<()> {
use solana_program_test::{find_file, read_file};
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.push("resources/test");
// Grab live accounts with
// solana account CZGf1qbYPaSoabuA1EmdN8W5UHvH5CeXcNZ7RTx65aVQ --output-file programs/mango-v4/resources/test/mangoaccount-v0.21.3.bin
let fixtures = vec!["mangoaccount-v0.21.3"];
for fixture in fixtures {
let filename = format!("resources/test/{}.bin", fixture);
let account_bytes = read_file(find_file(&filename).unwrap());
// Read with borsh
let mut account_bytes_slice: &[u8] = &account_bytes;
let borsh_account = MangoAccount::try_deserialize(&mut account_bytes_slice)?;
// Read with zerocopy
let zerocopy_reader = MangoAccountValue::from_bytes(&account_bytes[8..])?;
let fixed = &zerocopy_reader.fixed;
let zerocopy_account = MangoAccount {
group: fixed.group,
owner: fixed.owner,
name: fixed.name,
delegate: fixed.delegate,
account_num: fixed.account_num,
being_liquidated: fixed.being_liquidated,
in_health_region: fixed.in_health_region,
bump: fixed.bump,
padding: Default::default(),
net_deposits: fixed.net_deposits,
perp_spot_transfers: fixed.perp_spot_transfers,
health_region_begin_init_health: fixed.health_region_begin_init_health,
frozen_until: fixed.frozen_until,
buyback_fees_accrued_current: fixed.buyback_fees_accrued_current,
buyback_fees_accrued_previous: fixed.buyback_fees_accrued_previous,
buyback_fees_expiry_timestamp: fixed.buyback_fees_expiry_timestamp,
next_token_conditional_swap_id: fixed.next_token_conditional_swap_id,
temporary_delegate: fixed.temporary_delegate,
temporary_delegate_expiry: fixed.temporary_delegate_expiry,
last_collateral_fee_charge: fixed.last_collateral_fee_charge,
reserved: [0u8; 152],
header_version: *zerocopy_reader.header_version(),
padding3: Default::default(),
padding4: Default::default(),
tokens: zerocopy_reader.all_token_positions().cloned().collect_vec(),
padding5: Default::default(),
serum3: zerocopy_reader.all_serum3_orders().cloned().collect_vec(),
padding6: Default::default(),
perps: zerocopy_reader.all_perp_positions().cloned().collect_vec(),
padding7: Default::default(),
perp_open_orders: zerocopy_reader.all_perp_orders().cloned().collect_vec(),
padding8: Default::default(),
token_conditional_swaps: zerocopy_reader
.all_token_conditional_swaps()
.cloned()
.collect_vec(),
reserved_dynamic: zerocopy_reader.dynamic_reserved_bytes().try_into().unwrap(),
};
// Both methods agree?
assert_eq!(borsh_account, zerocopy_account);
// Serializing and deserializing produces the same data?
let mut borsh_bytes = Vec::new();
borsh_account.try_serialize(&mut borsh_bytes)?;
let mut slice: &[u8] = &borsh_bytes;
let roundtrip_account = MangoAccount::try_deserialize(&mut slice)?;
assert_eq!(borsh_account, roundtrip_account);
}
Ok(())
}
}

View File

@ -12,7 +12,7 @@ use crate::state::*;
pub const FREE_ORDER_SLOT: PerpMarketIndex = PerpMarketIndex::MAX;
#[zero_copy]
#[derive(AnchorDeserialize, AnchorSerialize, Derivative)]
#[derive(AnchorDeserialize, AnchorSerialize, Derivative, PartialEq)]
#[derivative(Debug)]
pub struct TokenPosition {
// TODO: Why did we have deposits and borrows as two different values
@ -110,7 +110,7 @@ impl TokenPosition {
}
#[zero_copy]
#[derive(AnchorSerialize, AnchorDeserialize, Derivative)]
#[derive(AnchorSerialize, AnchorDeserialize, Derivative, PartialEq)]
#[derivative(Debug)]
pub struct Serum3Orders {
pub open_orders: Pubkey,
@ -203,7 +203,7 @@ impl Default for Serum3Orders {
}
#[zero_copy]
#[derive(AnchorSerialize, AnchorDeserialize, Derivative)]
#[derive(AnchorSerialize, AnchorDeserialize, Derivative, PartialEq)]
#[derivative(Debug)]
pub struct PerpPosition {
pub market_index: PerpMarketIndex,
@ -800,7 +800,7 @@ impl PerpPosition {
}
#[zero_copy]
#[derive(AnchorSerialize, AnchorDeserialize, Derivative)]
#[derive(AnchorSerialize, AnchorDeserialize, Derivative, PartialEq)]
#[derivative(Debug)]
pub struct PerpOpenOrder {
pub side_and_tree: u8, // SideAndOrderTree -- enums aren't POD

View File

@ -82,7 +82,7 @@ pub mod sol_mint_mainnet {
}
#[zero_copy]
#[derive(AnchorDeserialize, AnchorSerialize, Derivative)]
#[derive(AnchorDeserialize, AnchorSerialize, Derivative, PartialEq, Eq)]
#[derivative(Debug)]
pub struct OracleConfig {
pub conf_filter: I80F48,
@ -94,7 +94,7 @@ const_assert_eq!(size_of::<OracleConfig>(), 16 + 8 + 72);
const_assert_eq!(size_of::<OracleConfig>(), 96);
const_assert_eq!(size_of::<OracleConfig>() % 8, 0);
#[derive(AnchorDeserialize, AnchorSerialize, Debug)]
#[derive(AnchorDeserialize, AnchorSerialize, Debug, Default)]
pub struct OracleConfigParams {
pub conf_filter: f32,
pub max_staleness_slots: Option<u32>,
@ -278,7 +278,7 @@ fn get_pyth_state(
pub struct OracleAccountInfos<'a, T: KeyedAccountReader> {
pub oracle: &'a T,
pub fallback_opt: Option<&'a T>,
pub usd_opt: Option<&'a T>,
pub usdc_opt: Option<&'a T>,
pub sol_opt: Option<&'a T>,
}
@ -287,7 +287,7 @@ impl<'a, T: KeyedAccountReader> OracleAccountInfos<'a, T> {
OracleAccountInfos {
oracle: acc_reader,
fallback_opt: None,
usd_opt: None,
usdc_opt: None,
sol_opt: None,
}
}
@ -406,9 +406,7 @@ fn oracle_state_unchecked_inner<T: KeyedAccountReader>(
OracleType::OrcaCLMM => {
let whirlpool = load_whirlpool_state(oracle_info)?;
let inverted = whirlpool.token_mint_a == usdc_mint_mainnet::ID
|| (whirlpool.token_mint_a == sol_mint_mainnet::ID
&& whirlpool.token_mint_b != usdc_mint_mainnet::ID);
let inverted = whirlpool.is_inverted();
let quote_state = if inverted {
quote_state_unchecked(acc_infos, &whirlpool.token_mint_a)?
} else {
@ -441,7 +439,7 @@ fn quote_state_unchecked<T: KeyedAccountReader>(
) -> Result<OracleState> {
if quote_mint == &usdc_mint_mainnet::ID {
let usd_feed = acc_infos
.usd_opt
.usdc_opt
.ok_or_else(|| error!(MangoError::MissingFeedForCLMMOracle))?;
let usd_state = get_pyth_state(usd_feed, QUOTE_DECIMALS as u8)?;
return Ok(usd_state);
@ -590,13 +588,13 @@ mod tests {
let usdc_ais = OracleAccountInfos {
oracle: usdc_ai,
fallback_opt: None,
usd_opt: None,
usdc_opt: None,
sol_opt: None,
};
let orca_ais = OracleAccountInfos {
oracle: ai,
fallback_opt: None,
usd_opt: Some(usdc_ai),
usdc_opt: Some(usdc_ai),
sol_opt: None,
};
let usdc = oracle_state_unchecked(&usdc_ais, usdc_decimals).unwrap();
@ -635,7 +633,7 @@ mod tests {
let oracle_infos = OracleAccountInfos {
oracle: ai,
fallback_opt: None,
usd_opt: None,
usdc_opt: None,
sol_opt: None,
};
assert!(oracle_state_unchecked(&oracle_infos, base_decimals)

View File

@ -3,6 +3,10 @@ use solana_program::pubkey::Pubkey;
use crate::{accounts_zerocopy::KeyedAccountReader, error::MangoError};
use super::{
pyth_mainnet_sol_oracle, pyth_mainnet_usdc_oracle, sol_mint_mainnet, usdc_mint_mainnet,
};
pub mod orca_mainnet_whirlpool {
use solana_program::declare_id;
declare_id!("whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc");
@ -18,6 +22,30 @@ pub struct WhirlpoolState {
pub token_mint_b: Pubkey, // 32
}
impl WhirlpoolState {
pub fn is_inverted(&self) -> bool {
self.token_mint_a == usdc_mint_mainnet::ID
|| (self.token_mint_a == sol_mint_mainnet::ID
&& self.token_mint_b != usdc_mint_mainnet::ID)
}
pub fn get_quote_oracle(&self) -> Result<Pubkey> {
let mint = if self.is_inverted() {
self.token_mint_a
} else {
self.token_mint_b
};
if mint == usdc_mint_mainnet::ID {
return Ok(pyth_mainnet_usdc_oracle::ID);
} else if mint == sol_mint_mainnet::ID {
return Ok(pyth_mainnet_sol_oracle::ID);
} else {
return Err(MangoError::MissingFeedForCLMMOracle.into());
}
}
}
pub fn load_whirlpool_state(acc_info: &impl KeyedAccountReader) -> Result<WhirlpoolState> {
let data = &acc_info.data();
require!(

View File

@ -45,7 +45,7 @@ pub enum TokenConditionalSwapType {
}
#[zero_copy]
#[derive(AnchorDeserialize, AnchorSerialize, Derivative)]
#[derive(AnchorDeserialize, AnchorSerialize, Derivative, PartialEq)]
#[derivative(Debug)]
pub struct TokenConditionalSwap {
pub id: u64,

View File

@ -17,6 +17,7 @@ mod test_bankrupt_tokens;
mod test_basic;
mod test_benchmark;
mod test_borrow_limits;
mod test_collateral_fees;
mod test_delegate;
mod test_fees_buyback_with_mngo;
mod test_force_close;

View File

@ -462,6 +462,8 @@ async fn test_bank_maint_weight_shift() -> Result<(), TransportError> {
mint: mints[0].pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
init_asset_weight_opt: Some(0.0),
init_liab_weight_opt: Some(2.0),
maint_weight_shift_start_opt: Some(start_time + 1000),
maint_weight_shift_end_opt: Some(start_time + 2000),
maint_weight_shift_asset_target_opt: Some(0.5),

View File

@ -0,0 +1,218 @@
#![allow(unused_assignments)]
use super::*;
#[tokio::test]
async fn test_collateral_fees() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();
let owner = context.users[0].key;
let payer = context.users[1].key;
let mints = &context.mints[0..2];
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
..mango_setup::GroupWithTokensConfig::default()
}
.create(solana)
.await;
// fund the vaults to allow borrowing
create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
mints,
1_000_000,
0,
)
.await;
let account = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
&mints[0..1],
1_500, // maint: 0.8 * 1500 = 1200
0,
)
.await;
let empty_account = create_funded_account(
&solana,
group,
owner,
2,
&context.users[1],
&mints[0..0],
0,
0,
)
.await;
let hour = 60 * 60;
send_tx(
solana,
GroupEdit {
group,
admin,
options: mango_v4::instruction::GroupEdit {
collateral_fee_interval_opt: Some(6 * hour),
..group_edit_instruction_default()
},
},
)
.await
.unwrap();
send_tx(
solana,
TokenEdit {
group,
admin,
mint: mints[0].pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
collateral_fee_per_day_opt: Some(0.1),
..token_edit_instruction_default()
},
},
)
.await
.unwrap();
send_tx(
solana,
TokenEdit {
group,
admin,
mint: mints[1].pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
loan_origination_fee_rate_opt: Some(0.0),
..token_edit_instruction_default()
},
},
)
.await
.unwrap();
//
// TEST: It works on empty accounts
//
send_tx(
solana,
TokenChargeCollateralFeesInstruction {
account: empty_account,
},
)
.await
.unwrap();
let mut last_time = solana.clock_timestamp().await;
solana.set_clock_timestamp(last_time + 9 * hour).await;
// send it twice, because the first time will never charge anything
send_tx(
solana,
TokenChargeCollateralFeesInstruction {
account: empty_account,
},
)
.await
.unwrap();
last_time = solana.clock_timestamp().await;
//
// TEST: Without borrows, charging collateral fees has no effect
//
send_tx(solana, TokenChargeCollateralFeesInstruction { account })
.await
.unwrap();
last_time = solana.clock_timestamp().await;
solana.set_clock_timestamp(last_time + 9 * hour).await;
// send it twice, because the first time will never charge anything
send_tx(solana, TokenChargeCollateralFeesInstruction { account })
.await
.unwrap();
last_time = solana.clock_timestamp().await;
// no effect
assert_eq!(
account_position(solana, account, tokens[0].bank).await,
1_500
);
//
// TEST: With borrows, there's an effect depending on the time that has passed
//
send_tx(
solana,
TokenWithdrawInstruction {
amount: 500, // maint: -1.2 * 500 = -600 (half of 1200)
allow_borrow: true,
account,
owner,
token_account: context.users[1].token_accounts[1],
bank_index: 0,
},
)
.await
.unwrap();
solana.set_clock_timestamp(last_time + 9 * hour).await;
send_tx(solana, TokenChargeCollateralFeesInstruction { account })
.await
.unwrap();
last_time = solana.clock_timestamp().await;
assert!(assert_equal_f64_f64(
account_position_f64(solana, account, tokens[0].bank).await,
1500.0 * (1.0 - 0.1 * (9.0 / 24.0) * (600.0 / 1200.0)),
0.01
));
let last_balance = account_position_f64(solana, account, tokens[0].bank).await;
//
// TEST: More borrows
//
send_tx(
solana,
TokenWithdrawInstruction {
amount: 100, // maint: -1.2 * 600 = -720
allow_borrow: true,
account,
owner,
token_account: context.users[1].token_accounts[1],
bank_index: 0,
},
)
.await
.unwrap();
solana.set_clock_timestamp(last_time + 7 * hour).await;
send_tx(solana, TokenChargeCollateralFeesInstruction { account })
.await
.unwrap();
//last_time = solana.clock_timestamp().await;
assert!(assert_equal_f64_f64(
account_position_f64(solana, account, tokens[0].bank).await,
last_balance * (1.0 - 0.1 * (7.0 / 24.0) * (720.0 / (last_balance * 0.8))),
0.01
));
Ok(())
}

View File

@ -438,3 +438,114 @@ async fn test_force_close_perp() -> Result<(), TransportError> {
Ok(())
}
#[tokio::test]
async fn test_force_withdraw_token() -> Result<(), TransportError> {
let test_builder = TestContextBuilder::new();
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();
let owner = context.users[0].key;
let payer = context.users[1].key;
let mints = &context.mints[0..1];
//
// SETUP: Create a group and an account to fill the vaults
//
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
let token = &tokens[0];
let deposit_amount = 100;
let account = create_funded_account(
&solana,
group,
owner,
0,
&context.users[0],
mints,
deposit_amount,
0,
)
.await;
//
// TEST: fails when force withdraw isn't enabled
//
assert!(send_tx(
solana,
TokenForceWithdrawInstruction {
account,
bank: token.bank,
target: context.users[0].token_accounts[0],
},
)
.await
.is_err());
// set force withdraw to enabled
send_tx(
solana,
TokenEdit {
admin,
group,
mint: token.mint.pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
init_asset_weight_opt: Some(0.0),
maint_asset_weight_opt: Some(0.0),
reduce_only_opt: Some(1),
disable_asset_liquidation_opt: Some(true),
force_withdraw_opt: Some(true),
..token_edit_instruction_default()
},
},
)
.await
.unwrap();
//
// TEST: can't withdraw to foreign address
//
assert!(send_tx(
solana,
TokenForceWithdrawInstruction {
account,
bank: token.bank,
target: context.users[1].token_accounts[0], // bad address/owner
},
)
.await
.is_err());
//
// TEST: passes and withdraws tokens
//
let token_account = context.users[0].token_accounts[0];
let before_balance = solana.token_account_balance(token_account).await;
send_tx(
solana,
TokenForceWithdrawInstruction {
account,
bank: token.bank,
target: token_account,
},
)
.await
.unwrap();
let after_balance = solana.token_account_balance(token_account).await;
assert_eq!(after_balance, before_balance + deposit_amount);
assert!(account_position_closed(solana, account, token.bank).await);
Ok(())
}

View File

@ -335,7 +335,7 @@ async fn test_health_compute_tokens_fallback_oracles() -> Result<(), TransportEr
println!("average success increase: {avg_success_increase}");
println!("average failure increase: {avg_failure_increase}");
assert!(avg_success_increase < 2_050);
assert!(avg_success_increase < 18_500);
assert!(avg_failure_increase < 19_500);
Ok(())
}

View File

@ -324,6 +324,66 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
//
set_bank_stub_oracle_price(solana, group, borrow_token1, admin, 2.0).await;
//
// TEST: can't liquidate if token has no asset weight
//
send_tx(
solana,
TokenEdit {
group,
admin,
mint: collateral_token2.mint.pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
maint_asset_weight_opt: Some(0.0),
init_asset_weight_opt: Some(0.0),
disable_asset_liquidation_opt: Some(true),
reduce_only_opt: Some(1),
..token_edit_instruction_default()
},
},
)
.await
.unwrap();
let res = send_tx(
solana,
TokenLiqWithTokenInstruction {
liqee: account,
liqor: vault_account,
liqor_owner: owner,
asset_token_index: collateral_token2.index,
liab_token_index: borrow_token2.index,
asset_bank_index: 0,
liab_bank_index: 0,
max_liab_transfer: I80F48::from_num(10000.0),
},
)
.await;
assert_mango_error(
&res,
MangoError::TokenAssetLiquidationDisabled.into(),
"liquidation disabled".to_string(),
);
send_tx(
solana,
TokenEdit {
group,
admin,
mint: collateral_token2.mint.pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
maint_asset_weight_opt: Some(0.8),
init_asset_weight_opt: Some(0.6),
disable_asset_liquidation_opt: Some(false),
reduce_only_opt: Some(0),
..token_edit_instruction_default()
},
},
)
.await
.unwrap();
//
// TEST: liquidate borrow2 against too little collateral2
//

View File

@ -1077,6 +1077,8 @@ impl ClientInstruction for TokenRegisterInstruction {
deposit_limit: 0,
zero_util_rate: 0.0,
platform_liquidation_fee: self.platform_liquidation_fee,
disable_asset_liquidation: false,
collateral_fee_per_day: 0.0,
};
let bank = Pubkey::find_program_address(
@ -1324,6 +1326,9 @@ pub fn token_edit_instruction_default() -> mango_v4::instruction::TokenEdit {
deposit_limit_opt: None,
zero_util_rate_opt: None,
platform_liquidation_fee_opt: None,
disable_asset_liquidation_opt: None,
collateral_fee_per_day_opt: None,
force_withdraw_opt: None,
}
}
@ -1842,6 +1847,7 @@ pub fn group_edit_instruction_default() -> mango_v4::instruction::GroupEdit {
mngo_token_index_opt: None,
buyback_fees_expiry_interval_opt: None,
allowed_fast_listings_per_interval_opt: None,
collateral_fee_interval_opt: None,
}
}
@ -3107,6 +3113,58 @@ impl ClientInstruction for TokenForceCloseBorrowsWithTokenInstruction {
}
}
pub struct TokenForceWithdrawInstruction {
pub account: Pubkey,
pub bank: Pubkey,
pub target: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for TokenForceWithdrawInstruction {
type Accounts = mango_v4::accounts::TokenForceWithdraw;
type Instruction = mango_v4::instruction::TokenForceWithdraw;
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 bank = account_loader.load::<Bank>(&self.bank).await.unwrap();
let health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account,
None,
false,
None,
)
.await;
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
bank: self.bank,
vault: bank.vault,
oracle: bank.oracle,
owner_ata_token_account: self.target,
alternate_owner_token_account: self.target,
token_program: Token::id(),
};
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![]
}
}
pub struct TokenLiqWithTokenInstruction {
pub liqee: Pubkey,
pub liqor: Pubkey,
@ -5036,3 +5094,48 @@ impl ClientInstruction for TokenConditionalSwapStartInstruction {
vec![self.liqor_owner]
}
}
#[derive(Clone)]
pub struct TokenChargeCollateralFeesInstruction {
pub account: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for TokenChargeCollateralFeesInstruction {
type Accounts = mango_v4::accounts::TokenChargeCollateralFees;
type Instruction = mango_v4::instruction::TokenChargeCollateralFees;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let instruction = Self::Instruction {};
let health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account,
None,
true,
None,
)
.await;
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
};
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![]
}
}

View File

@ -73,6 +73,7 @@ async function main(): Promise<void> {
group,
usdcDevnetMint,
usdcDevnetOracle.publicKey,
PublicKey.default,
0, // tokenIndex
'USDC',
{
@ -101,6 +102,7 @@ async function main(): Promise<void> {
group,
solDevnetMint,
solDevnetOracle,
PublicKey.default,
4, // tokenIndex
'SOL',
{
@ -130,6 +132,7 @@ async function main(): Promise<void> {
group,
usdtDevnetMint,
usdcDevnetOracle.publicKey,
PublicKey.default,
5, // tokenIndex
'USDT',
{

View File

@ -94,6 +94,7 @@ async function main() {
group,
usdcDevnetMint,
usdcDevnetOracle.publicKey,
PublicKey.default,
0, // tokenIndex
'USDC',
{
@ -124,6 +125,7 @@ async function main() {
group,
solDevnetMint,
solDevnetOracle,
PublicKey.default,
1, // tokenIndex
'SOL',
{

View File

@ -206,6 +206,7 @@ async function registerTokens() {
group,
usdcMainnetMint,
usdcMainnetOracle.publicKey,
PublicKey.default,
0,
'USDC',
{
@ -226,6 +227,7 @@ async function registerTokens() {
group,
usdtMainnetMint,
usdtMainnetOracle,
PublicKey.default,
1,
'USDT',
{
@ -246,6 +248,7 @@ async function registerTokens() {
group,
daiMainnetMint,
daiMainnetOracle,
PublicKey.default,
2,
'DAI',
{
@ -266,6 +269,7 @@ async function registerTokens() {
group,
ethMainnetMint,
ethMainnetOracle,
PublicKey.default,
3,
'ETH',
{
@ -286,6 +290,7 @@ async function registerTokens() {
group,
solMainnetMint,
solMainnetOracle,
PublicKey.default,
4,
'SOL',
{
@ -306,6 +311,7 @@ async function registerTokens() {
group,
msolMainnetMint,
msolMainnetOracle,
PublicKey.default,
5,
'MSOL',
{

View File

@ -60,31 +60,31 @@ async function buildClient(): Promise<MangoClient> {
);
}
async function groupEdit(): Promise<void> {
const client = await buildClient();
const group = await client.getGroup(new PublicKey(GROUP_PK));
const ix = await client.program.methods
.groupEdit(
null, // admin
null, // fastListingAdmin
null, // securityAdmin
null, // testing
null, // version
null, // depositLimitQuote
null, // feesPayWithMngo
null, // feesMngoBonusRate
null, // feesSwapMangoAccount
6, // feesMngoTokenIndex
null, // feesExpiryInterval
5, // allowedFastListingsPerInterval
)
.accounts({
group: group.publicKey,
admin: group.admin,
})
.instruction();
console.log(serializeInstructionToBase64(ix));
}
// async function groupEdit(): Promise<void> {
// const client = await buildClient();
// const group = await client.getGroup(new PublicKey(GROUP_PK));
// const ix = await client.program.methods
// .groupEdit(
// null, // admin
// null, // fastListingAdmin
// null, // securityAdmin
// null, // testing
// null, // version
// null, // depositLimitQuote
// null, // feesPayWithMngo
// null, // feesMngoBonusRate
// null, // feesSwapMangoAccount
// 6, // feesMngoTokenIndex
// null, // feesExpiryInterval
// 5, // allowedFastListingsPerInterval
// )
// .accounts({
// group: group.publicKey,
// admin: group.admin,
// })
// .instruction();
// console.log(serializeInstructionToBase64(ix));
// }
// async function tokenRegister(): Promise<void> {
// const client = await buildClient();
@ -468,7 +468,7 @@ async function idlSetAuthority(): Promise<void> {
async function main(): Promise<void> {
try {
await groupEdit();
// await groupEdit();
// await tokenRegister();
// await tokenEdit();
// await perpCreate();

View File

@ -0,0 +1,73 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { TokenIndex } from '../src/accounts/bank';
import { MangoClient } from '../src/client';
import { MANGO_V4_ID } from '../src/constants';
const CLUSTER: Cluster =
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
const CLUSTER_URL =
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
const USER_KEYPAIR =
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
const GROUP_PK =
process.env.GROUP_PK || '78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX';
const TOKEN_INDEX = Number(process.env.TOKEN_INDEX) as TokenIndex;
async function forceWithdrawTokens(): Promise<void> {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(CLUSTER_URL!, options);
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(
process.env.KEYPAIR || fs.readFileSync(USER_KEYPAIR!, 'utf-8'),
),
),
);
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
CLUSTER,
MANGO_V4_ID[CLUSTER],
{
idsSource: 'get-program-accounts',
},
);
const group = await client.getGroup(new PublicKey(GROUP_PK));
const forceWithdrawBank = group.getFirstBankByTokenIndex(TOKEN_INDEX);
if (forceWithdrawBank.reduceOnly != 2) {
throw new Error(
`Unexpected reduce only state ${forceWithdrawBank.reduceOnly}`,
);
}
if (!forceWithdrawBank.forceWithdraw) {
throw new Error(
`Unexpected force withdraw state ${forceWithdrawBank.forceWithdraw}`,
);
}
// Get all mango accounts with deposits for given token
const mangoAccountsWithDeposits = (
await client.getAllMangoAccounts(group)
).filter((a) => a.getTokenBalanceUi(forceWithdrawBank) > 0);
for (const mangoAccount of mangoAccountsWithDeposits) {
const sig = await client.tokenForceWithdraw(
group,
mangoAccount,
TOKEN_INDEX,
);
console.log(
` tokenForceWithdraw for ${mangoAccount.publicKey}, owner ${
mangoAccount.owner
}, sig https://explorer.solana.com/tx/${sig}?cluster=${
CLUSTER == 'devnet' ? 'devnet' : ''
}`,
);
}
}
forceWithdrawTokens();

View File

@ -29,6 +29,7 @@ const MAINNET_MINTS = new Map([
['ETH', MINTS[1]],
['SOL', MINTS[2]],
['MNGO', MINTS[3]],
['MSOL', MINTS[4]],
]);
const STUB_PRICES = new Map([
@ -36,13 +37,7 @@ const STUB_PRICES = new Map([
['ETH', 1200.0], // eth and usdc both have 6 decimals
['SOL', 0.015], // sol has 9 decimals, equivalent to $15 per SOL
['MNGO', 0.02],
]);
// External markets are matched with those in https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/ids.json
// and verified to have best liquidity for pair on https://openserum.io/
const MAINNET_SERUM3_MARKETS = new Map([
['ETH/USDC', SERUM_MARKETS[0]],
['SOL/USDC', SERUM_MARKETS[1]],
['MSOL', 0.017],
]);
const MIN_VAULT_TO_DEPOSITS_RATIO = 0.2;
@ -90,11 +85,13 @@ async function main(): Promise<void> {
for (const [name, mint] of MAINNET_MINTS) {
console.log(`Creating stub oracle for ${name}...`);
const mintPk = new PublicKey(mint);
try {
const price = STUB_PRICES.get(name)!;
await client.stubOracleCreate(group, mintPk, price);
} catch (error) {
console.log(error);
if ((await client.getStubOracle(group, mintPk)).length == 0) {
try {
const price = STUB_PRICES.get(name)!;
await client.stubOracleCreate(group, mintPk, price);
} catch (error) {
console.log(error);
}
}
const oracle = (await client.getStubOracle(group, mintPk))[0];
console.log(`...created stub oracle ${oracle.publicKey}`);
@ -114,22 +111,32 @@ async function main(): Promise<void> {
maxRate: 1.5,
};
const noFallbackOracle = PublicKey.default;
// register token 0
console.log(`Registering USDC...`);
const usdcMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
const usdcOracle = oracles.get('USDC');
try {
await client.tokenRegister(group, usdcMint, usdcOracle, 0, 'USDC', {
...DefaultTokenRegisterParams,
loanOriginationFeeRate: 0,
loanFeeRate: 0.0001,
initAssetWeight: 1,
maintAssetWeight: 1,
initLiabWeight: 1,
maintLiabWeight: 1,
liquidationFee: 0,
netBorrowLimitPerWindowQuote: NET_BORROWS_LIMIT_NATIVE,
});
await client.tokenRegister(
group,
usdcMint,
usdcOracle,
noFallbackOracle,
0,
'USDC',
{
...DefaultTokenRegisterParams,
loanOriginationFeeRate: 0,
loanFeeRate: 0.0001,
initAssetWeight: 1,
maintAssetWeight: 1,
initLiabWeight: 1,
maintLiabWeight: 1,
liquidationFee: 0,
netBorrowLimitPerWindowQuote: NET_BORROWS_LIMIT_NATIVE,
},
);
await group.reloadAll(client);
} catch (error) {
console.log(error);
@ -140,17 +147,25 @@ async function main(): Promise<void> {
const ethMint = new PublicKey(MAINNET_MINTS.get('ETH')!);
const ethOracle = oracles.get('ETH');
try {
await client.tokenRegister(group, ethMint, ethOracle, 1, 'ETH', {
...DefaultTokenRegisterParams,
loanOriginationFeeRate: 0,
loanFeeRate: 0.0001,
maintAssetWeight: 0.9,
initAssetWeight: 0.8,
maintLiabWeight: 1.1,
initLiabWeight: 1.2,
liquidationFee: 0.05,
netBorrowLimitPerWindowQuote: NET_BORROWS_LIMIT_NATIVE,
});
await client.tokenRegister(
group,
ethMint,
ethOracle,
noFallbackOracle,
1,
'ETH',
{
...DefaultTokenRegisterParams,
loanOriginationFeeRate: 0,
loanFeeRate: 0.0001,
maintAssetWeight: 0.9,
initAssetWeight: 0.8,
maintLiabWeight: 1.1,
initLiabWeight: 1.2,
liquidationFee: 0.05,
netBorrowLimitPerWindowQuote: NET_BORROWS_LIMIT_NATIVE,
},
);
await group.reloadAll(client);
} catch (error) {
console.log(error);
@ -165,6 +180,7 @@ async function main(): Promise<void> {
group,
solMint,
solOracle,
noFallbackOracle,
2, // tokenIndex
'SOL',
{
@ -184,27 +200,72 @@ async function main(): Promise<void> {
console.log(error);
}
const genericBanks = ['MNGO', 'MSOL'];
let nextTokenIndex = 3;
for (const name of genericBanks) {
console.log(`Registering ${name}...`);
const mint = new PublicKey(MAINNET_MINTS.get(name)!);
const oracle = oracles.get(name);
try {
await client.tokenRegister(
group,
mint,
oracle,
noFallbackOracle,
nextTokenIndex,
name,
{
...DefaultTokenRegisterParams,
loanOriginationFeeRate: 0,
loanFeeRate: 0.0001,
maintAssetWeight: 0.9,
initAssetWeight: 0.8,
maintLiabWeight: 1.1,
initLiabWeight: 1.2,
liquidationFee: 0.05,
netBorrowLimitPerWindowQuote: NET_BORROWS_LIMIT_NATIVE,
},
);
nextTokenIndex += 1;
await group.reloadAll(client);
} catch (error) {
console.log(error);
}
}
// log tokens/banks
for (const bank of await group.banksMapByMint.values()) {
console.log(`${bank.toString()}`);
}
console.log('Registering SOL/USDC serum market...');
try {
await client.serum3RegisterMarket(
group,
new PublicKey(MAINNET_SERUM3_MARKETS.get('SOL/USDC')!),
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('SOL')!)),
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('USDC')!)),
1,
'SOL/USDC',
0,
);
} catch (error) {
console.log(error);
let nextSerumMarketIndex = 0;
for (const [name, mint] of MAINNET_MINTS) {
if (name == 'USDC') {
continue;
}
console.log(`Registering ${name}/USDC serum market...`);
try {
await client.serum3RegisterMarket(
group,
new PublicKey(SERUM_MARKETS[nextSerumMarketIndex]),
group.getFirstBankByMint(new PublicKey(mint)),
group.getFirstBankByMint(usdcMint),
nextSerumMarketIndex,
`${name}/USDC`,
0,
);
nextSerumMarketIndex += 1;
} catch (error) {
console.log(error);
}
}
console.log('Registering MNGO-PERP market...');
if (!group.banksMapByMint.get(usdcMint.toString())) {
console.log('stopping, no USDC bank');
return;
}
const mngoOracle = oracles.get('MNGO');
try {
await client.perpCreateMarket(
@ -237,7 +298,7 @@ async function main(): Promise<void> {
-1.0,
2 * 60 * 60,
0.025,
0,
0.0,
);
} catch (error) {
console.log(error);

View File

@ -20,6 +20,9 @@ import { generateSerum3MarketExternalVaultSignerAddress } from '../../src/accoun
// Script which creates three mints and two serum3 markets relating them
//
const MINT_COUNT = 5;
const SERUM_MARKET_COUNT = 4;
function getVaultOwnerAndNonce(
market: PublicKey,
programId: PublicKey,
@ -56,7 +59,7 @@ async function main(): Promise<void> {
// Make mints
const mints = await Promise.all(
Array(4)
Array(MINT_COUNT)
.fill(null)
.map(() =>
splToken.createMint(connection, admin, admin.publicKey, null, 6),
@ -78,11 +81,11 @@ async function main(): Promise<void> {
// Make serum markets
const serumMarkets: PublicKey[] = [];
const quoteMint = mints[0];
for (const baseMint of mints.slice(1, 3)) {
for (const baseMint of mints.slice(1, 1 + SERUM_MARKET_COUNT)) {
const feeRateBps = 0.25; // don't think this does anything
const quoteDustThreshold = 100;
const baseLotSize = 1000;
const quoteLotSize = 1000;
const quoteLotSize = 1; // makes prices be in 1000ths
const openbookProgramId = OPENBOOK_PROGRAM_ID.devnet;
const market = Keypair.generate();

View File

@ -31,7 +31,7 @@ const CLUSTER = process.env.CLUSTER || 'mainnet-beta';
// native prices
const PRICES = {
ETH: 1200.0,
SOL: 0.015,
SOL: 0.015, // not updated for the fact that the new mints we use have 6 decimals!
USDC: 1,
MNGO: 0.02,
};
@ -100,7 +100,7 @@ async function main() {
async function createMangoAccount(name: string): Promise<MangoAccount> {
const accountNum = maxAccountNum + 1;
maxAccountNum = maxAccountNum + 1;
await client.createMangoAccount(group, accountNum, name, 4, 4, 4, 4);
await client.createMangoAccount(group, accountNum, name, 5, 4, 4, 4);
return (await client.getMangoAccountForOwner(
group,
admin.publicKey,
@ -202,7 +202,7 @@ async function main() {
group,
mangoAccount,
sellMint,
new BN(100000),
new BN(150000),
);
await mangoAccount.reload(client);
@ -217,20 +217,40 @@ async function main() {
.build(),
);
try {
// At a price of $1/ui-SOL we can buy 0.1 ui-SOL for the 100k native-USDC we have.
// With maint weight of 0.9 we have 10x main-leverage. Buying 12x as much causes liquidation.
// At a price of $0.015/ui-SOL we can buy 10 ui-SOL for the 0.15 USDC (150k native-USDC) we have.
// With maint weight of 0.9 we have 10x main-leverage. Buying 11x as much causes liquidation.
await client.serum3PlaceOrder(
group,
mangoAccount,
market.serumMarketExternal,
Serum3Side.bid,
1,
12 * 0.1,
0.015,
11 * 10,
Serum3SelfTradeBehavior.abortTransaction,
Serum3OrderType.limit,
0,
5,
);
await mangoAccount.reload(client);
for (let market of group.serum3MarketsMapByMarketIndex.values()) {
if (market.name == 'SOL/USDC') {
continue;
}
await client.serum3PlaceOrder(
group,
mangoAccount,
market.serumMarketExternal,
Serum3Side.bid,
0.001,
1,
Serum3SelfTradeBehavior.abortTransaction,
Serum3OrderType.limit,
0,
5,
);
await mangoAccount.reload(client);
}
} finally {
// restore the weights
await client.tokenEdit(

View File

@ -1,6 +1,6 @@
import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor';
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
import * as splToken from '@solana/spl-token';
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { Bank } from '../../src/accounts/bank';
import {
@ -280,6 +280,7 @@ async function main() {
group,
newMint,
newOracle.publicKey,
PublicKey.default,
newTokenIndex,
'TMP',
{

View File

@ -57,6 +57,9 @@ async function main() {
`closing serum orders on: ${account} for market ${serumMarket.name}`,
);
await client.serum3CancelAllOrders(group, account, serumExternal, 10);
try {
await client.serum3ConsumeEvents(group, serumExternal);
} catch (e) {}
await client.serum3SettleFunds(group, account, serumExternal);
await client.serum3CloseOpenOrders(group, account, serumExternal);
}

View File

@ -143,6 +143,7 @@ async function registerTokens(): Promise<void> {
group,
usdcMainnetMint,
usdcMainnetOracle,
PublicKey.default,
0,
'USDC',
defaultTokenParams,

View File

@ -224,7 +224,7 @@ async function populateExistingAlts(): Promise<void> {
.map((perpMarket) => [perpMarket.publicKey, perpMarket.oracle])
.flat(),
);
// Well known addressess
// Well known addresses
await extendTable(
client,
group,

View File

@ -398,6 +398,9 @@ async function updateTokenParams(): Promise<void> {
params.depositLimit,
params.zeroUtilRate,
params.platformLiquidationFee,
params.disableAssetLiquidation,
params.collateralFeePerDay,
params.forceWithdraw,
)
.accounts({
group: group.publicKey,

View File

@ -82,6 +82,7 @@ export class Bank implements BankForHealth {
public zeroUtilRate: I80F48;
public platformLiquidationFee: I80F48;
public collectedLiquidationFees: I80F48;
public collectedCollateralFees: I80F48;
static from(
publicKey: PublicKey,
@ -129,6 +130,8 @@ export class Bank implements BankForHealth {
depositWeightScaleStartQuote: number;
reduceOnly: number;
forceClose: number;
disableAssetLiquidation: number;
forceWithdraw: number;
feesWithdrawn: BN;
tokenConditionalSwapTakerFeeRate: number;
tokenConditionalSwapMakerFeeRate: number;
@ -146,6 +149,8 @@ export class Bank implements BankForHealth {
zeroUtilRate: I80F48Dto;
platformLiquidationFee: I80F48Dto;
collectedLiquidationFees: I80F48Dto;
collectedCollateralFees: I80F48Dto;
collateralFeePerDay: number;
},
): Bank {
return new Bank(
@ -210,6 +215,10 @@ export class Bank implements BankForHealth {
obj.zeroUtilRate,
obj.platformLiquidationFee,
obj.collectedLiquidationFees,
obj.disableAssetLiquidation == 0,
obj.collectedCollateralFees,
obj.collateralFeePerDay,
obj.forceWithdraw == 1,
);
}
@ -275,6 +284,10 @@ export class Bank implements BankForHealth {
zeroUtilRate: I80F48Dto,
platformLiquidationFee: I80F48Dto,
collectedLiquidationFees: I80F48Dto,
public allowAssetLiquidation: boolean,
collectedCollateralFees: I80F48Dto,
public collateralFeePerDay: number,
public forceWithdraw: boolean,
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
this.oracleConfig = {
@ -307,6 +320,7 @@ export class Bank implements BankForHealth {
this.zeroUtilRate = I80F48.from(zeroUtilRate);
this.platformLiquidationFee = I80F48.from(platformLiquidationFee);
this.collectedLiquidationFees = I80F48.from(collectedLiquidationFees);
this.collectedCollateralFees = I80F48.from(collectedCollateralFees);
this._price = undefined;
this._uiPrice = undefined;
this._oracleLastUpdatedSlot = undefined;

View File

@ -55,6 +55,7 @@ export class Group {
fastListingIntervalStart: BN;
fastListingsInInterval: number;
allowedFastListingsPerInterval: number;
collateralFeeInterval: BN;
},
): Group {
return new Group(
@ -79,6 +80,7 @@ export class Group {
obj.fastListingIntervalStart,
obj.fastListingsInInterval,
obj.allowedFastListingsPerInterval,
obj.collateralFeeInterval,
[], // addressLookupTablesList
new Map(), // banksMapByName
new Map(), // banksMapByMint
@ -118,6 +120,7 @@ export class Group {
public fastListingIntervalStart: BN,
public fastListingsInInterval: number,
public allowedFastListingsPerInterval: number,
public collateralFeeInterval: BN,
public addressLookupTablesList: AddressLookupTableAccount[],
public banksMapByName: Map<string, Bank[]>,
public banksMapByMint: Map<string, Bank[]>,

View File

@ -443,7 +443,7 @@ export class PerpMarket {
/**
*
* Returns instantaneous funding rate for the day. How is it actually applied - funding is
* continously applied on every interaction to a perp position. The rate is further multiplied
* continuously applied on every interaction to a perp position. The rate is further multiplied
* by the time elapsed since it was last applied (capped to max. 1hr).
*
* @param bids

View File

@ -7,8 +7,10 @@ import {
} from '@coral-xyz/anchor';
import { OpenOrders, decodeEventQueue } from '@project-serum/serum';
import {
createAccount,
createCloseAccountInstruction,
createInitializeAccount3Instruction,
unpackAccount,
} from '@solana/spl-token';
import {
AccountInfo,
@ -24,6 +26,7 @@ import {
RecentPrioritizationFees,
SYSVAR_INSTRUCTIONS_PUBKEY,
SYSVAR_RENT_PUBKEY,
Signer,
SystemProgram,
TransactionInstruction,
} from '@solana/web3.js';
@ -322,6 +325,7 @@ export class MangoClient {
feesMngoTokenIndex?: TokenIndex,
feesExpiryInterval?: BN,
allowedFastListingsPerInterval?: number,
collateralFeeInterval?: BN,
): Promise<MangoSignatureStatus> {
const ix = await this.program.methods
.groupEdit(
@ -337,6 +341,7 @@ export class MangoClient {
feesMngoTokenIndex ?? null,
feesExpiryInterval ?? null,
allowedFastListingsPerInterval ?? null,
collateralFeeInterval ?? null,
)
.accounts({
group: group.publicKey,
@ -443,6 +448,7 @@ export class MangoClient {
group: Group,
mintPk: PublicKey,
oraclePk: PublicKey,
fallbackOraclePk: PublicKey,
tokenIndex: number,
name: string,
params: TokenRegisterParams,
@ -478,12 +484,15 @@ export class MangoClient {
params.depositLimit,
params.zeroUtilRate,
params.platformLiquidationFee,
params.disableAssetLiquidation,
params.collateralFeePerDay,
)
.accounts({
group: group.publicKey,
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
mint: mintPk,
oracle: oraclePk,
fallbackOracle: fallbackOraclePk,
payer: (this.program.provider as AnchorProvider).wallet.publicKey,
rent: SYSVAR_RENT_PUBKEY,
})
@ -560,14 +569,18 @@ export class MangoClient {
params.maintWeightShiftAssetTarget,
params.maintWeightShiftLiabTarget,
params.maintWeightShiftAbort ?? false,
params.setFallbackOracle ?? false,
params.fallbackOracle !== null, // setFallbackOracle
params.depositLimit,
params.zeroUtilRate,
params.platformLiquidationFee,
params.disableAssetLiquidation,
params.collateralFeePerDay,
params.forceWithdraw,
)
.accounts({
group: group.publicKey,
oracle: params.oracle ?? bank.oracle,
fallbackOracle: params.fallbackOracle ?? bank.fallbackOracle,
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
mintInfo: mintInfo.publicKey,
})
@ -629,6 +642,94 @@ export class MangoClient {
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
}
public async tokenForceWithdraw(
group: Group,
mangoAccount: MangoAccount,
tokenIndex: TokenIndex,
): Promise<MangoSignatureStatus> {
const bank = group.getFirstBankByTokenIndex(tokenIndex);
if (!bank.forceWithdraw) {
throw new Error('Bank is not in force-withdraw mode');
}
const ownerAtaTokenAccount = await getAssociatedTokenAddress(
bank.mint,
mangoAccount.owner,
true,
);
let alternateOwnerTokenAccount = PublicKey.default;
const preInstructions: TransactionInstruction[] = [];
const postInstructions: TransactionInstruction[] = [];
const ai = await this.connection.getAccountInfo(ownerAtaTokenAccount);
// ensure withdraws don't fail with missing ATAs
if (ai == null) {
preInstructions.push(
await createAssociatedTokenAccountIdempotentInstruction(
(this.program.provider as AnchorProvider).wallet.publicKey,
mangoAccount.owner,
bank.mint,
),
);
// wsol case
if (bank.mint.equals(NATIVE_MINT)) {
postInstructions.push(
createCloseAccountInstruction(
ownerAtaTokenAccount,
mangoAccount.owner,
mangoAccount.owner,
),
);
}
} else {
const account = await unpackAccount(ownerAtaTokenAccount, ai);
// if owner is not same as mango account's owner on the ATA (for whatever reason)
// then create another token account
if (!account.owner.equals(mangoAccount.owner)) {
const kp = Keypair.generate();
alternateOwnerTokenAccount = kp.publicKey;
await createAccount(
this.connection,
(this.program.provider as AnchorProvider).wallet as any as Signer,
bank.mint,
mangoAccount.owner,
kp,
);
// wsol case
if (bank.mint.equals(NATIVE_MINT)) {
postInstructions.push(
createCloseAccountInstruction(
alternateOwnerTokenAccount,
mangoAccount.owner,
mangoAccount.owner,
),
);
}
}
}
const ix = await this.program.methods
.tokenForceWithdraw()
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
bank: bank.publicKey,
vault: bank.vault,
oracle: bank.oracle,
ownerAtaTokenAccount,
alternateOwnerTokenAccount,
})
.instruction();
return await this.sendAndConfirmTransactionForGroup(group, [
...preInstructions,
ix,
...postInstructions,
]);
}
public async tokenDeregister(
group: Group,
mintPk: PublicKey,
@ -737,16 +838,20 @@ export class MangoClient {
mintPk: PublicKey,
price: number,
): Promise<MangoSignatureStatus> {
const stubOracle = Keypair.generate();
const ix = await this.program.methods
.stubOracleCreate({ val: I80F48.fromNumber(price).getData() })
.accounts({
group: group.publicKey,
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
oracle: stubOracle.publicKey,
mint: mintPk,
payer: (this.program.provider as AnchorProvider).wallet.publicKey,
})
.instruction();
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
return await this.sendAndConfirmTransactionForGroup(group, [ix], {
additionalSigners: [stubOracle],
});
}
public async stubOracleClose(

View File

@ -30,6 +30,8 @@ export interface TokenRegisterParams {
depositLimit: BN;
zeroUtilRate: number;
platformLiquidationFee: number;
disableAssetLiquidation: boolean;
collateralFeePerDay: number;
}
export const DefaultTokenRegisterParams: TokenRegisterParams = {
@ -70,6 +72,8 @@ export const DefaultTokenRegisterParams: TokenRegisterParams = {
depositLimit: new BN(0),
zeroUtilRate: 0.0,
platformLiquidationFee: 0.0,
disableAssetLiquidation: false,
collateralFeePerDay: 0.0,
};
export interface TokenEditParams {
@ -107,10 +111,13 @@ export interface TokenEditParams {
maintWeightShiftAssetTarget: number | null;
maintWeightShiftLiabTarget: number | null;
maintWeightShiftAbort: boolean | null;
setFallbackOracle: boolean | null;
fallbackOracle: PublicKey | null;
depositLimit: BN | null;
zeroUtilRate: number | null;
platformLiquidationFee: number | null;
disableAssetLiquidation: boolean | null;
collateralFeePerDay: number | null;
forceWithdraw: boolean | null;
}
export const NullTokenEditParams: TokenEditParams = {
@ -148,10 +155,13 @@ export const NullTokenEditParams: TokenEditParams = {
maintWeightShiftAssetTarget: null,
maintWeightShiftLiabTarget: null,
maintWeightShiftAbort: null,
setFallbackOracle: null,
fallbackOracle: null,
depositLimit: null,
zeroUtilRate: null,
platformLiquidationFee: null,
disableAssetLiquidation: null,
collateralFeePerDay: null,
forceWithdraw: null,
};
export interface PerpEditParams {
@ -299,6 +309,7 @@ export interface IxGateParams {
TokenConditionalSwapCreatePremiumAuction: boolean;
TokenConditionalSwapCreateLinearAuction: boolean;
Serum3PlaceOrderV2: boolean;
TokenForceWithdraw: boolean;
}
// Default with all ixs enabled, use with buildIxGate
@ -378,6 +389,7 @@ export const TrueIxGateParams: IxGateParams = {
TokenConditionalSwapCreatePremiumAuction: true,
TokenConditionalSwapCreateLinearAuction: true,
Serum3PlaceOrderV2: true,
TokenForceWithdraw: true,
};
// build ix gate e.g. buildIxGate(Builder(TrueIxGateParams).TokenDeposit(false).build()).toNumber(),
@ -467,6 +479,7 @@ export function buildIxGate(p: IxGateParams): BN {
toggleIx(ixGate, p, 'TokenConditionalSwapCreatePremiumAuction', 69);
toggleIx(ixGate, p, 'TokenConditionalSwapCreateLinearAuction', 70);
toggleIx(ixGate, p, 'Serum3PlaceOrderV2', 71);
toggleIx(ixGate, p, 'TokenForceWithdraw', 72);
return ixGate;
}

View File

@ -1,5 +1,5 @@
export type MangoV4 = {
"version": "0.22.0",
"version": "0.23.0",
"name": "mango_v4",
"instructions": [
{
@ -277,6 +277,12 @@ export type MangoV4 = {
"type": {
"option": "u16"
}
},
{
"name": "collateralFeeIntervalOpt",
"type": {
"option": "u64"
}
}
]
},
@ -631,6 +637,14 @@ export type MangoV4 = {
{
"name": "platformLiquidationFee",
"type": "f32"
},
{
"name": "disableAssetLiquidation",
"type": "bool"
},
{
"name": "collateralFeePerDay",
"type": "f32"
}
]
},
@ -1041,6 +1055,24 @@ export type MangoV4 = {
"type": {
"option": "f32"
}
},
{
"name": "disableAssetLiquidationOpt",
"type": {
"option": "bool"
}
},
{
"name": "collateralFeePerDayOpt",
"type": {
"option": "f32"
}
},
{
"name": "forceWithdrawOpt",
"type": {
"option": "bool"
}
}
]
},
@ -3763,6 +3795,63 @@ export type MangoV4 = {
}
]
},
{
"name": "tokenForceWithdraw",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group"
]
},
{
"name": "bank",
"isMut": true,
"isSigner": false,
"relations": [
"group",
"vault",
"oracle"
]
},
{
"name": "vault",
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
},
{
"name": "ownerAtaTokenAccount",
"isMut": true,
"isSigner": false
},
{
"name": "alternateOwnerTokenAccount",
"isMut": true,
"isSigner": false,
"docs": [
"Only for the unusual case where the owner_ata account is not owned by account.owner"
]
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": []
},
{
"name": "perpCreateMarket",
"docs": [
@ -5953,6 +6042,25 @@ export type MangoV4 = {
}
]
},
{
"name": "tokenChargeCollateralFees",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group"
]
}
],
"args": []
},
{
"name": "altSet",
"accounts": [
@ -7373,12 +7481,24 @@ export type MangoV4 = {
"name": "forceClose",
"type": "u8"
},
{
"name": "disableAssetLiquidation",
"docs": [
"If set to 1, deposits cannot be liquidated when an account is liquidatable.",
"That means bankrupt accounts may still have assets of this type deposited."
],
"type": "u8"
},
{
"name": "forceWithdraw",
"type": "u8"
},
{
"name": "padding",
"type": {
"array": [
"u8",
6
4
]
}
},
@ -7513,12 +7633,30 @@ export type MangoV4 = {
"defined": "I80F48"
}
},
{
"name": "collectedCollateralFees",
"docs": [
"Collateral fees that have been collected (in native tokens)",
"",
"See also collected_fees_native and fees_withdrawn."
],
"type": {
"defined": "I80F48"
}
},
{
"name": "collateralFeePerDay",
"docs": [
"The daily collateral fees rate for fully utilized collateral."
],
"type": "f32"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
1920
1900
]
}
}
@ -7646,12 +7784,28 @@ export type MangoV4 = {
],
"type": "u16"
},
{
"name": "padding2",
"type": {
"array": [
"u8",
4
]
}
},
{
"name": "collateralFeeInterval",
"docs": [
"Intervals in which collateral fee is applied"
],
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
1812
1800
]
}
}
@ -7773,12 +7927,27 @@ export type MangoV4 = {
],
"type": "u64"
},
{
"name": "temporaryDelegate",
"type": "publicKey"
},
{
"name": "temporaryDelegateExpiry",
"type": "u64"
},
{
"name": "lastCollateralFeeCharge",
"docs": [
"Time at which the last collateral fee was charged"
],
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
200
152
]
}
},
@ -9548,12 +9717,16 @@ export type MangoV4 = {
"name": "temporaryDelegateExpiry",
"type": "u64"
},
{
"name": "lastCollateralFeeCharge",
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
160
152
]
}
}
@ -10474,6 +10647,9 @@ export type MangoV4 = {
},
{
"name": "Swap"
},
{
"name": "SwapWithoutFee"
}
]
}
@ -10829,6 +11005,9 @@ export type MangoV4 = {
},
{
"name": "Serum3PlaceOrderV2"
},
{
"name": "TokenForceWithdraw"
}
]
}
@ -13746,6 +13925,76 @@ export type MangoV4 = {
"index": false
}
]
},
{
"name": "TokenCollateralFeeLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mangoAccount",
"type": "publicKey",
"index": false
},
{
"name": "tokenIndex",
"type": "u16",
"index": false
},
{
"name": "assetUsageFraction",
"type": "i128",
"index": false
},
{
"name": "fee",
"type": "i128",
"index": false
},
{
"name": "price",
"type": "i128",
"index": false
}
]
},
{
"name": "ForceWithdrawLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mangoAccount",
"type": "publicKey",
"index": false
},
{
"name": "tokenIndex",
"type": "u16",
"index": false
},
{
"name": "quantity",
"type": "u64",
"index": false
},
{
"name": "price",
"type": "i128",
"index": false
},
{
"name": "toTokenAccount",
"type": "publicKey",
"index": false
}
]
}
],
"errors": [
@ -14093,12 +14342,17 @@ export type MangoV4 = {
"code": 6068,
"name": "MissingFeedForCLMMOracle",
"msg": "Pyth USDC/USD or SOL/USD feed not found (required by CLMM oracle)"
},
{
"code": 6069,
"name": "TokenAssetLiquidationDisabled",
"msg": "the asset does not allow liquidation"
}
]
};
export const IDL: MangoV4 = {
"version": "0.22.0",
"version": "0.23.0",
"name": "mango_v4",
"instructions": [
{
@ -14376,6 +14630,12 @@ export const IDL: MangoV4 = {
"type": {
"option": "u16"
}
},
{
"name": "collateralFeeIntervalOpt",
"type": {
"option": "u64"
}
}
]
},
@ -14730,6 +14990,14 @@ export const IDL: MangoV4 = {
{
"name": "platformLiquidationFee",
"type": "f32"
},
{
"name": "disableAssetLiquidation",
"type": "bool"
},
{
"name": "collateralFeePerDay",
"type": "f32"
}
]
},
@ -15140,6 +15408,24 @@ export const IDL: MangoV4 = {
"type": {
"option": "f32"
}
},
{
"name": "disableAssetLiquidationOpt",
"type": {
"option": "bool"
}
},
{
"name": "collateralFeePerDayOpt",
"type": {
"option": "f32"
}
},
{
"name": "forceWithdrawOpt",
"type": {
"option": "bool"
}
}
]
},
@ -17862,6 +18148,63 @@ export const IDL: MangoV4 = {
}
]
},
{
"name": "tokenForceWithdraw",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group"
]
},
{
"name": "bank",
"isMut": true,
"isSigner": false,
"relations": [
"group",
"vault",
"oracle"
]
},
{
"name": "vault",
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
},
{
"name": "ownerAtaTokenAccount",
"isMut": true,
"isSigner": false
},
{
"name": "alternateOwnerTokenAccount",
"isMut": true,
"isSigner": false,
"docs": [
"Only for the unusual case where the owner_ata account is not owned by account.owner"
]
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": []
},
{
"name": "perpCreateMarket",
"docs": [
@ -20052,6 +20395,25 @@ export const IDL: MangoV4 = {
}
]
},
{
"name": "tokenChargeCollateralFees",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group"
]
}
],
"args": []
},
{
"name": "altSet",
"accounts": [
@ -21472,12 +21834,24 @@ export const IDL: MangoV4 = {
"name": "forceClose",
"type": "u8"
},
{
"name": "disableAssetLiquidation",
"docs": [
"If set to 1, deposits cannot be liquidated when an account is liquidatable.",
"That means bankrupt accounts may still have assets of this type deposited."
],
"type": "u8"
},
{
"name": "forceWithdraw",
"type": "u8"
},
{
"name": "padding",
"type": {
"array": [
"u8",
6
4
]
}
},
@ -21612,12 +21986,30 @@ export const IDL: MangoV4 = {
"defined": "I80F48"
}
},
{
"name": "collectedCollateralFees",
"docs": [
"Collateral fees that have been collected (in native tokens)",
"",
"See also collected_fees_native and fees_withdrawn."
],
"type": {
"defined": "I80F48"
}
},
{
"name": "collateralFeePerDay",
"docs": [
"The daily collateral fees rate for fully utilized collateral."
],
"type": "f32"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
1920
1900
]
}
}
@ -21745,12 +22137,28 @@ export const IDL: MangoV4 = {
],
"type": "u16"
},
{
"name": "padding2",
"type": {
"array": [
"u8",
4
]
}
},
{
"name": "collateralFeeInterval",
"docs": [
"Intervals in which collateral fee is applied"
],
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
1812
1800
]
}
}
@ -21872,12 +22280,27 @@ export const IDL: MangoV4 = {
],
"type": "u64"
},
{
"name": "temporaryDelegate",
"type": "publicKey"
},
{
"name": "temporaryDelegateExpiry",
"type": "u64"
},
{
"name": "lastCollateralFeeCharge",
"docs": [
"Time at which the last collateral fee was charged"
],
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
200
152
]
}
},
@ -23647,12 +24070,16 @@ export const IDL: MangoV4 = {
"name": "temporaryDelegateExpiry",
"type": "u64"
},
{
"name": "lastCollateralFeeCharge",
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
160
152
]
}
}
@ -24573,6 +25000,9 @@ export const IDL: MangoV4 = {
},
{
"name": "Swap"
},
{
"name": "SwapWithoutFee"
}
]
}
@ -24928,6 +25358,9 @@ export const IDL: MangoV4 = {
},
{
"name": "Serum3PlaceOrderV2"
},
{
"name": "TokenForceWithdraw"
}
]
}
@ -27845,6 +28278,76 @@ export const IDL: MangoV4 = {
"index": false
}
]
},
{
"name": "TokenCollateralFeeLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mangoAccount",
"type": "publicKey",
"index": false
},
{
"name": "tokenIndex",
"type": "u16",
"index": false
},
{
"name": "assetUsageFraction",
"type": "i128",
"index": false
},
{
"name": "fee",
"type": "i128",
"index": false
},
{
"name": "price",
"type": "i128",
"index": false
}
]
},
{
"name": "ForceWithdrawLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mangoAccount",
"type": "publicKey",
"index": false
},
{
"name": "tokenIndex",
"type": "u16",
"index": false
},
{
"name": "quantity",
"type": "u64",
"index": false
},
{
"name": "price",
"type": "i128",
"index": false
},
{
"name": "toTokenAccount",
"type": "publicKey",
"index": false
}
]
}
],
"errors": [
@ -28192,6 +28695,11 @@ export const IDL: MangoV4 = {
"code": 6068,
"name": "MissingFeedForCLMMOracle",
"msg": "Pyth USDC/USD or SOL/USD feed not found (required by CLMM oracle)"
},
{
"code": 6069,
"name": "TokenAssetLiquidationDisabled",
"msg": "the asset does not allow liquidation"
}
]
};

View File

@ -9,11 +9,13 @@ export class FlashLoanWithdraw {
export type FlashLoanType =
| { unknown: Record<string, never> }
| { swap: Record<string, never> };
| { swap: Record<string, never> }
| { swapWithoutFee: Record<string, never> };
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace FlashLoanType {
export const unknown = { unknown: {} };
export const swap = { swap: {} };
export const swapWithoutFee = { swapWithoutFee: {} };
}
export class InterestRateParams {