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 - name: Verifiable Build
run: | 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 - name: Generate Checksum
run: | run: |

View File

@ -4,6 +4,7 @@ Update this for each program release and mainnet deployment.
## not on mainnet ## not on mainnet
<<<<<<< HEAD
### v0.22.0, 2024-2- ### v0.22.0, 2024-2-
- Perp: Allow reusing your own perp order slots immediately (#817) - Perp: Allow reusing your own perp order slots immediately (#817)
@ -32,6 +33,61 @@ Update this for each program release and mainnet deployment.
## mainnet ## 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 ### v0.21.3, 2024-2-9
Deployment: Feb 9, 2024 at 11:21:58 Central European Standard Time, https://explorer.solana.com/tx/44f2wcLyLiic1aycdaPTdfwXJBMeGeuA984kvCByg4L5iGprH6xW3D35gd3bvZ6kU3SipEtoY3kDuexJghbxL89T 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]] [[package]]
name = "mango-v4" name = "mango-v4"
<<<<<<< HEAD
version = "0.22.0" version = "0.22.0"
=======
version = "0.23.0"
>>>>>>> main
dependencies = [ dependencies = [
"anchor-lang", "anchor-lang",
"anchor-spl", "anchor-spl",
@ -3445,6 +3449,7 @@ dependencies = [
"atty", "atty",
"base64 0.13.1", "base64 0.13.1",
"bincode", "bincode",
"clap 3.2.25",
"derive_builder", "derive_builder",
"fixed 1.11.0 (git+https://github.com/blockworks-foundation/fixed.git?branch=v1.11.0-borsh0_10-mango)", "fixed 1.11.0 (git+https://github.com/blockworks-foundation/fixed.git?branch=v1.11.0-borsh0_10-mango)",
"futures 0.3.28", "futures 0.3.28",
@ -3464,10 +3469,12 @@ dependencies = [
"solana-client", "solana-client",
"solana-rpc", "solana-rpc",
"solana-sdk", "solana-sdk",
"solana-transaction-status",
"spl-associated-token-account 1.1.3", "spl-associated-token-account 1.1.3",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tokio-tungstenite 0.17.2",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
] ]
@ -3528,6 +3535,7 @@ dependencies = [
"once_cell", "once_cell",
"pyth-sdk-solana", "pyth-sdk-solana",
"rand 0.7.3", "rand 0.7.3",
"regex",
"serde", "serde",
"serde_derive", "serde_derive",
"serde_json", "serde_json",

View File

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

View File

@ -23,7 +23,9 @@
- Do a verifiable build - 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) (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 clap::{Args, Parser, Subcommand};
use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
use mango_v4::state::{PlaceOrderType, SelfTradeBehavior, Side};
use mango_v4_client::{ use mango_v4_client::{
keypair_from_cli, pubkey_from_cli, Client, MangoClient, TransactionBuilderConfig, keypair_from_cli, pubkey_from_cli, Client, MangoClient, TransactionBuilderConfig,
}; };
use solana_sdk::pubkey::Pubkey; use solana_sdk::pubkey::Pubkey;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
mod save_snapshot; mod save_snapshot;
mod test_oracles; mod test_oracles;
@ -88,6 +92,98 @@ struct JupiterSwap {
rpc: Rpc, 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)] #[derive(Subcommand, Debug, Clone)]
enum Command { enum Command {
CreateAccount(CreateAccount), CreateAccount(CreateAccount),
@ -128,21 +224,28 @@ enum Command {
#[clap(short, long)] #[clap(short, long)]
output: String, output: String,
}, },
PerpPlaceOrder(PerpPlaceOrder),
Serum3CloseOpenOrders(Serum3CloseOpenOrders),
Serum3CreateOpenOrders(Serum3CreateOpenOrders),
Serum3PlaceOrder(Serum3PlaceOrder),
} }
impl Rpc { impl Rpc {
fn client(&self, override_fee_payer: Option<&str>) -> anyhow::Result<Client> { 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)); let fee_payer = keypair_from_cli(override_fee_payer.unwrap_or(&self.fee_payer));
Ok(Client::new( Ok(Client::builder()
anchor_client::Cluster::from_str(&self.url)?, .cluster(anchor_client::Cluster::from_str(&self.url)?)
solana_sdk::commitment_config::CommitmentConfig::confirmed(), .commitment(solana_sdk::commitment_config::CommitmentConfig::confirmed())
Arc::new(fee_payer), .fee_payer(Some(Arc::new(fee_payer)))
None, .transaction_builder_config(
TransactionBuilderConfig { TransactionBuilderConfig::builder()
prioritization_micro_lamports: Some(5), .prioritization_micro_lamports(Some(5))
compute_budget_per_instruction: Some(250_000), .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 output_mint = pubkey_from_cli(&cmd.output_mint);
let client = MangoClient::new_for_existing_account(client, account, owner).await?; let client = MangoClient::new_for_existing_account(client, account, owner).await?;
let txsig = client let txsig = client
.jupiter_v4() .jupiter_v6()
.swap( .swap(input_mint, output_mint, cmd.amount, cmd.slippage_bps, false)
input_mint,
output_mint,
cmd.amount,
cmd.slippage_bps,
mango_v4_client::JupiterSwapMode::ExactIn,
false,
)
.await?; .await?;
println!("{}", txsig); println!("{}", txsig);
} }
@ -245,6 +341,111 @@ async fn main() -> Result<(), anyhow::Error> {
let client = rpc.client(None)?; let client = rpc.client(None)?;
save_snapshot::save_snapshot(mango_group, client, output).await? 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(()) Ok(())

View File

@ -23,10 +23,10 @@ pub async fn save_snapshot(
} }
fs::create_dir_all(out_path).unwrap(); fs::create_dir_all(out_path).unwrap();
let rpc_url = client.cluster.url().to_string(); let rpc_url = client.config().cluster.url().to_string();
let ws_url = client.cluster.ws_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 let oracles_and_vaults = group_context
.tokens .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 crate::MangoClient;
use anyhow::Context;
use itertools::Itertools; 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 futures::Future;
use mango_v4::state::{EventQueue, EventType, FillEvent, OutEvent, TokenIndex}; use mango_v4::{
use mango_v4_client::PerpMarketContext; 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 prometheus::{register_histogram, Encoder, Histogram, IntCounter, Registry};
use solana_sdk::{ use solana_sdk::{
instruction::{AccountMeta, Instruction}, instruction::{AccountMeta, Instruction},
pubkey::Pubkey, pubkey::Pubkey,
signature::Signature,
}; };
use tokio::task::JoinHandle;
use tracing::*; use tracing::*;
use warp::Filter; use warp::Filter;
@ -80,6 +97,9 @@ pub async fn runner(
interval_consume_events: u64, interval_consume_events: u64,
interval_update_funding: u64, interval_update_funding: u64,
interval_check_for_changes_and_abort: 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> { ) -> Result<(), anyhow::Error> {
let handles1 = mango_client let handles1 = mango_client
.context .context
@ -138,12 +158,18 @@ pub async fn runner(
futures::future::join_all(handles1), futures::future::join_all(handles1),
futures::future::join_all(handles2), futures::future::join_all(handles2),
futures::future::join_all(handles3), 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( MangoClient::loop_check_for_context_changes_and_abort(
mango_client.clone(), mango_client.clone(),
Duration::from_secs(interval_check_for_changes_and_abort), Duration::from_secs(interval_check_for_changes_and_abort),
), ),
serve_metrics(), serve_metrics(),
debugging_handle, debugging_handle,
futures::future::join_all(extra_jobs),
); );
Ok(()) 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 anchor_client::Cluster;
use clap::{Parser, Subcommand}; 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::commitment_config::CommitmentConfig;
use solana_sdk::pubkey::Pubkey; use solana_sdk::pubkey::Pubkey;
use tokio::time; use tokio::time;
@ -58,12 +61,23 @@ struct Cli {
#[clap(long, env, default_value_t = 120)] #[clap(long, env, default_value_t = 120)]
interval_check_new_listings_and_abort: u64, 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)] #[clap(long, env, default_value_t = 10)]
timeout: u64, timeout: u64,
/// prioritize each transaction with this many microlamports/cu #[clap(flatten)]
#[clap(long, env, default_value = "0")] prioritization_fee_cli: priority_fees_cli::PriorityFeeArgs,
prioritization_micro_lamports: u64,
/// 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)] #[derive(Subcommand, Debug, Clone)]
@ -85,6 +99,10 @@ async fn main() -> Result<(), anyhow::Error> {
}; };
let cli = Cli::parse_from(args); 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 owner = Arc::new(keypair_from_cli(&cli.owner));
let rpc_url = cli.rpc_url; let rpc_url = cli.rpc_url;
@ -98,19 +116,23 @@ async fn main() -> Result<(), anyhow::Error> {
let mango_client = Arc::new( let mango_client = Arc::new(
MangoClient::new_for_existing_account( MangoClient::new_for_existing_account(
Client::new( Client::builder()
cluster, .cluster(cluster)
commitment, .commitment(commitment)
owner.clone(), .fee_payer(Some(owner.clone()))
Some(Duration::from_secs(cli.timeout)), .timeout(Duration::from_secs(cli.timeout))
TransactionBuilderConfig { .transaction_builder_config(
prioritization_micro_lamports: (cli.prioritization_micro_lamports > 0) TransactionBuilderConfig::builder()
.then_some(cli.prioritization_micro_lamports), .priority_fee_provider(prio_provider)
compute_budget_per_instruction: None, .compute_budget_per_instruction(None)
}, .build()
), .unwrap(),
)
.fallback_oracle_config(FallbackOracleConfig::Never)
.build()
.unwrap(),
cli.mango_account, cli.mango_account,
owner.clone(), owner,
) )
.await?, .await?,
); );
@ -139,12 +161,15 @@ async fn main() -> Result<(), anyhow::Error> {
cli.interval_consume_events, cli.interval_consume_events,
cli.interval_update_funding, cli.interval_update_funding,
cli.interval_check_new_listings_and_abort, cli.interval_check_new_listings_and_abort,
cli.interval_charge_collateral_fees,
cli.max_cu_when_batching,
prio_jobs,
) )
.await .await
} }
Command::Taker { .. } => { Command::Taker { .. } => {
let client = mango_client.clone(); 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}, accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side},
state::TokenIndex, state::TokenIndex,
}; };
use tokio::task::JoinHandle;
use tracing::*; use tracing::*;
use crate::MangoClient; use crate::MangoClient;
pub async fn runner( pub async fn runner(
mango_client: Arc<MangoClient>, mango_client: Arc<MangoClient>,
_debugging_handle: impl Future, debugging_handle: impl Future,
extra_jobs: Vec<JoinHandle<()>>,
) -> Result<(), anyhow::Error> { ) -> Result<(), anyhow::Error> {
ensure_deposit(&mango_client).await?; ensure_deposit(&mango_client).await?;
ensure_oo(&mango_client).await?; ensure_oo(&mango_client).await?;
@ -53,7 +55,9 @@ pub async fn runner(
futures::join!( futures::join!(
futures::future::join_all(handles1), futures::future::join_all(handles1),
futures::future::join_all(handles2) futures::future::join_all(handles2),
debugging_handle,
futures::future::join_all(extra_jobs),
); );
Ok(()) Ok(())

View File

@ -48,3 +48,4 @@ tokio = { version = "1", features = ["full"] }
tokio-stream = { version = "0.1.9"} tokio-stream = { version = "0.1.9"}
tokio-tungstenite = "0.16.1" tokio-tungstenite = "0.16.1"
tracing = "0.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::collections::HashSet;
use std::time::Duration; use std::time::Duration;
use itertools::Itertools; use itertools::Itertools;
use mango_v4::health::{HealthCache, HealthType}; use mango_v4::health::{HealthCache, HealthType};
use mango_v4::state::{MangoAccountValue, PerpMarketIndex, Side, TokenIndex, QUOTE_TOKEN_INDEX}; 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 solana_sdk::signature::Signature;
use futures::{stream, StreamExt, TryStreamExt}; use futures::{stream, StreamExt, TryStreamExt};
@ -19,6 +20,16 @@ pub struct Config {
pub min_health_ratio: f64, pub min_health_ratio: f64,
pub refresh_timeout: Duration, pub refresh_timeout: Duration,
pub compute_limit_for_liq_ix: u32, 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> { struct LiquidateHelper<'a> {
@ -29,8 +40,6 @@ struct LiquidateHelper<'a> {
health_cache: &'a HealthCache, health_cache: &'a HealthCache,
maint_health: I80F48, maint_health: I80F48,
liqor_min_health_ratio: I80F48, liqor_min_health_ratio: I80F48,
allowed_asset_tokens: HashSet<Pubkey>,
allowed_liab_tokens: HashSet<Pubkey>,
config: Config, config: Config,
} }
@ -46,7 +55,7 @@ impl<'a> LiquidateHelper<'a> {
Ok((*orders, *open_orders)) Ok((*orders, *open_orders))
}) })
.try_collect(); .try_collect();
let serum_force_cancels = serum_oos? let mut serum_force_cancels = serum_oos?
.into_iter() .into_iter()
.filter_map(|(orders, open_orders)| { .filter_map(|(orders, open_orders)| {
let can_force_cancel = open_orders.native_coin_total > 0 let can_force_cancel = open_orders.native_coin_total > 0
@ -62,18 +71,42 @@ impl<'a> LiquidateHelper<'a> {
if serum_force_cancels.is_empty() { if serum_force_cancels.is_empty() {
return Ok(None); return Ok(None);
} }
// Cancel all orders on a random serum market serum_force_cancels.shuffle(&mut rand::thread_rng());
let serum_orders = serum_force_cancels.choose(&mut rand::thread_rng()).unwrap();
let txsig = self let mut ixs = PreparedInstructions::new();
.client let mut cancelled_markets = vec![];
.serum3_liq_force_cancel_orders( let mut tx_builder = self.client.transaction_builder().await?;
(self.pubkey, self.liqee),
serum_orders.market_index, for force_cancel in serum_force_cancels {
&serum_orders.open_orders, let mut new_ixs = ixs.clone();
) new_ixs.append(
.await?; 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!( info!(
market_index = serum_orders.market_index, market_indexes = ?cancelled_markets,
%txsig, %txsig,
"Force cancelled serum orders", "Force cancelled serum orders",
); );
@ -108,6 +141,25 @@ impl<'a> LiquidateHelper<'a> {
let all_perp_base_positions: anyhow::Result< let all_perp_base_positions: anyhow::Result<
Vec<Option<(PerpMarketIndex, i64, I80F48, I80F48)>>, Vec<Option<(PerpMarketIndex, i64, I80F48, I80F48)>>,
> = stream::iter(self.liqee.active_perp_positions()) > = 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 { .then(|pp| async {
let base_lots = pp.base_position_lots(); let base_lots = pp.base_position_lots();
if (base_lots == 0 && pp.quote_position_native() <= 0) || pp.has_open_taker_fills() if (base_lots == 0 && pp.quote_position_native() <= 0) || pp.has_open_taker_fills()
@ -155,10 +207,7 @@ impl<'a> LiquidateHelper<'a> {
.await .await
.context("getting liquidator account")?; .context("getting liquidator account")?;
liqor.ensure_perp_position(*perp_market_index, QUOTE_TOKEN_INDEX)?; liqor.ensure_perp_position(*perp_market_index, QUOTE_TOKEN_INDEX)?;
let mut health_cache = let mut health_cache = self.client.health_cache(&liqor).await.expect("always ok");
health_cache::new(&self.client.context, self.account_fetcher, &liqor)
.await
.context("health cache")?;
let quote_bank = self let quote_bank = self
.client .client
.first_bank(QUOTE_TOKEN_INDEX) .first_bank(QUOTE_TOKEN_INDEX)
@ -328,6 +377,7 @@ impl<'a> LiquidateHelper<'a> {
.health_cache .health_cache
.token_infos .token_infos
.iter() .iter()
.filter(|p| !self.config.forbidden_tokens.contains(&p.token_index))
.zip( .zip(
self.health_cache self.health_cache
.effective_token_balances(HealthType::LiquidationEnd) .effective_token_balances(HealthType::LiquidationEnd)
@ -345,34 +395,17 @@ impl<'a> LiquidateHelper<'a> {
.filter_map(|(ti, effective)| { .filter_map(|(ti, effective)| {
// check constraints for liquidatable assets, see also has_possible_spot_liquidations() // check constraints for liquidatable assets, see also has_possible_spot_liquidations()
let tokens = ti.balance_spot.min(effective.spot_and_perp); 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; let quote_value = tokens * ti.prices.oracle;
// prefer to liquidate tokens with asset weight that have >$1 liquidatable // prefer to liquidate tokens with asset weight that have >$1 liquidatable
let is_preferred = 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)) is_valid_asset.then_some((ti.token_index, is_preferred, quote_value))
}) })
.collect_vec(); .collect_vec();
// sort such that preferred tokens are at the end, and the one with the larget quote value is // sort such that preferred tokens are at the start, and the one with the larget quote value is
// at the very end // at 0
potential_assets.sort_by_key(|(_, is_preferred, amount)| (*is_preferred, *amount)); potential_assets.sort_by_key(|(_, is_preferred, amount)| Reverse((*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,
),
};
// //
// find a good liab, same as for assets // 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 tokens = (-ti.balance_spot).min(-effective.spot_and_perp);
let is_valid_liab = tokens > 0; let is_valid_liab = tokens > 0;
let quote_value = tokens * ti.prices.oracle; 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(); .collect_vec();
// largest liquidatable liability at the end // largest liquidatable liability at the start
potential_liabs.sort_by_key(|(_, amount)| *amount); 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, _)| { // Find a pair
let is_allowed = self //
.allowed_liab_tokens
.contains(&self.client.context.token(*ti).mint);
is_allowed.then_some(*ti)
});
let liab_token_index = match potential_allowed_liabs.last() { fn find_best_token(
Some(token_index) => token_index, lh: &LiquidateHelper,
None => anyhow::bail!( token_list: &Vec<(TokenIndex, bool, I80F48)>,
"mango account {}, has no liab tokens that are liquidatable: {:?}", ) -> (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, self.pubkey,
potential_assets,
potential_liabs, potential_liabs,
), )
}; };
let (asset_token_index, liab_token_index) = best_pair_opt.unwrap();
//
// Compute max transfer size
//
let max_liab_transfer = self let max_liab_transfer = self
.max_token_liab_transfer(liab_token_index, asset_token_index) .max_token_liab_transfer(liab_token_index, asset_token_index)
.await .await
@ -459,9 +532,7 @@ impl<'a> LiquidateHelper<'a> {
.iter() .iter()
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| { .find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
liab_usdc_equivalent.is_negative() liab_usdc_equivalent.is_negative()
&& self && !self.config.forbidden_tokens.contains(liab_token_index)
.allowed_liab_tokens
.contains(&self.client.context.token(*liab_token_index).mint)
}) })
.ok_or_else(|| { .ok_or_else(|| {
anyhow::anyhow!( 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 liqor_min_health_ratio = I80F48::from_num(config.min_health_ratio);
let account = account_fetcher.fetch_mango_account(pubkey)?; 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 .await
.context("creating health cache 1")?; .context("creating health cache 1")?;
let maint_health = health_cache.health(HealthType::Maint); 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 // This is -- unfortunately -- needed because the websocket streams seem to not
// be great at providing timely updates to the account data. // be great at providing timely updates to the account data.
let account = account_fetcher.fetch_fresh_mango_account(pubkey).await?; 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 .await
.context("creating health cache 2")?; .context("creating health cache 2")?;
if !health_cache.is_liquidatable() { if !health_cache.is_liquidatable() {
@ -616,8 +689,6 @@ pub async fn maybe_liquidate_account(
let maint_health = health_cache.health(HealthType::Maint); 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 // try liquidating
let maybe_txsig = LiquidateHelper { let maybe_txsig = LiquidateHelper {
client: mango_client, client: mango_client,
@ -627,8 +698,6 @@ pub async fn maybe_liquidate_account(
health_cache: &health_cache, health_cache: &health_cache,
maint_health, maint_health,
liqor_min_health_ratio, liqor_min_health_ratio,
allowed_asset_tokens: all_token_mints.clone(),
allowed_liab_tokens: all_token_mints,
config: config.clone(), config: config.clone(),
} }
.send_liq_tx() .send_liq_tx()

View File

@ -9,7 +9,7 @@ use clap::Parser;
use mango_v4::state::{PerpMarketIndex, TokenIndex}; use mango_v4::state::{PerpMarketIndex, TokenIndex};
use mango_v4_client::AsyncChannelSendUnlessFull; use mango_v4_client::AsyncChannelSendUnlessFull;
use mango_v4_client::{ 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, snapshot_source, websocket_source, Client, MangoClient, MangoClientError, MangoGroupContext,
TransactionBuilderConfig, TransactionBuilderConfig,
}; };
@ -20,14 +20,17 @@ use solana_sdk::pubkey::Pubkey;
use solana_sdk::signer::Signer; use solana_sdk::signer::Signer;
use tracing::*; use tracing::*;
pub mod cli_args;
pub mod liquidate; pub mod liquidate;
pub mod metrics; pub mod metrics;
pub mod rebalance; pub mod rebalance;
pub mod telemetry; pub mod telemetry;
pub mod token_swap_info; pub mod token_swap_info;
pub mod trigger_tcs; pub mod trigger_tcs;
mod unwrappable_oracle_error;
pub mod util; pub mod util;
use crate::unwrappable_oracle_error::UnwrappableOracleError;
use crate::util::{is_mango_account, is_mint_info, is_perp_market}; use crate::util::{is_mango_account, is_mint_info, is_perp_market};
// jemalloc seems to be better at keeping the memory footprint reasonable over // 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] #[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; 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 { pub fn encode_address(addr: &Pubkey) -> String {
bs58::encode(&addr.to_bytes()).into_string() bs58::encode(&addr.to_bytes()).into_string()
} }
@ -186,20 +46,31 @@ pub fn encode_address(addr: &Pubkey) -> String {
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
mango_v4_client::tracing_subscriber_init(); 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)?; 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 { } else {
dotenv::dotenv().ok(); dotenv::dotenv().ok();
std::env::args_os().collect() std::env::args_os().collect()
}; };
let cli = Cli::parse_from(args); 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 rpc_url = cli.rpc_url;
let ws_url = rpc_url.replace("https", "wss"); let ws_url = rpc_url.replace("https", "wss");
let rpc_timeout = Duration::from_secs(10); let rpc_timeout = Duration::from_secs(10);
let cluster = Cluster::Custom(rpc_url.clone(), ws_url.clone()); let cluster = Cluster::Custom(rpc_url.clone(), ws_url.clone());
let commitment = CommitmentConfig::processed(); let commitment = CommitmentConfig::processed();
@ -207,16 +78,18 @@ async fn main() -> anyhow::Result<()> {
.cluster(cluster.clone()) .cluster(cluster.clone())
.commitment(commitment) .commitment(commitment)
.fee_payer(Some(liqor_owner.clone())) .fee_payer(Some(liqor_owner.clone()))
.timeout(Some(rpc_timeout)) .timeout(rpc_timeout)
.jupiter_v4_url(cli.jupiter_v4_url)
.jupiter_v6_url(cli.jupiter_v6_url) .jupiter_v6_url(cli.jupiter_v6_url)
.jupiter_token(cli.jupiter_token) .jupiter_token(cli.jupiter_token)
.transaction_builder_config(TransactionBuilderConfig { .transaction_builder_config(
prioritization_micro_lamports: (cli.prioritization_micro_lamports > 0) TransactionBuilderConfig::builder()
.then_some(cli.prioritization_micro_lamports), .priority_fee_provider(prio_provider)
// Liquidation and tcs triggers set their own budgets, this is a default for other tx // Liquidation and tcs triggers set their own budgets, this is a default for other tx
compute_budget_per_instruction: Some(250_000), .compute_budget_per_instruction(Some(250_000))
}) .build()
.unwrap(),
)
.override_send_transaction_urls(cli.override_send_transaction_url)
.build() .build()
.unwrap(); .unwrap();
@ -225,7 +98,7 @@ async fn main() -> anyhow::Result<()> {
// Reading accounts from chain_data // Reading accounts from chain_data
let account_fetcher = Arc::new(chain_data::AccountFetcher { let account_fetcher = Arc::new(chain_data::AccountFetcher {
chain_data: chain_data.clone(), chain_data: chain_data.clone(),
rpc: client.rpc_async(), rpc: client.new_rpc_async(),
}); });
let mango_account = account_fetcher 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"); 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 let mango_oracles = group_context
.tokens .tokens
@ -319,8 +192,8 @@ async fn main() -> anyhow::Result<()> {
}; };
let token_swap_info_config = token_swap_info::Config { let token_swap_info_config = token_swap_info::Config {
quote_index: 0, // USDC quote_index: 0, // USDC
quote_amount: 1_000_000_000, // TODO: config, $1000, should be >= tcs_config.max_trigger_quote_amount quote_amount: (cli.jupiter_swap_info_amount * 1e6) as u64,
jupiter_version: cli.jupiter_version.into(), jupiter_version: cli.jupiter_version.into(),
}; };
@ -332,24 +205,33 @@ async fn main() -> anyhow::Result<()> {
let liq_config = liquidate::Config { let liq_config = liquidate::Config {
min_health_ratio: cli.min_health_ratio, min_health_ratio: cli.min_health_ratio,
compute_limit_for_liq_ix: cli.compute_limit_for_liquidation, compute_limit_for_liq_ix: cli.compute_limit_for_liquidation,
// TODO: config max_cu_per_transaction: 1_000_000,
refresh_timeout: Duration::from_secs(30), 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 { let tcs_config = trigger_tcs::Config {
min_health_ratio: cli.min_health_ratio, 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, compute_limit_for_trigger: cli.compute_limit_for_tcs,
profit_fraction: cli.tcs_profit_fraction, profit_fraction: cli.tcs_profit_fraction,
collateral_token_index: 0, // USDC collateral_token_index: 0, // USDC
// TODO: config
refresh_timeout: Duration::from_secs(30),
jupiter_version: cli.jupiter_version.into(), jupiter_version: cli.jupiter_version.into(),
jupiter_slippage_bps: cli.rebalance_slippage_bps, jupiter_slippage_bps: cli.rebalance_slippage_bps,
mode: cli.tcs_mode.into(), 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)); 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 { let rebalance_config = rebalance::Config {
enabled: cli.rebalance == BoolArg::True, enabled: cli.rebalance == BoolArg::True,
slippage_bps: cli.rebalance_slippage_bps, slippage_bps: cli.rebalance_slippage_bps,
// TODO: config borrow_settle_excess: (1f64 + cli.rebalance_borrow_settle_excess).max(1f64),
borrow_settle_excess: 1.05, refresh_timeout: Duration::from_secs(cli.rebalance_refresh_timeout_secs),
refresh_timeout: Duration::from_secs(30),
jupiter_version: cli.jupiter_version.into(), jupiter_version: cli.jupiter_version.into(),
skip_tokens: cli skip_tokens: cli.rebalance_skip_tokens.unwrap_or(Vec::new()),
.rebalance_skip_tokens
.split(',')
.filter(|v| !v.is_empty())
.map(|name| mango_client.context.token_by_name(name).token_index)
.collect(),
allow_withdraws: signer_is_owner, allow_withdraws: signer_is_owner,
}; };
@ -388,6 +264,12 @@ async fn main() -> anyhow::Result<()> {
.skip_threshold_for_type(LiqErrorType::Liq, 5) .skip_threshold_for_type(LiqErrorType::Liq, 5)
.skip_duration(Duration::from_secs(120)) .skip_duration(Duration::from_secs(120))
.build()?, .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"); info!("main loop");
@ -501,6 +383,7 @@ async fn main() -> anyhow::Result<()> {
}; };
liquidation.errors.update(); liquidation.errors.update();
liquidation.oracle_errors.update();
let liquidated = liquidation let liquidated = liquidation
.maybe_liquidate_one(account_addresses.iter()) .maybe_liquidate_one(account_addresses.iter())
@ -508,16 +391,13 @@ async fn main() -> anyhow::Result<()> {
let mut took_tcs = false; let mut took_tcs = false;
if !liquidated && cli.take_tcs == BoolArg::True { if !liquidated && cli.take_tcs == BoolArg::True {
took_tcs = match liquidation took_tcs = liquidation
.maybe_take_token_conditional_swap(account_addresses.iter()) .maybe_take_token_conditional_swap(account_addresses.iter())
.await .await
{ .unwrap_or_else(|err| {
Ok(v) => v,
Err(err) => {
error!("error during maybe_take_token_conditional_swap: {err}"); error!("error during maybe_take_token_conditional_swap: {err}");
false false
} })
}
} }
if liquidated || took_tcs { if liquidated || took_tcs {
@ -528,14 +408,15 @@ async fn main() -> anyhow::Result<()> {
}); });
let token_swap_info_job = tokio::spawn({ let token_swap_info_job = tokio::spawn({
// TODO: configurable interval let mut interval = mango_v4_client::delay_interval(Duration::from_secs(
let mut interval = mango_v4_client::delay_interval(Duration::from_secs(60)); cli.token_swap_refresh_interval_secs,
));
let mut startup_wait = mango_v4_client::delay_interval(Duration::from_secs(1)); let mut startup_wait = mango_v4_client::delay_interval(Duration::from_secs(1));
let shared_state = shared_state.clone(); let shared_state = shared_state.clone();
async move { async move {
loop { loop {
startup_wait.tick().await;
if !shared_state.read().unwrap().one_snapshot_done { if !shared_state.read().unwrap().one_snapshot_done {
startup_wait.tick().await;
continue; continue;
} }
@ -570,6 +451,7 @@ async fn main() -> anyhow::Result<()> {
)); ));
} }
use cli_args::{BoolArg, Cli, CliDotenv};
use futures::StreamExt; use futures::StreamExt;
let mut jobs: futures::stream::FuturesUnordered<_> = vec![ let mut jobs: futures::stream::FuturesUnordered<_> = vec![
data_job, data_job,
@ -579,6 +461,7 @@ async fn main() -> anyhow::Result<()> {
check_changes_for_abort_job, check_changes_for_abort_job,
] ]
.into_iter() .into_iter()
.chain(prio_jobs.into_iter())
.collect(); .collect();
jobs.next().await; jobs.next().await;
@ -625,6 +508,7 @@ struct LiquidationState {
trigger_tcs_config: trigger_tcs::Config, trigger_tcs_config: trigger_tcs::Config,
errors: ErrorTracking<Pubkey, LiqErrorType>, errors: ErrorTracking<Pubkey, LiqErrorType>,
oracle_errors: ErrorTracking<TokenIndex, LiqErrorType>,
} }
impl LiquidationState { impl LiquidationState {
@ -678,6 +562,25 @@ impl LiquidationState {
.await; .await;
if let Err(err) = result.as_ref() { 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 // Keep track of pubkeys that had errors
error_tracking.record(LiqErrorType::Liq, pubkey, err.to_string()); error_tracking.record(LiqErrorType::Liq, pubkey, err.to_string());

View File

@ -151,18 +151,7 @@ impl Rebalancer {
let direct_sol_route_job = let direct_sol_route_job =
self.jupiter_quote(sol_mint, output_mint, in_amount_sol, true, jupiter_version); 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]; let 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 mut results = futures::future::join_all(jobs).await; let mut results = futures::future::join_all(jobs).await;
let full_route = results.remove(0)?; let full_route = results.remove(0)?;
@ -211,18 +200,7 @@ impl Rebalancer {
let direct_sol_route_job = let direct_sol_route_job =
self.jupiter_quote(input_mint, sol_mint, in_amount, true, jupiter_version); 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]; let 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 mut results = futures::future::join_all(jobs).await; let mut results = futures::future::join_all(jobs).await;
let full_route = results.remove(0)?; let full_route = results.remove(0)?;
@ -253,7 +231,7 @@ impl Rebalancer {
.prepare_swap_transaction(full) .prepare_swap_transaction(full)
.await?; .await?;
let tx_size = builder.transaction_size()?; let tx_size = builder.transaction_size()?;
if tx_size.is_ok() { if tx_size.is_within_limit() {
return Ok((builder, full.clone())); return Ok((builder, full.clone()));
} }
trace!( trace!(
@ -520,6 +498,7 @@ impl Rebalancer {
}; };
let counters = perp_pnl::fetch_top( let counters = perp_pnl::fetch_top(
&self.mango_client.context, &self.mango_client.context,
&self.mango_client.client.config().fallback_oracle_config,
self.account_fetcher.as_ref(), self.account_fetcher.as_ref(),
perp_position.market_index, perp_position.market_index,
direction, direction,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,8 +6,8 @@ use anchor_client::Cluster;
use clap::Parser; use clap::Parser;
use mango_v4::state::{PerpMarketIndex, TokenIndex}; use mango_v4::state::{PerpMarketIndex, TokenIndex};
use mango_v4_client::{ use mango_v4_client::{
account_update_stream, chain_data, keypair_from_cli, snapshot_source, websocket_source, Client, account_update_stream, chain_data, keypair_from_cli, priority_fees_cli, snapshot_source,
MangoClient, MangoGroupContext, TransactionBuilderConfig, websocket_source, Client, MangoClient, MangoGroupContext, TransactionBuilderConfig,
}; };
use tracing::*; use tracing::*;
@ -61,9 +61,12 @@ struct Cli {
#[clap(long, env, default_value = "100")] #[clap(long, env, default_value = "100")]
get_multiple_accounts_count: usize, get_multiple_accounts_count: usize,
/// prioritize each transaction with this many microlamports/cu #[clap(flatten)]
#[clap(long, env, default_value = "0")] prioritization_fee_cli: priority_fees_cli::PriorityFeeArgs,
prioritization_micro_lamports: u64,
/// url to the lite-rpc websocket, optional
#[clap(long, env, default_value = "")]
lite_rpc_url: String,
/// compute budget for each instruction /// compute budget for each instruction
#[clap(long, env, default_value = "250000")] #[clap(long, env, default_value = "250000")]
@ -87,6 +90,10 @@ async fn main() -> anyhow::Result<()> {
}; };
let cli = Cli::parse_from(args); 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 settler_owner = Arc::new(keypair_from_cli(&cli.settler_owner));
let rpc_url = cli.rpc_url; let rpc_url = cli.rpc_url;
@ -100,11 +107,11 @@ async fn main() -> anyhow::Result<()> {
commitment, commitment,
settler_owner.clone(), settler_owner.clone(),
Some(rpc_timeout), Some(rpc_timeout),
TransactionBuilderConfig { TransactionBuilderConfig::builder()
prioritization_micro_lamports: (cli.prioritization_micro_lamports > 0) .compute_budget_per_instruction(Some(cli.compute_budget_per_instruction))
.then_some(cli.prioritization_micro_lamports), .priority_fee_provider(prio_provider)
compute_budget_per_instruction: Some(cli.compute_budget_per_instruction), .build()
}, .unwrap(),
); );
// The representation of current on-chain account data // The representation of current on-chain account data
@ -112,7 +119,7 @@ async fn main() -> anyhow::Result<()> {
// Reading accounts from chain_data // Reading accounts from chain_data
let account_fetcher = Arc::new(chain_data::AccountFetcher { let account_fetcher = Arc::new(chain_data::AccountFetcher {
chain_data: chain_data.clone(), chain_data: chain_data.clone(),
rpc: client.rpc_async(), rpc: client.new_rpc_async(),
}); });
let mango_account = account_fetcher let mango_account = account_fetcher
@ -120,7 +127,7 @@ async fn main() -> anyhow::Result<()> {
.await?; .await?;
let mango_group = mango_account.fixed.group; 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 let mango_oracles = group_context
.tokens .tokens
@ -352,6 +359,7 @@ async fn main() -> anyhow::Result<()> {
check_changes_for_abort_job, check_changes_for_abort_job,
] ]
.into_iter() .into_iter()
.chain(prio_jobs.into_iter())
.collect(); .collect();
jobs.next().await; 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::accounts_zerocopy::KeyedAccountSharedData;
use mango_v4::health::HealthType; use mango_v4::health::HealthType;
use mango_v4::state::{OracleAccountInfos, PerpMarket, PerpMarketIndex}; use mango_v4::state::{OracleAccountInfos, PerpMarket, PerpMarketIndex};
use mango_v4_client::{ use mango_v4_client::{chain_data, MangoClient, PreparedInstructions, TransactionBuilder};
chain_data, health_cache, prettify_solana_client_error, MangoClient, PreparedInstructions,
TransactionBuilder,
};
use solana_sdk::address_lookup_table_account::AddressLookupTableAccount; use solana_sdk::address_lookup_table_account::AddressLookupTableAccount;
use solana_sdk::commitment_config::CommitmentConfig; use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::signature::Signature; use solana_sdk::signature::Signature;
@ -120,11 +117,10 @@ impl SettlementState {
continue; continue;
} }
let health_cache = let health_cache = match mango_client.health_cache(&account).await {
match health_cache::new(&mango_client.context, account_fetcher, &account).await { Ok(hc) => hc,
Ok(hc) => hc, Err(_) => continue, // Skip for stale/unconfident oracles
Err(_) => continue, // Skip for stale/unconfident oracles };
};
let liq_end_health = health_cache.health(HealthType::LiquidationEnd); let liq_end_health = health_cache.health(HealthType::LiquidationEnd);
for perp_market_index in perp_indexes { for perp_market_index in perp_indexes {
@ -288,7 +284,7 @@ impl<'a> SettleBatchProcessor<'a> {
address_lookup_tables: self.address_lookup_tables.clone(), address_lookup_tables: self.address_lookup_tables.clone(),
payer: fee_payer.pubkey(), payer: fee_payer.pubkey(),
signers: vec![fee_payer], signers: vec![fee_payer],
config: client.transaction_builder_config, config: client.config().transaction_builder_config.clone(),
} }
.transaction_with_blockhash(self.blockhash) .transaction_with_blockhash(self.blockhash)
} }
@ -301,13 +297,7 @@ impl<'a> SettleBatchProcessor<'a> {
let tx = self.transaction()?; let tx = self.transaction()?;
self.instructions.clear(); self.instructions.clear();
let send_result = self let send_result = self.mango_client.client.send_transaction(&tx).await;
.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);
match send_result { match send_result {
Ok(txsig) => { Ok(txsig) => {
@ -328,11 +318,14 @@ impl<'a> SettleBatchProcessor<'a> {
) -> anyhow::Result<Option<Signature>> { ) -> anyhow::Result<Option<Signature>> {
let a_value = self.account_fetcher.fetch_mango_account(&account_a)?; let a_value = self.account_fetcher.fetch_mango_account(&account_a)?;
let b_value = self.account_fetcher.fetch_mango_account(&account_b)?; let b_value = self.account_fetcher.fetch_mango_account(&account_b)?;
let new_ixs = self.mango_client.perp_settle_pnl_instruction( let new_ixs = self
self.perp_market_index, .mango_client
(&account_a, &a_value), .perp_settle_pnl_instruction(
(&account_b, &b_value), self.perp_market_index,
)?; (&account_a, &a_value),
(&account_b, &b_value),
)
.await?;
let previous = self.instructions.clone(); let previous = self.instructions.clone();
self.instructions.append(new_ixs.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 // 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 mint = mango_client.context.token(token_index).mint;
let ix = match mango_client.token_withdraw_instructions( let ix = match mango_client
&liqor_account, .token_withdraw_instructions(&liqor_account, mint, u64::MAX, false)
mint, .await
u64::MAX, {
false,
) {
Ok(ix) => ix, Ok(ix) => ix,
Err(_) => continue, Err(_) => continue,
}; };

View File

@ -15,6 +15,7 @@ async-channel = "1.6"
async-once-cell = { version = "0.4.2", features = ["unpin"] } async-once-cell = { version = "0.4.2", features = ["unpin"] }
async-trait = "0.1.52" async-trait = "0.1.52"
atty = "0.2" atty = "0.2"
clap = { version = "3.1.8", features = ["derive", "env"] }
derive_builder = "0.12.0" derive_builder = "0.12.0"
fixed = { workspace = true, features = ["serde", "borsh"] } fixed = { workspace = true, features = ["serde", "borsh"] }
futures = "0.3.25" futures = "0.3.25"
@ -30,6 +31,7 @@ solana-client = { workspace = true }
solana-rpc = { workspace = true } solana-rpc = { workspace = true }
solana-sdk = { workspace = true } solana-sdk = { workspace = true }
solana-address-lookup-table-program = { workspace = true } solana-address-lookup-table-program = { workspace = true }
solana-transaction-status = { workspace = true }
mango-feeds-connector = { workspace = true } mango-feeds-connector = { workspace = true }
spl-associated-token-account = "1.0.3" spl-associated-token-account = "1.0.3"
thiserror = "1.0.31" thiserror = "1.0.31"
@ -37,6 +39,7 @@ thiserror = "1.0.31"
reqwest = "0.11.17" reqwest = "0.11.17"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tokio-stream = { version = "0.1.9"} tokio-stream = { version = "0.1.9"}
tokio-tungstenite = "0.17.0"
serde = "1.0.141" serde = "1.0.141"
serde_json = "1.0.82" serde_json = "1.0.82"
base64 = "0.13.0" 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_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
use solana_sdk::account::{AccountSharedData, ReadableAccount}; use solana_sdk::account::{AccountSharedData, ReadableAccount};
use solana_sdk::hash::Hash;
use solana_sdk::hash::Hasher;
use solana_sdk::pubkey::Pubkey; use solana_sdk::pubkey::Pubkey;
use mango_v4::state::MangoAccountValue; use mango_v4::state::MangoAccountValue;
use crate::gpa;
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait AccountFetcher: Sync + Send { pub trait AccountFetcher: Sync + Send {
async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData>; async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData>;
@ -29,6 +33,13 @@ pub trait AccountFetcher: Sync + Send {
program: &Pubkey, program: &Pubkey,
discriminator: [u8; 8], discriminator: [u8; 8],
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>>; ) -> 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... // 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())) .map(|(pk, acc)| (pk, acc.into()))
.collect::<Vec<_>>()) .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> { struct CoalescedAsyncJob<Key, Output> {
@ -138,6 +160,8 @@ struct AccountCache {
keys_for_program_and_discriminator: HashMap<(Pubkey, [u8; 8]), Vec<Pubkey>>, keys_for_program_and_discriminator: HashMap<(Pubkey, [u8; 8]), Vec<Pubkey>>,
account_jobs: CoalescedAsyncJob<Pubkey, anyhow::Result<AccountSharedData>>, account_jobs: CoalescedAsyncJob<Pubkey, anyhow::Result<AccountSharedData>>,
multiple_accounts_jobs:
CoalescedAsyncJob<Hash, anyhow::Result<Vec<(Pubkey, AccountSharedData)>>>,
program_accounts_jobs: program_accounts_jobs:
CoalescedAsyncJob<(Pubkey, [u8; 8]), anyhow::Result<Vec<(Pubkey, AccountSharedData)>>>, 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 fixed::types::I80F48;
use mango_v4::accounts_zerocopy::{KeyedAccountSharedData, LoadZeroCopy}; 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; use anyhow::Context;
@ -64,12 +67,34 @@ impl AccountFetcher {
pub fn fetch_bank_and_price(&self, bank: &Pubkey) -> anyhow::Result<(Bank, I80F48)> { pub fn fetch_bank_and_price(&self, bank: &Pubkey) -> anyhow::Result<(Bank, I80F48)> {
let bank: Bank = self.fetch(bank)?; let bank: Bank = self.fetch(bank)?;
let oracle = self.fetch_raw(&bank.oracle)?; let oracle_data = self.fetch_raw(&bank.oracle)?;
let oracle_acc = &KeyedAccountSharedData::new(bank.oracle, oracle.into()); let oracle = &KeyedAccountSharedData::new(bank.oracle, oracle_data.into());
let price = bank.oracle_price(&OracleAccountInfos::from_reader(oracle_acc), None)?;
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)) 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> { pub fn fetch_bank_price(&self, bank: &Pubkey) -> anyhow::Result<I80F48> {
self.fetch_bank_and_price(bank).map(|(_, p)| p) self.fetch_bank_and_price(bank).map(|(_, p)| p)
} }
@ -217,4 +242,20 @@ impl crate::AccountFetcher for AccountFetcher {
}) })
.collect::<Vec<_>>()) .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 anchor_lang::__private::bytemuck;
use mango_v4::state::{ use mango_v4::{
Group, MangoAccountValue, PerpMarketIndex, Serum3MarketIndex, TokenIndex, MAX_BANKS, 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 fixed::types::I80F48;
use futures::{stream, StreamExt, TryStreamExt}; use futures::{stream, StreamExt, TryStreamExt};
use itertools::Itertools; use itertools::Itertools;
use crate::gpa::*; use crate::{gpa::*, AccountFetcher, FallbackOracleConfig};
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync; use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
use solana_sdk::account::Account; use solana_sdk::account::Account;
@ -28,9 +33,10 @@ pub struct TokenContext {
pub oracle: Pubkey, pub oracle: Pubkey,
pub banks: [Pubkey; MAX_BANKS], pub banks: [Pubkey; MAX_BANKS],
pub vaults: [Pubkey; MAX_BANKS], pub vaults: [Pubkey; MAX_BANKS],
pub fallback_oracle: Pubkey, pub fallback_context: FallbackOracleContext,
pub mint_info_address: Pubkey, pub mint_info_address: Pubkey,
pub decimals: u8, pub decimals: u8,
pub oracle_config: OracleConfig,
} }
impl TokenContext { 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)] #[derive(Clone, PartialEq, Eq)]
pub struct Serum3MarketContext { pub struct Serum3MarketContext {
pub address: Pubkey, pub address: Pubkey,
@ -101,6 +119,9 @@ pub struct ComputeEstimates {
pub cu_per_serum3_order_cancel: u32, pub cu_per_serum3_order_cancel: u32,
pub cu_per_perp_order_match: u32, pub cu_per_perp_order_match: u32,
pub cu_per_perp_order_cancel: 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 { impl Default for ComputeEstimates {
@ -118,25 +139,40 @@ impl Default for ComputeEstimates {
cu_per_perp_order_match: 7_000, cu_per_perp_order_match: 7_000,
// measured around 3.5k, see test_perp_compute // measured around 3.5k, see test_perp_compute
cu_per_perp_order_cancel: 7_000, 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 { 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 tokens: u32 = tokens.try_into().unwrap();
let perps: u32 = perps.try_into().unwrap(); let perps: u32 = perps.try_into().unwrap();
let serums: u32 = serums.try_into().unwrap(); let serums: u32 = serums.try_into().unwrap();
let fallbacks: u32 = fallbacks.try_into().unwrap();
tokens * self.health_cu_per_token tokens * self.health_cu_per_token
+ perps * self.health_cu_per_perp + perps * self.health_cu_per_perp
+ serums * self.health_cu_per_serum + 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( self.health_for_counts(
account.active_token_positions().count(), account.active_token_positions().count(),
account.active_perp_positions().count(), account.active_perp_positions().count(),
account.active_serum3_orders().count(), account.active_serum3_orders().count(),
num_fallbacks,
) )
} }
} }
@ -227,8 +263,12 @@ impl MangoGroupContext {
decimals: u8::MAX, decimals: u8::MAX,
banks: mi.banks, banks: mi.banks,
vaults: mi.vaults, vaults: mi.vaults,
fallback_oracle: mi.fallback_oracle,
oracle: mi.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, group: mi.group,
mint: mi.mint, mint: mi.mint,
}, },
@ -236,14 +276,23 @@ impl MangoGroupContext {
}) })
.collect::<HashMap<_, _>>(); .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 // FUTURE: either store the names on MintInfo as well, or maybe don't store them at all
// because they are in metaplex? // because they are in metaplex?
let bank_tuples = fetch_banks(rpc, program, group).await?; 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(); let token = tokens.get_mut(&bank.token_index).unwrap();
token.name = bank.name().into(); token.name = bank.name().into();
token.decimals = bank.mint_decimals; 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)); assert!(tokens.values().all(|t| t.decimals != u8::MAX));
@ -357,6 +406,7 @@ impl MangoGroupContext {
affected_tokens: Vec<TokenIndex>, affected_tokens: Vec<TokenIndex>,
writable_banks: Vec<TokenIndex>, writable_banks: Vec<TokenIndex>,
affected_perp_markets: Vec<PerpMarketIndex>, affected_perp_markets: Vec<PerpMarketIndex>,
fallback_contexts: HashMap<Pubkey, FallbackOracleContext>,
) -> anyhow::Result<(Vec<AccountMeta>, u32)> { ) -> anyhow::Result<(Vec<AccountMeta>, u32)> {
let mut account = account.clone(); let mut account = account.clone();
for affected_token_index in affected_tokens.iter().chain(writable_banks.iter()) { 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 // figure out all the banks/oracles that need to be passed for the health check
let mut banks = vec![]; let mut banks = vec![];
let mut oracles = vec![]; let mut oracles = vec![];
let mut fallbacks = vec![];
for position in account.active_token_positions() { for position in account.active_token_positions() {
let token = self.token(position.token_index); let token = self.token(position.token_index);
banks.push(( banks.push((
@ -377,6 +428,9 @@ impl MangoGroupContext {
writable_banks.iter().any(|&ti| ti == position.token_index), writable_banks.iter().any(|&ti| ti == position.token_index),
)); ));
oracles.push(token.oracle); 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); let serum_oos = account.active_serum3_orders().map(|&s| s.open_orders);
@ -386,6 +440,14 @@ impl MangoGroupContext {
let perp_oracles = account let perp_oracles = account
.active_perp_positions() .active_perp_positions()
.map(|&pa| self.perp(pa.market_index).oracle); .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 { let to_account_meta = |pubkey| AccountMeta {
pubkey, pubkey,
@ -404,9 +466,12 @@ impl MangoGroupContext {
.chain(perp_markets.map(to_account_meta)) .chain(perp_markets.map(to_account_meta))
.chain(perp_oracles.map(to_account_meta)) .chain(perp_oracles.map(to_account_meta))
.chain(serum_oos.map(to_account_meta)) .chain(serum_oos.map(to_account_meta))
.chain(fallback_oracles.into_iter().map(to_account_meta))
.collect(); .collect();
let cu = self.compute_estimates.health_for_account(&account); let cu = self
.compute_estimates
.health_for_account(&account, fallbacks_len);
Ok((accounts, cu)) Ok((accounts, cu))
} }
@ -417,10 +482,12 @@ impl MangoGroupContext {
account2: &MangoAccountValue, account2: &MangoAccountValue,
affected_tokens: &[TokenIndex], affected_tokens: &[TokenIndex],
writable_banks: &[TokenIndex], writable_banks: &[TokenIndex],
fallback_contexts: HashMap<Pubkey, FallbackOracleContext>,
) -> anyhow::Result<(Vec<AccountMeta>, u32)> { ) -> anyhow::Result<(Vec<AccountMeta>, u32)> {
// figure out all the banks/oracles that need to be passed for the health check // figure out all the banks/oracles that need to be passed for the health check
let mut banks = vec![]; let mut banks = vec![];
let mut oracles = vec![]; let mut oracles = vec![];
let mut fallbacks = vec![];
let token_indexes = account2 let token_indexes = account2
.active_token_positions() .active_token_positions()
@ -434,6 +501,9 @@ impl MangoGroupContext {
let writable_bank = writable_banks.iter().contains(&token_index); let writable_bank = writable_banks.iter().contains(&token_index);
banks.push((token.first_bank(), writable_bank)); banks.push((token.first_bank(), writable_bank));
oracles.push(token.oracle); oracles.push(token.oracle);
if let Some(fallback_context) = fallback_contexts.get(&token.oracle) {
fallbacks.extend(fallback_context.keys());
}
} }
let serum_oos = account2 let serum_oos = account2
@ -452,6 +522,14 @@ impl MangoGroupContext {
let perp_oracles = perp_market_indexes let perp_oracles = perp_market_indexes
.iter() .iter()
.map(|&index| self.perp(index).oracle); .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 { let to_account_meta = |pubkey| AccountMeta {
pubkey, pubkey,
@ -470,6 +548,7 @@ impl MangoGroupContext {
.chain(perp_markets.map(to_account_meta)) .chain(perp_markets.map(to_account_meta))
.chain(perp_oracles.map(to_account_meta)) .chain(perp_oracles.map(to_account_meta))
.chain(serum_oos.map(to_account_meta)) .chain(serum_oos.map(to_account_meta))
.chain(fallback_oracles.into_iter().map(to_account_meta))
.collect(); .collect();
// Since health is likely to be computed separately for both accounts, we don't use the // 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_token_count,
account1.active_perp_positions().count(), account1.active_perp_positions().count(),
account1.active_serum3_orders().count(), account1.active_serum3_orders().count(),
fallbacks_len,
) + self.compute_estimates.health_for_counts( ) + self.compute_estimates.health_for_counts(
account2_token_count, account2_token_count,
account2.active_perp_positions().count(), account2.active_perp_positions().count(),
account2.active_serum3_orders().count(), account2.active_serum3_orders().count(),
fallbacks_len,
); );
Ok((accounts, cu)) Ok((accounts, cu))
@ -554,6 +635,61 @@ impl MangoGroupContext {
let new_perp_markets = fetch_perp_markets(rpc, mango_v4::id(), self.group).await?; let new_perp_markets = fetch_perp_markets(rpc, mango_v4::id(), self.group).await?;
Ok(new_perp_markets.len() > self.perp_markets.len()) 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 { 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 .value
.ok_or(ClientError::AccountNotFound) .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)] #[derive(Builder)]
pub struct ErrorTracking<Key, ErrorType> { pub struct ErrorTracking<Key, ErrorType> {
#[builder(setter(custom))] #[builder(default, setter(custom))]
errors_by_type: HashMap<ErrorType, ErrorTypeState<Key>>, errors_by_type: HashMap<ErrorType, ErrorTypeState<Key>>,
/// number of errors of a type after which had_too_many_errors returns true /// 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 anchor_lang::{AccountDeserialize, Discriminator};
use mango_v4::state::{Bank, MangoAccount, MangoAccountValue, MintInfo, PerpMarket, Serum3Market}; use mango_v4::state::{Bank, MangoAccount, MangoAccountValue, MintInfo, PerpMarket, Serum3Market};
use solana_account_decoder::UiAccountEncoding; use solana_account_decoder::UiAccountEncoding;
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync; use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}; use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig};
use solana_client::rpc_filter::{Memcmp, RpcFilterType}; use solana_client::rpc_filter::{Memcmp, RpcFilterType};
use solana_sdk::account::AccountSharedData;
use solana_sdk::pubkey::Pubkey; use solana_sdk::pubkey::Pubkey;
pub async fn fetch_mango_accounts( pub async fn fetch_mango_accounts(
@ -129,3 +129,22 @@ pub async fn fetch_perp_markets(
) )
.await .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 anyhow::Context;
use futures::{stream, StreamExt, TryStreamExt}; use futures::{stream, StreamExt, TryStreamExt};
use mango_v4::accounts_zerocopy::KeyedAccountSharedData; use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
use mango_v4::health::{FixedOrderAccountRetriever, HealthCache}; use mango_v4::health::{FixedOrderAccountRetriever, HealthCache};
use mango_v4::state::MangoAccountValue; use mango_v4::state::{pyth_mainnet_sol_oracle, pyth_mainnet_usdc_oracle, MangoAccountValue};
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
pub async fn new( pub async fn new(
context: &MangoGroupContext, context: &MangoGroupContext,
account_fetcher: &impl AccountFetcher, fallback_config: &FallbackOracleConfig,
account_fetcher: &dyn AccountFetcher,
account: &MangoAccountValue, account: &MangoAccountValue,
) -> anyhow::Result<HealthCache> { ) -> anyhow::Result<HealthCache> {
let active_token_len = account.active_token_positions().count(); let active_token_len = account.active_token_positions().count();
let active_perp_len = account.active_perp_positions().count(); let active_perp_len = account.active_perp_positions().count();
let (metas, _health_cu) = let fallback_keys = context
context.derive_health_check_remaining_account_metas(account, vec![], vec![], vec![])?; .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()) let accounts: anyhow::Result<Vec<KeyedAccountSharedData>> = stream::iter(metas.iter())
.then(|meta| async { .then(|meta| async {
Ok(KeyedAccountSharedData::new( Ok(KeyedAccountSharedData::new(
@ -34,9 +44,13 @@ pub async fn new(
begin_perp: active_token_len * 2, begin_perp: active_token_len * 2,
begin_serum3: active_token_len * 2 + active_perp_len * 2, begin_serum3: active_token_len * 2 + active_perp_len * 2,
staleness_slot: None, staleness_slot: None,
begin_fallback_oracles: metas.len(), // TODO: add support for fallback oracle accounts begin_fallback_oracles: metas.len(),
usd_oracle_index: None, usdc_oracle_index: metas
sol_oracle_index: None, .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(); let now_ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
mango_v4::health::new_health_cache(&account.borrow(), &retriever, now_ts) 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_token_len = account.active_token_positions().count();
let active_perp_len = account.active_perp_positions().count(); let active_perp_len = account.active_perp_positions().count();
let (metas, _health_cu) = let (metas, _health_cu) = context.derive_health_check_remaining_account_metas(
context.derive_health_check_remaining_account_metas(account, vec![], vec![], vec![])?; account,
vec![],
vec![],
vec![],
HashMap::new(),
)?;
let accounts = metas let accounts = metas
.iter() .iter()
.map(|meta| { .map(|meta| {
@ -70,8 +89,8 @@ pub fn new_sync(
begin_perp: active_token_len * 2, begin_perp: active_token_len * 2,
begin_serum3: active_token_len * 2 + active_perp_len * 2, begin_serum3: active_token_len * 2 + active_perp_len * 2,
staleness_slot: None, staleness_slot: None,
begin_fallback_oracles: metas.len(), // TODO: add support for fallback oracle accounts begin_fallback_oracles: metas.len(),
usd_oracle_index: None, usdc_oracle_index: None,
sol_oracle_index: None, sol_oracle_index: None,
}; };
let now_ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); let now_ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();

View File

@ -1,23 +1,21 @@
pub mod v4;
pub mod v6; pub mod v6;
use anchor_lang::prelude::*; use anchor_lang::prelude::*;
use std::str::FromStr; use std::str::FromStr;
use crate::{JupiterSwapMode, MangoClient, TransactionBuilder}; use crate::{MangoClient, TransactionBuilder};
use fixed::types::I80F48; use fixed::types::I80F48;
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq)]
pub enum Version { pub enum Version {
Mock, Mock,
V4,
V6, V6,
} }
#[derive(Clone)] #[derive(Clone)]
#[allow(clippy::large_enum_variant)]
pub enum RawQuote { pub enum RawQuote {
Mock, Mock,
V4(v4::QueryRoute),
V6(v6::QuoteResponse), V6(v6::QuoteResponse),
} }
@ -32,21 +30,6 @@ pub struct Quote {
} }
impl 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> { pub fn try_from_v6(query: v6::QuoteResponse) -> anyhow::Result<Self> {
Ok(Quote { Ok(Quote {
input_mint: Pubkey::from_str(&query.input_mint)?, input_mint: Pubkey::from_str(&query.input_mint)?,
@ -65,7 +48,6 @@ impl Quote {
pub fn first_route_label(&self) -> String { pub fn first_route_label(&self) -> String {
let label_maybe = match &self.raw { let label_maybe = match &self.raw {
RawQuote::Mock => Some("mock".into()), RawQuote::Mock => Some("mock".into()),
RawQuote::V4(raw) => raw.market_infos.first().map(|v| v.label.clone()),
RawQuote::V6(raw) => raw RawQuote::V6(raw) => raw
.route_plan .route_plan
.first() .first()
@ -129,21 +111,6 @@ impl<'a> Jupiter<'a> {
) -> anyhow::Result<Quote> { ) -> anyhow::Result<Quote> {
Ok(match version { Ok(match version {
Version::Mock => self.quote_mock(input_mint, output_mint, amount).await?, 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( Version::V6 => Quote::try_from_v6(
self.mango_client self.mango_client
.jupiter_v6() .jupiter_v6()
@ -165,12 +132,6 @@ impl<'a> Jupiter<'a> {
) -> anyhow::Result<TransactionBuilder> { ) -> anyhow::Result<TransactionBuilder> {
match &quote.raw { match &quote.raw {
RawQuote::Mock => anyhow::bail!("can't prepare jupiter swap for the mock"), 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) => { RawQuote::V6(raw) => {
self.mango_client self.mango_client
.jupiter_v6() .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; let config = self.mango_client.client.config();
if !client.jupiter_token.is_empty() { if !config.jupiter_token.is_empty() {
query_args.push(("token", client.jupiter_token.clone())); query_args.push(("token", config.jupiter_token.clone()));
} }
let response = self let response = self
.mango_client .mango_client
.http_client .http_client
.get(format!("{}/quote", client.jupiter_v6_url)) .get(format!("{}/quote", config.jupiter_v6_url))
.query(&query_args) .query(&query_args)
.send() .send()
.await .await
@ -237,6 +237,7 @@ impl<'a> JupiterV6<'a> {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let owner = self.mango_client.owner(); let owner = self.mango_client.owner();
let account = &self.mango_client.mango_account().await?;
let token_ams = [source_token.mint, target_token.mint] let token_ams = [source_token.mint, target_token.mint]
.into_iter() .into_iter()
@ -259,6 +260,7 @@ impl<'a> JupiterV6<'a> {
let (health_ams, _health_cu) = self let (health_ams, _health_cu) = self
.mango_client .mango_client
.derive_health_check_remaining_account_metas( .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![source_token.token_index, target_token.token_index], vec![source_token.token_index, target_token.token_index],
vec![], vec![],
@ -267,15 +269,15 @@ impl<'a> JupiterV6<'a> {
.context("building health accounts")?; .context("building health accounts")?;
let mut query_args = vec![]; let mut query_args = vec![];
let client = &self.mango_client.client; let config = self.mango_client.client.config();
if !client.jupiter_token.is_empty() { if !config.jupiter_token.is_empty() {
query_args.push(("token", client.jupiter_token.clone())); query_args.push(("token", config.jupiter_token.clone()));
} }
let swap_response = self let swap_response = self
.mango_client .mango_client
.http_client .http_client
.post(format!("{}/swap-instructions", client.jupiter_v6_url)) .post(format!("{}/swap-instructions", config.jupiter_v6_url))
.query(&query_args) .query(&query_args)
.json(&SwapRequest { .json(&SwapRequest {
user_public_key: owner.to_string(), user_public_key: owner.to_string(),
@ -386,7 +388,12 @@ impl<'a> JupiterV6<'a> {
address_lookup_tables, address_lookup_tables,
payer, payer,
signers: vec![self.mango_client.owner.clone()], 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; pub mod chain_data;
mod chain_data_fetcher; mod chain_data_fetcher;
mod client; mod client;
pub mod confirm_transaction;
mod context; mod context;
pub mod error_tracking; pub mod error_tracking;
pub mod gpa; pub mod gpa;
pub mod health_cache; pub mod health_cache;
pub mod jupiter; pub mod jupiter;
pub mod perp_pnl; pub mod perp_pnl;
pub mod priority_fees;
pub mod priority_fees_cli;
pub mod snapshot_source; pub mod snapshot_source;
mod util; mod util;
pub mod websocket_source; pub mod websocket_source;

View File

@ -17,6 +17,7 @@ pub enum Direction {
/// Note: keep in sync with perp.ts:getSettlePnlCandidates /// Note: keep in sync with perp.ts:getSettlePnlCandidates
pub async fn fetch_top( pub async fn fetch_top(
context: &crate::context::MangoGroupContext, context: &crate::context::MangoGroupContext,
fallback_config: &FallbackOracleConfig,
account_fetcher: &impl AccountFetcher, account_fetcher: &impl AccountFetcher,
perp_market_index: PerpMarketIndex, perp_market_index: PerpMarketIndex,
direction: Direction, direction: Direction,
@ -91,9 +92,10 @@ pub async fn fetch_top(
} else { } else {
I80F48::ZERO I80F48::ZERO
}; };
let perp_max_settle = crate::health_cache::new(context, account_fetcher, &acc) let perp_max_settle =
.await? crate::health_cache::new(context, fallback_config, account_fetcher, &acc)
.perp_max_settle(perp_market.settle_token_index)?; .await?
.perp_max_settle(perp_market.settle_token_index)?;
let settleable_pnl = if perp_max_settle > 0 { let settleable_pnl = if perp_max_settle > 0 {
(*pnl).max(-perp_max_settle) (*pnl).max(-perp_max_settle)
} else { } 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::compute_budget::ComputeBudgetInstruction;
use solana_sdk::instruction::Instruction; 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 anchor_lang::prelude::{AccountMeta, Pubkey};
use anyhow::Context; use anyhow::Context;
use std::{thread, time};
/// Some Result<> types don't convert to anyhow::Result nicely. Force them through stringification. /// Some Result<> types don't convert to anyhow::Result nicely. Force them through stringification.
pub trait AnyhowWrap { pub trait AnyhowWrap {
@ -57,67 +48,6 @@ pub fn delay_interval(period: std::time::Duration) -> tokio::time::Interval {
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, /// Convenience function used in binaries to set up the fmt tracing_subscriber,
/// with cololring enabled only if logging to a terminal and with EnvFilter. /// with cololring enabled only if logging to a terminal and with EnvFilter.
pub fn tracing_subscriber_init() { pub fn tracing_subscriber_init() {

View File

@ -1,5 +1,9 @@
{ {
<<<<<<< HEAD
"version": "0.22.0", "version": "0.22.0",
=======
"version": "0.23.0",
>>>>>>> main
"name": "mango_v4", "name": "mango_v4",
"instructions": [ "instructions": [
{ {
@ -277,6 +281,12 @@
"type": { "type": {
"option": "u16" "option": "u16"
} }
},
{
"name": "collateralFeeIntervalOpt",
"type": {
"option": "u64"
}
} }
] ]
}, },
@ -631,6 +641,17 @@
{ {
"name": "platformLiquidationFee", "name": "platformLiquidationFee",
"type": "f32" "type": "f32"
<<<<<<< HEAD
=======
},
{
"name": "disableAssetLiquidation",
"type": "bool"
},
{
"name": "collateralFeePerDay",
"type": "f32"
>>>>>>> main
} }
] ]
}, },
@ -1041,6 +1062,27 @@
"type": { "type": {
"option": "f32" "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", "name": "perpCreateMarket",
"docs": [ "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", "name": "altSet",
"accounts": [ "accounts": [
@ -7373,12 +7491,24 @@
"name": "forceClose", "name": "forceClose",
"type": "u8" "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", "name": "padding",
"type": { "type": {
"array": [ "array": [
"u8", "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", "name": "reserved",
"type": { "type": {
"array": [ "array": [
"u8", "u8",
<<<<<<< HEAD
1920 1920
=======
1900
>>>>>>> main
] ]
} }
} }
@ -7646,12 +7801,28 @@
], ],
"type": "u16" "type": "u16"
}, },
{
"name": "padding2",
"type": {
"array": [
"u8",
4
]
}
},
{
"name": "collateralFeeInterval",
"docs": [
"Intervals in which collateral fee is applied"
],
"type": "u64"
},
{ {
"name": "reserved", "name": "reserved",
"type": { "type": {
"array": [ "array": [
"u8", "u8",
1812 1800
] ]
} }
} }
@ -7773,12 +7944,27 @@
], ],
"type": "u64" "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", "name": "reserved",
"type": { "type": {
"array": [ "array": [
"u8", "u8",
200 152
] ]
} }
}, },
@ -9548,12 +9734,16 @@
"name": "temporaryDelegateExpiry", "name": "temporaryDelegateExpiry",
"type": "u64" "type": "u64"
}, },
{
"name": "lastCollateralFeeCharge",
"type": "u64"
},
{ {
"name": "reserved", "name": "reserved",
"type": { "type": {
"array": [ "array": [
"u8", "u8",
160 152
] ]
} }
} }
@ -10474,6 +10664,9 @@
}, },
{ {
"name": "Swap" "name": "Swap"
},
{
"name": "SwapWithoutFee"
} }
] ]
} }
@ -10829,6 +11022,9 @@
}, },
{ {
"name": "Serum3PlaceOrderV2" "name": "Serum3PlaceOrderV2"
},
{
"name": "TokenForceWithdraw"
} }
] ]
} }
@ -13746,6 +13942,76 @@
"index": false "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": [ "errors": [
@ -14093,6 +14359,11 @@
"code": 6068, "code": 6068,
"name": "MissingFeedForCLMMOracle", "name": "MissingFeedForCLMMOracle",
"msg": "Pyth USDC/USD or SOL/USD feed not found (required by CLMM oracle)" "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] [package]
name = "mango-v4" name = "mango-v4"
version = "0.22.0" version = "0.23.0"
description = "Created with Anchor" description = "Created with Anchor"
edition = "2021" edition = "2021"
@ -32,7 +32,11 @@ borsh = { version = "0.10.3", features = ["const-generics"] }
bytemuck = { version = "^1.7.2", features = ["min_const_generics"] } bytemuck = { version = "^1.7.2", features = ["min_const_generics"] }
default-env = "0.1.1" default-env = "0.1.1"
derivative = "2.2.0" 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" num_enum = "0.5.1"
pyth-sdk-solana = { workspace = true } pyth-sdk-solana = { workspace = true }
serde = "^1.0" serde = "^1.0"
@ -48,7 +52,9 @@ switchboard-program = "0.2"
switchboard-v2 = { package = "switchboard-solana", version = "0.28" } 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] [dev-dependencies]
@ -56,7 +62,9 @@ solana-sdk = { workspace = true, default-features = false }
solana-program-test = { workspace = true } solana-program-test = { workspace = true }
solana-logger = { workspace = true } solana-logger = { workspace = true }
spl-token = { version = "^3.0.0", features = ["no-entrypoint"] } 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" bincode = "^1.3.1"
log = "0.4.14" log = "0.4.14"
env_logger = "0.9.0" env_logger = "0.9.0"

View File

@ -92,6 +92,12 @@ pub struct FlashLoanEnd<'info> {
#[derive(PartialEq, Copy, Clone, Debug, AnchorSerialize, AnchorDeserialize)] #[derive(PartialEq, Copy, Clone, Debug, AnchorSerialize, AnchorDeserialize)]
#[repr(u8)] #[repr(u8)]
pub enum FlashLoanType { pub enum FlashLoanType {
/// An arbitrary flash loan
Unknown, 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, 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_create::*;
pub use stub_oracle_set::*; pub use stub_oracle_set::*;
pub use token_add_bank::*; pub use token_add_bank::*;
pub use token_charge_collateral_fees::*;
pub use token_conditional_swap_cancel::*; pub use token_conditional_swap_cancel::*;
pub use token_conditional_swap_create::*; pub use token_conditional_swap_create::*;
pub use token_conditional_swap_start::*; pub use token_conditional_swap_start::*;
@ -67,6 +68,7 @@ pub use token_deposit::*;
pub use token_deregister::*; pub use token_deregister::*;
pub use token_edit::*; pub use token_edit::*;
pub use token_force_close_borrows_with_token::*; pub use token_force_close_borrows_with_token::*;
pub use token_force_withdraw::*;
pub use token_liq_bankruptcy::*; pub use token_liq_bankruptcy::*;
pub use token_liq_with_token::*; pub use token_liq_with_token::*;
pub use token_register::*; pub use token_register::*;
@ -135,6 +137,7 @@ mod stub_oracle_close;
mod stub_oracle_create; mod stub_oracle_create;
mod stub_oracle_set; mod stub_oracle_set;
mod token_add_bank; mod token_add_bank;
mod token_charge_collateral_fees;
mod token_conditional_swap_cancel; mod token_conditional_swap_cancel;
mod token_conditional_swap_create; mod token_conditional_swap_create;
mod token_conditional_swap_start; mod token_conditional_swap_start;
@ -143,6 +146,7 @@ mod token_deposit;
mod token_deregister; mod token_deregister;
mod token_edit; mod token_edit;
mod token_force_close_borrows_with_token; mod token_force_close_borrows_with_token;
mod token_force_withdraw;
mod token_liq_bankruptcy; mod token_liq_bankruptcy;
mod token_liq_with_token; mod token_liq_with_token;
mod token_register; 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, InvalidFeedForCLMMOracle,
#[msg("Pyth USDC/USD or SOL/USD feed not found (required by CLMM oracle)")] #[msg("Pyth USDC/USD or SOL/USD feed not found (required by CLMM oracle)")]
MissingFeedForCLMMOracle, MissingFeedForCLMMOracle,
#[msg("the asset does not allow liquidation")]
TokenAssetLiquidationDisabled,
} }
impl MangoError { impl MangoError {

View File

@ -59,7 +59,7 @@ pub struct FixedOrderAccountRetriever<T: KeyedAccountReader> {
pub begin_serum3: usize, pub begin_serum3: usize,
pub staleness_slot: Option<u64>, pub staleness_slot: Option<u64>,
pub begin_fallback_oracles: usize, pub begin_fallback_oracles: usize,
pub usd_oracle_index: Option<usize>, pub usdc_oracle_index: Option<usize>,
pub sol_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, ais.len(), expected_ais,
active_token_len, active_token_len, active_perp_len, active_perp_len, active_serum3_len 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() .iter()
.position(|o| o.key == &pyth_mainnet_usdc_oracle::ID); .position(|o| o.key == &pyth_mainnet_usdc_oracle::ID);
let sol_oracle_index = ais[..] 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, begin_serum3: active_token_len * 2 + active_perp_len * 2,
staleness_slot: Some(Clock::get()?.slot), staleness_slot: Some(Clock::get()?.slot),
begin_fallback_oracles: expected_ais, begin_fallback_oracles: expected_ais,
usd_oracle_index, usdc_oracle_index,
sol_oracle_index, sol_oracle_index,
}) })
} }
@ -139,7 +139,7 @@ impl<T: KeyedAccountReader> FixedOrderAccountRetriever<T> {
OracleAccountInfos { OracleAccountInfos {
oracle, oracle,
fallback_opt, 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]), sol_opt: self.sol_oracle_index.map(|i| &self.ais[i]),
} }
} }
@ -324,7 +324,7 @@ impl<'a, 'info> ScannedBanksAndOracles<'a, 'info> {
OracleAccountInfos { OracleAccountInfos {
oracle, oracle,
fallback_opt, 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]), 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. /// Includes TokenPosition and free Serum3OpenOrders balances.
/// Does not include perp upnl or Serum3 reserved amounts. /// Does not include perp upnl or Serum3 reserved amounts.
pub balance_spot: I80F48, pub balance_spot: I80F48,
pub allow_asset_liquidation: bool,
} }
/// Temporary value used during health computations /// 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 /// 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 { pub fn has_liq_spot_assets(&self) -> bool {
let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd); let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd);
self.token_infos self.token_infos
@ -914,11 +917,11 @@ impl HealthCache {
.zip(health_token_balances.iter()) .zip(health_token_balances.iter())
.any(|(ti, b)| { .any(|(ti, b)| {
// need 1 native token to use token_liq_with_token // 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 { pub fn has_liq_spot_borrows(&self) -> bool {
let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd); let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd);
self.token_infos self.token_infos
@ -932,7 +935,9 @@ impl HealthCache {
let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd); let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd);
let all_iter = || self.token_infos.iter().zip(health_token_balances.iter()); 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 < 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 { 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), init_scaled_liab_weight: bank.scaled_init_liab_weight(liab_price),
prices, prices,
balance_spot: native, 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), init_scaled_liab_weight: I80F48::from_num(1.0 + x),
prices: Prices::new_single_price(I80F48::from_num(price)), prices: Prices::new_single_price(I80F48::from_num(price)),
balance_spot: I80F48::ZERO, balance_spot: I80F48::ZERO,
allow_asset_liquidation: true,
} }
} }
@ -1461,27 +1462,49 @@ mod tests {
I80F48::ZERO I80F48::ZERO
); );
let find_max_borrow = |c: &HealthCache, ratio: f64| { let now_ts = system_epoch_secs();
let max_borrow = c
.max_borrow_for_health_ratio(&account, bank0_data, I80F48::from_num(ratio)) let cache_after_borrow = |account: &MangoAccountValue,
.unwrap(); c: &HealthCache,
// compute the health ratio we'd get when executing the trade bank: &Bank,
let actual_ratio = { amount: I80F48|
let mut c = c.clone(); -> Result<HealthCache> {
c.token_infos[0].balance_spot -= max_borrow; let mut position = account.token_position(bank.token_index)?.clone();
c.health_ratio(HealthType::Init).to_num::<f64>()
}; let mut bank = bank.clone();
// the ratio for borrowing one native token extra bank.withdraw_with_fee(&mut position, amount, now_ts)?;
let plus_ratio = { bank.check_net_borrows(c.token_info(bank.token_index)?.prices.oracle)?;
let mut c = c.clone();
c.token_infos[0].balance_spot -= max_borrow + I80F48::ONE; let mut resulting_cache = c.clone();
c.health_ratio(HealthType::Init).to_num::<f64>() resulting_cache.adjust_token_balance(&bank, -amount)?;
};
(max_borrow, actual_ratio, plus_ratio) 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 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!( println!(
"checking target ratio {ratio}: initial ratio: {initial_ratio}, actual ratio: {actual_ratio}, plus ratio: {plus_ratio}, borrow: {max_borrow}", "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(); let mut health_cache = health_cache.clone();
health_cache.token_infos[0].balance_spot = I80F48::from_num(100.0); 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(); 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 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(&account, &health_cache, 100.0, bank0_data);
check_max_borrow(&health_cache, 50.0); check_max_borrow(&account, &health_cache, 50.0, bank0_data);
check_max_borrow(&health_cache, 0.0); check_max_borrow(&account, &health_cache, 0.0, bank0_data);
} }
{ {
let mut health_cache = health_cache.clone(); let mut health_cache = health_cache.clone();
health_cache.token_infos[0].balance_spot = I80F48::from_num(50.0); health_cache.token_infos[0].balance_spot = I80F48::from_num(50.0);
health_cache.token_infos[1].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(&account, &health_cache, 100.0, bank0_data);
check_max_borrow(&health_cache, 50.0); check_max_borrow(&account, &health_cache, 50.0, bank0_data);
check_max_borrow(&health_cache, 0.0); check_max_borrow(&account, &health_cache, 0.0, bank0_data);
} }
{ {
let mut health_cache = health_cache.clone(); let mut health_cache = health_cache.clone();
health_cache.token_infos[0].balance_spot = I80F48::from_num(-50.0); health_cache.token_infos[0].balance_spot = I80F48::from_num(-50.0);
health_cache.token_infos[1].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(&account, &health_cache, 100.0, bank0_data);
check_max_borrow(&health_cache, 50.0); check_max_borrow(&account, &health_cache, 50.0, bank0_data);
check_max_borrow(&health_cache, 0.0); 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 // 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)?; 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. // into the vault. Compute the total change in the vault balance.
let mut change = -I80F48::from(bank.flash_loan_approved_amount); let mut change = -I80F48::from(bank.flash_loan_approved_amount);
if token_account.amount > bank.flash_loan_token_account_initial { 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 { match flash_loan_type {
FlashLoanType::Unknown => {} FlashLoanType::Unknown => {}
FlashLoanType::Swap => { FlashLoanType::Swap | FlashLoanType::SwapWithoutFee => {
require_msg!( require_msg!(
changes.len() == 2, 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>, mngo_token_index_opt: Option<TokenIndex>,
buyback_fees_expiry_interval_opt: Option<u64>, buyback_fees_expiry_interval_opt: Option<u64>,
allowed_fast_listings_per_interval_opt: Option<u16>, allowed_fast_listings_per_interval_opt: Option<u16>,
collateral_fee_interval_opt: Option<u64>,
) -> Result<()> { ) -> Result<()> {
let mut group = ctx.accounts.group.load_mut()?; 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; 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(()) Ok(())
} }

View File

@ -95,6 +95,7 @@ pub fn ix_gate_set(ctx: Context<IxGateSet>, ix_gate: u128) -> Result<()> {
IxGate::TokenConditionalSwapCreateLinearAuction, IxGate::TokenConditionalSwapCreateLinearAuction,
); );
log_if_changed(&group, ix_gate, IxGate::Serum3PlaceOrderV2); log_if_changed(&group, ix_gate, IxGate::Serum3PlaceOrderV2);
log_if_changed(&group, ix_gate, IxGate::TokenForceWithdraw);
group.ix_gate = ix_gate; 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_create::*;
pub use stub_oracle_set::*; pub use stub_oracle_set::*;
pub use token_add_bank::*; pub use token_add_bank::*;
pub use token_charge_collateral_fees::*;
pub use token_conditional_swap_cancel::*; pub use token_conditional_swap_cancel::*;
pub use token_conditional_swap_create::*; pub use token_conditional_swap_create::*;
pub use token_conditional_swap_start::*; pub use token_conditional_swap_start::*;
@ -58,6 +59,7 @@ pub use token_deposit::*;
pub use token_deregister::*; pub use token_deregister::*;
pub use token_edit::*; pub use token_edit::*;
pub use token_force_close_borrows_with_token::*; pub use token_force_close_borrows_with_token::*;
pub use token_force_withdraw::*;
pub use token_liq_bankruptcy::*; pub use token_liq_bankruptcy::*;
pub use token_liq_with_token::*; pub use token_liq_with_token::*;
pub use token_register::*; pub use token_register::*;
@ -117,6 +119,7 @@ mod stub_oracle_close;
mod stub_oracle_create; mod stub_oracle_create;
mod stub_oracle_set; mod stub_oracle_set;
mod token_add_bank; mod token_add_bank;
mod token_charge_collateral_fees;
mod token_conditional_swap_cancel; mod token_conditional_swap_cancel;
mod token_conditional_swap_create; mod token_conditional_swap_create;
mod token_conditional_swap_start; mod token_conditional_swap_start;
@ -125,6 +128,7 @@ mod token_deposit;
mod token_deregister; mod token_deregister;
mod token_edit; mod token_edit;
mod token_force_close_borrows_with_token; mod token_force_close_borrows_with_token;
mod token_force_withdraw;
mod token_liq_bankruptcy; mod token_liq_bankruptcy;
mod token_liq_with_token; mod token_liq_with_token;
mod token_register; 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>, deposit_limit_opt: Option<u64>,
zero_util_rate: Option<f32>, zero_util_rate: Option<f32>,
platform_liquidation_fee: 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<()> { ) -> Result<()> {
let group = ctx.accounts.group.load()?; let group = ctx.accounts.group.load()?;
@ -482,6 +485,43 @@ pub fn token_edit(
platform_liquidation_fee platform_liquidation_fee
); );
bank.platform_liquidation_fee = I80F48::from_num(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; 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)?; liqee.token_position_and_raw_index(asset_token_index)?;
let liqee_asset_native = liqee_asset_position.native(asset_bank); let liqee_asset_native = liqee_asset_position.native(asset_bank);
require_gt!(liqee_asset_native, 0); require_gt!(liqee_asset_native, 0);
require!(
asset_bank.allows_asset_liquidation(),
MangoError::TokenAssetLiquidationDisabled
);
let (liqee_liab_position, liqee_liab_raw_index) = let (liqee_liab_position, liqee_liab_raw_index) =
liqee.token_position_and_raw_index(liab_token_index)?; liqee.token_position_and_raw_index(liab_token_index)?;

View File

@ -44,6 +44,8 @@ pub fn token_register(
deposit_limit: u64, deposit_limit: u64,
zero_util_rate: f32, zero_util_rate: f32,
platform_liquidation_fee: f32, platform_liquidation_fee: f32,
disable_asset_liquidation: bool,
collateral_fee_per_day: f32,
) -> Result<()> { ) -> Result<()> {
// Require token 0 to be in the insurance token // Require token 0 to be in the insurance token
if token_index == INSURANCE_TOKEN_INDEX { if token_index == INSURANCE_TOKEN_INDEX {
@ -109,6 +111,8 @@ pub fn token_register(
deposit_weight_scale_start_quote, deposit_weight_scale_start_quote,
reduce_only, reduce_only,
force_close: 0, force_close: 0,
disable_asset_liquidation: u8::from(disable_asset_liquidation),
force_withdraw: 0,
padding: Default::default(), padding: Default::default(),
fees_withdrawn: 0, fees_withdrawn: 0,
token_conditional_swap_taker_fee_rate, token_conditional_swap_taker_fee_rate,
@ -127,7 +131,9 @@ pub fn token_register(
zero_util_rate: I80F48::from_num(zero_util_rate), zero_util_rate: I80F48::from_num(zero_util_rate),
platform_liquidation_fee: I80F48::from_num(platform_liquidation_fee), platform_liquidation_fee: I80F48::from_num(platform_liquidation_fee),
collected_liquidation_fees: I80F48::ZERO, 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())?; 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 deposit_weight_scale_start_quote: 5_000_000_000.0, // $5k
reduce_only: 2, // deposit-only reduce_only: 2, // deposit-only
force_close: 0, force_close: 0,
disable_asset_liquidation: 1,
force_withdraw: 0,
padding: Default::default(), padding: Default::default(),
fees_withdrawn: 0, fees_withdrawn: 0,
token_conditional_swap_taker_fee_rate: 0.0, token_conditional_swap_taker_fee_rate: 0.0,
@ -107,7 +109,9 @@ pub fn token_register_trustless(
deposit_limit: 0, deposit_limit: 0,
zero_util_rate: I80F48::ZERO, zero_util_rate: I80F48::ZERO,
collected_liquidation_fees: 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())?; let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
if let Ok(oracle_price) = bank.oracle_price(&OracleAccountInfos::from_reader(oracle_ref), None) 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>, mngo_token_index_opt: Option<TokenIndex>,
buyback_fees_expiry_interval_opt: Option<u64>, buyback_fees_expiry_interval_opt: Option<u64>,
allowed_fast_listings_per_interval_opt: Option<u16>, allowed_fast_listings_per_interval_opt: Option<u16>,
collateral_fee_interval_opt: Option<u64>,
) -> Result<()> { ) -> Result<()> {
#[cfg(feature = "enable-gpl")] #[cfg(feature = "enable-gpl")]
instructions::group_edit( instructions::group_edit(
@ -100,6 +101,7 @@ pub mod mango_v4 {
mngo_token_index_opt, mngo_token_index_opt,
buyback_fees_expiry_interval_opt, buyback_fees_expiry_interval_opt,
allowed_fast_listings_per_interval_opt, allowed_fast_listings_per_interval_opt,
collateral_fee_interval_opt,
)?; )?;
Ok(()) Ok(())
} }
@ -157,6 +159,8 @@ pub mod mango_v4 {
deposit_limit: u64, deposit_limit: u64,
zero_util_rate: f32, zero_util_rate: f32,
platform_liquidation_fee: f32, platform_liquidation_fee: f32,
disable_asset_liquidation: bool,
collateral_fee_per_day: f32,
) -> Result<()> { ) -> Result<()> {
#[cfg(feature = "enable-gpl")] #[cfg(feature = "enable-gpl")]
instructions::token_register( instructions::token_register(
@ -190,6 +194,8 @@ pub mod mango_v4 {
deposit_limit, deposit_limit,
zero_util_rate, zero_util_rate,
platform_liquidation_fee, platform_liquidation_fee,
disable_asset_liquidation,
collateral_fee_per_day,
)?; )?;
Ok(()) Ok(())
} }
@ -245,6 +251,9 @@ pub mod mango_v4 {
deposit_limit_opt: Option<u64>, deposit_limit_opt: Option<u64>,
zero_util_rate_opt: Option<f32>, zero_util_rate_opt: Option<f32>,
platform_liquidation_fee_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<()> { ) -> Result<()> {
#[cfg(feature = "enable-gpl")] #[cfg(feature = "enable-gpl")]
instructions::token_edit( instructions::token_edit(
@ -287,6 +296,9 @@ pub mod mango_v4 {
deposit_limit_opt, deposit_limit_opt,
zero_util_rate_opt, zero_util_rate_opt,
platform_liquidation_fee_opt, platform_liquidation_fee_opt,
disable_asset_liquidation_opt,
collateral_fee_per_day_opt,
force_withdraw_opt,
)?; )?;
Ok(()) Ok(())
} }
@ -807,6 +819,12 @@ pub mod mango_v4 {
Ok(()) Ok(())
} }
pub fn token_force_withdraw(ctx: Context<TokenForceWithdraw>) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::token_force_withdraw(ctx)?;
Ok(())
}
/// ///
/// Perps /// Perps
/// ///
@ -1605,6 +1623,12 @@ pub mod mango_v4 {
Ok(()) 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<()> { pub fn alt_set(ctx: Context<AltSet>, index: u8) -> Result<()> {
#[cfg(feature = "enable-gpl")] #[cfg(feature = "enable-gpl")]
instructions::alt_set(ctx, index)?; instructions::alt_set(ctx, index)?;

View File

@ -795,3 +795,23 @@ pub struct TokenConditionalSwapStartLog {
pub incentive_token_index: u16, pub incentive_token_index: u16,
pub incentive_amount: u64, 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 reduce_only: u8,
pub force_close: 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")] #[derivative(Debug = "ignore")]
pub padding: [u8; 6], pub padding: [u8; 4],
// Do separate bookkeping for how many tokens were withdrawn // Do separate bookkeping for how many tokens were withdrawn
// This ensures that collected_fees_native is strictly increasing for stats gathering purposes // 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. /// See also collected_fees_native and fees_withdrawn.
pub collected_liquidation_fees: I80F48, 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")] #[derivative(Debug = "ignore")]
pub reserved: [u8; 1920], pub reserved: [u8; 1900],
} }
const_assert_eq!( const_assert_eq!(
size_of::<Bank>(), size_of::<Bank>(),
@ -255,8 +269,9 @@ const_assert_eq!(
+ 16 * 3 + 16 * 3
+ 32 + 32
+ 8 + 8
+ 16 * 3 + 16 * 4
+ 1920 + 4
+ 1900
); );
const_assert_eq!(size_of::<Bank>(), 3064); const_assert_eq!(size_of::<Bank>(), 3064);
const_assert_eq!(size_of::<Bank>() % 8, 0); const_assert_eq!(size_of::<Bank>() % 8, 0);
@ -300,6 +315,7 @@ impl Bank {
indexed_borrows: I80F48::ZERO, indexed_borrows: I80F48::ZERO,
collected_fees_native: I80F48::ZERO, collected_fees_native: I80F48::ZERO,
collected_liquidation_fees: I80F48::ZERO, collected_liquidation_fees: I80F48::ZERO,
collected_collateral_fees: I80F48::ZERO,
fees_withdrawn: 0, fees_withdrawn: 0,
dust: I80F48::ZERO, dust: I80F48::ZERO,
flash_loan_approved_amount: 0, flash_loan_approved_amount: 0,
@ -346,7 +362,9 @@ impl Bank {
deposit_weight_scale_start_quote: existing_bank.deposit_weight_scale_start_quote, deposit_weight_scale_start_quote: existing_bank.deposit_weight_scale_start_quote,
reduce_only: existing_bank.reduce_only, reduce_only: existing_bank.reduce_only,
force_close: existing_bank.force_close, 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: existing_bank
.token_conditional_swap_taker_fee_rate, .token_conditional_swap_taker_fee_rate,
token_conditional_swap_maker_fee_rate: existing_bank token_conditional_swap_maker_fee_rate: existing_bank
@ -363,7 +381,8 @@ impl Bank {
deposit_limit: existing_bank.deposit_limit, deposit_limit: existing_bank.deposit_limit,
zero_util_rate: existing_bank.zero_util_rate, zero_util_rate: existing_bank.zero_util_rate,
platform_liquidation_fee: existing_bank.platform_liquidation_fee, 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.rate0, I80F48::ZERO);
require_gte!(self.rate1, I80F48::ZERO); require_gte!(self.rate1, I80F48::ZERO);
require_gte!(self.max_rate, 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_fee_rate, 0.0);
require_gte!(self.loan_origination_fee_rate, 0.0); require_gte!(self.loan_origination_fee_rate, 0.0);
require_gte!(self.maint_asset_weight, 0.0); require_gte!(self.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.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.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.liquidation_fee, 0.0);
require_gte!(self.min_vault_to_deposits_ratio, 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_gte!(self.net_borrow_limit_per_window_quote, -1);
require_gt!(self.borrow_weight_scale_start_quote, 0.0); require_gt!(self.borrow_weight_scale_start_quote, 0.0);
require_gt!(self.deposit_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.flash_loan_swap_fee_rate, 0.0);
require_gte!(self.interest_curve_scaling, 1.0); require_gte!(self.interest_curve_scaling, 1.0);
require_gte!(self.interest_target_utilization, 0.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_duration_inv, 0.0);
require_gte!(self.maint_weight_shift_asset_target, 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.maint_weight_shift_liab_target, 0.0);
require_gte!(self.zero_util_rate, I80F48::ZERO); require_gte!(self.zero_util_rate, I80F48::ZERO);
require_gte!(self.platform_liquidation_fee, 0.0); 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(()) Ok(())
} }
@ -418,6 +452,14 @@ impl Bank {
self.force_close == 1 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)] #[inline(always)]
pub fn native_borrows(&self) -> I80F48 { pub fn native_borrows(&self) -> I80F48 {
self.borrow_index * self.indexed_borrows 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( pub fn withdraw_loan_origination_fee(
&mut self, &mut self,
position: &mut TokenPosition, 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 /// rate based on the given parameters
#[inline(always)] #[inline(always)]
pub fn interest_rate_curve_calculator( pub fn interest_rate_curve_calculator(

View File

@ -98,11 +98,32 @@ pub struct Group {
/// Number of fast listings that are allowed per interval /// Number of fast listings that are allowed per interval
pub allowed_fast_listings_per_interval: u16, 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!( const_assert_eq!(
size_of::<Group>(), 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>(), 2736);
const_assert_eq!(size_of::<Group>() % 8, 0); const_assert_eq!(size_of::<Group>() % 8, 0);
@ -224,6 +245,7 @@ pub enum IxGate {
TokenConditionalSwapCreatePremiumAuction = 69, TokenConditionalSwapCreatePremiumAuction = 69,
TokenConditionalSwapCreateLinearAuction = 70, TokenConditionalSwapCreateLinearAuction = 70,
Serum3PlaceOrderV2 = 71, Serum3PlaceOrderV2 = 71,
TokenForceWithdraw = 72,
// NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction. // NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction.
} }

View File

@ -86,7 +86,7 @@ impl MangoAccountPdaSeeds {
// When not reading via idl, MangoAccount binary data is backwards compatible: when ignoring trailing bytes, // 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. // a v2 account can be read as a v1 account and a v3 account can be read as v1 or v2 etc.
#[account] #[account]
#[derive(Derivative)] #[derive(Derivative, PartialEq)]
#[derivative(Debug)] #[derivative(Debug)]
pub struct MangoAccount { pub struct MangoAccount {
// fixed // fixed
@ -151,8 +151,14 @@ pub struct MangoAccount {
/// Next id to use when adding a token condition swap /// Next id to use when adding a token condition swap
pub next_token_conditional_swap_id: u64, 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")] #[derivative(Debug = "ignore")]
pub reserved: [u8; 200], pub reserved: [u8; 152],
// dynamic // dynamic
pub header_version: u8, pub header_version: u8,
@ -203,7 +209,10 @@ impl MangoAccount {
buyback_fees_accrued_previous: 0, buyback_fees_accrued_previous: 0,
buyback_fees_expiry_timestamp: 0, buyback_fees_expiry_timestamp: 0,
next_token_conditional_swap_id: 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, header_version: DEFAULT_MANGO_ACCOUNT_VERSION,
padding3: Default::default(), padding3: Default::default(),
padding4: Default::default(), padding4: Default::default(),
@ -327,11 +336,12 @@ pub struct MangoAccountFixed {
pub next_token_conditional_swap_id: u64, pub next_token_conditional_swap_id: u64,
pub temporary_delegate: Pubkey, pub temporary_delegate: Pubkey,
pub temporary_delegate_expiry: u64, pub temporary_delegate_expiry: u64,
pub reserved: [u8; 160], pub last_collateral_fee_charge: u64,
pub reserved: [u8; 152],
} }
const_assert_eq!( const_assert_eq!(
size_of::<MangoAccountFixed>(), 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>(), 400);
const_assert_eq!(size_of::<MangoAccountFixed>() % 8, 0); const_assert_eq!(size_of::<MangoAccountFixed>() % 8, 0);
@ -737,6 +747,12 @@ impl<
self.dynamic.deref_or_borrow() 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 /// Returns
/// - the position /// - the position
/// - the raw index into the token positions list (for use with get_raw/deactivate) /// - 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( pub fn deactivate_perp_position(
&mut self, &mut self,
perp_market_index: PerpMarketIndex, perp_market_index: PerpMarketIndex,
@ -1196,6 +1213,19 @@ impl<
Ok(()) 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( pub fn add_perp_order(
&mut self, &mut self,
perp_market_index: PerpMarketIndex, perp_market_index: PerpMarketIndex,
@ -1852,6 +1882,7 @@ impl<'a, 'info: 'a> MangoAccountLoader<'a> for &'a AccountLoader<'info, MangoAcc
mod tests { mod tests {
use bytemuck::Zeroable; use bytemuck::Zeroable;
use itertools::Itertools; use itertools::Itertools;
use std::path::PathBuf;
use crate::state::PostOrderType; use crate::state::PostOrderType;
@ -2378,12 +2409,7 @@ mod tests {
); );
} }
let reserved_offset = account.header.reserved_bytes_offset(); assert!(account.dynamic_reserved_bytes().iter().all(|&v| v == 0));
assert!(
account.dynamic[reserved_offset..reserved_offset + DYNAMIC_RESERVED_BYTES]
.iter()
.all(|&v| v == 0)
);
Ok(()) Ok(())
} }
@ -2808,4 +2834,118 @@ mod tests {
Ok(()) 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; pub const FREE_ORDER_SLOT: PerpMarketIndex = PerpMarketIndex::MAX;
#[zero_copy] #[zero_copy]
#[derive(AnchorDeserialize, AnchorSerialize, Derivative)] #[derive(AnchorDeserialize, AnchorSerialize, Derivative, PartialEq)]
#[derivative(Debug)] #[derivative(Debug)]
pub struct TokenPosition { pub struct TokenPosition {
// TODO: Why did we have deposits and borrows as two different values // TODO: Why did we have deposits and borrows as two different values
@ -110,7 +110,7 @@ impl TokenPosition {
} }
#[zero_copy] #[zero_copy]
#[derive(AnchorSerialize, AnchorDeserialize, Derivative)] #[derive(AnchorSerialize, AnchorDeserialize, Derivative, PartialEq)]
#[derivative(Debug)] #[derivative(Debug)]
pub struct Serum3Orders { pub struct Serum3Orders {
pub open_orders: Pubkey, pub open_orders: Pubkey,
@ -203,7 +203,7 @@ impl Default for Serum3Orders {
} }
#[zero_copy] #[zero_copy]
#[derive(AnchorSerialize, AnchorDeserialize, Derivative)] #[derive(AnchorSerialize, AnchorDeserialize, Derivative, PartialEq)]
#[derivative(Debug)] #[derivative(Debug)]
pub struct PerpPosition { pub struct PerpPosition {
pub market_index: PerpMarketIndex, pub market_index: PerpMarketIndex,
@ -800,7 +800,7 @@ impl PerpPosition {
} }
#[zero_copy] #[zero_copy]
#[derive(AnchorSerialize, AnchorDeserialize, Derivative)] #[derive(AnchorSerialize, AnchorDeserialize, Derivative, PartialEq)]
#[derivative(Debug)] #[derivative(Debug)]
pub struct PerpOpenOrder { pub struct PerpOpenOrder {
pub side_and_tree: u8, // SideAndOrderTree -- enums aren't POD pub side_and_tree: u8, // SideAndOrderTree -- enums aren't POD

View File

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

View File

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

View File

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

View File

@ -462,6 +462,8 @@ async fn test_bank_maint_weight_shift() -> Result<(), TransportError> {
mint: mints[0].pubkey, mint: mints[0].pubkey,
fallback_oracle: Pubkey::default(), fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit { 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_start_opt: Some(start_time + 1000),
maint_weight_shift_end_opt: Some(start_time + 2000), maint_weight_shift_end_opt: Some(start_time + 2000),
maint_weight_shift_asset_target_opt: Some(0.5), 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(()) 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 success increase: {avg_success_increase}");
println!("average failure increase: {avg_failure_increase}"); println!("average failure increase: {avg_failure_increase}");
assert!(avg_success_increase < 2_050); assert!(avg_success_increase < 2_050);
assert!(avg_success_increase < 18_500); assert!(avg_failure_increase < 19_500);
Ok(()) 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; 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 // TEST: liquidate borrow2 against too little collateral2
// //

View File

@ -1077,6 +1077,8 @@ impl ClientInstruction for TokenRegisterInstruction {
deposit_limit: 0, deposit_limit: 0,
zero_util_rate: 0.0, zero_util_rate: 0.0,
platform_liquidation_fee: self.platform_liquidation_fee, platform_liquidation_fee: self.platform_liquidation_fee,
disable_asset_liquidation: false,
collateral_fee_per_day: 0.0,
}; };
let bank = Pubkey::find_program_address( let bank = Pubkey::find_program_address(
@ -1324,6 +1326,9 @@ pub fn token_edit_instruction_default() -> mango_v4::instruction::TokenEdit {
deposit_limit_opt: None, deposit_limit_opt: None,
zero_util_rate_opt: None, zero_util_rate_opt: None,
platform_liquidation_fee_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, mngo_token_index_opt: None,
buyback_fees_expiry_interval_opt: None, buyback_fees_expiry_interval_opt: None,
allowed_fast_listings_per_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 struct TokenLiqWithTokenInstruction {
pub liqee: Pubkey, pub liqee: Pubkey,
pub liqor: Pubkey, pub liqor: Pubkey,
@ -5036,3 +5094,48 @@ impl ClientInstruction for TokenConditionalSwapStartInstruction {
vec![self.liqor_owner] 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, group,
usdcDevnetMint, usdcDevnetMint,
usdcDevnetOracle.publicKey, usdcDevnetOracle.publicKey,
PublicKey.default,
0, // tokenIndex 0, // tokenIndex
'USDC', 'USDC',
{ {
@ -101,6 +102,7 @@ async function main(): Promise<void> {
group, group,
solDevnetMint, solDevnetMint,
solDevnetOracle, solDevnetOracle,
PublicKey.default,
4, // tokenIndex 4, // tokenIndex
'SOL', 'SOL',
{ {
@ -130,6 +132,7 @@ async function main(): Promise<void> {
group, group,
usdtDevnetMint, usdtDevnetMint,
usdcDevnetOracle.publicKey, usdcDevnetOracle.publicKey,
PublicKey.default,
5, // tokenIndex 5, // tokenIndex
'USDT', 'USDT',
{ {

View File

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

View File

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

View File

@ -60,31 +60,31 @@ async function buildClient(): Promise<MangoClient> {
); );
} }
async function groupEdit(): Promise<void> { // async function groupEdit(): Promise<void> {
const client = await buildClient(); // const client = await buildClient();
const group = await client.getGroup(new PublicKey(GROUP_PK)); // const group = await client.getGroup(new PublicKey(GROUP_PK));
const ix = await client.program.methods // const ix = await client.program.methods
.groupEdit( // .groupEdit(
null, // admin // null, // admin
null, // fastListingAdmin // null, // fastListingAdmin
null, // securityAdmin // null, // securityAdmin
null, // testing // null, // testing
null, // version // null, // version
null, // depositLimitQuote // null, // depositLimitQuote
null, // feesPayWithMngo // null, // feesPayWithMngo
null, // feesMngoBonusRate // null, // feesMngoBonusRate
null, // feesSwapMangoAccount // null, // feesSwapMangoAccount
6, // feesMngoTokenIndex // 6, // feesMngoTokenIndex
null, // feesExpiryInterval // null, // feesExpiryInterval
5, // allowedFastListingsPerInterval // 5, // allowedFastListingsPerInterval
) // )
.accounts({ // .accounts({
group: group.publicKey, // group: group.publicKey,
admin: group.admin, // admin: group.admin,
}) // })
.instruction(); // .instruction();
console.log(serializeInstructionToBase64(ix)); // console.log(serializeInstructionToBase64(ix));
} // }
// async function tokenRegister(): Promise<void> { // async function tokenRegister(): Promise<void> {
// const client = await buildClient(); // const client = await buildClient();
@ -468,7 +468,7 @@ async function idlSetAuthority(): Promise<void> {
async function main(): Promise<void> { async function main(): Promise<void> {
try { try {
await groupEdit(); // await groupEdit();
// await tokenRegister(); // await tokenRegister();
// await tokenEdit(); // await tokenEdit();
// await perpCreate(); // 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]], ['ETH', MINTS[1]],
['SOL', MINTS[2]], ['SOL', MINTS[2]],
['MNGO', MINTS[3]], ['MNGO', MINTS[3]],
['MSOL', MINTS[4]],
]); ]);
const STUB_PRICES = new Map([ const STUB_PRICES = new Map([
@ -36,13 +37,7 @@ const STUB_PRICES = new Map([
['ETH', 1200.0], // eth and usdc both have 6 decimals ['ETH', 1200.0], // eth and usdc both have 6 decimals
['SOL', 0.015], // sol has 9 decimals, equivalent to $15 per SOL ['SOL', 0.015], // sol has 9 decimals, equivalent to $15 per SOL
['MNGO', 0.02], ['MNGO', 0.02],
]); ['MSOL', 0.017],
// 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]],
]); ]);
const MIN_VAULT_TO_DEPOSITS_RATIO = 0.2; const MIN_VAULT_TO_DEPOSITS_RATIO = 0.2;
@ -90,11 +85,13 @@ async function main(): Promise<void> {
for (const [name, mint] of MAINNET_MINTS) { for (const [name, mint] of MAINNET_MINTS) {
console.log(`Creating stub oracle for ${name}...`); console.log(`Creating stub oracle for ${name}...`);
const mintPk = new PublicKey(mint); const mintPk = new PublicKey(mint);
try { if ((await client.getStubOracle(group, mintPk)).length == 0) {
const price = STUB_PRICES.get(name)!; try {
await client.stubOracleCreate(group, mintPk, price); const price = STUB_PRICES.get(name)!;
} catch (error) { await client.stubOracleCreate(group, mintPk, price);
console.log(error); } catch (error) {
console.log(error);
}
} }
const oracle = (await client.getStubOracle(group, mintPk))[0]; const oracle = (await client.getStubOracle(group, mintPk))[0];
console.log(`...created stub oracle ${oracle.publicKey}`); console.log(`...created stub oracle ${oracle.publicKey}`);
@ -114,22 +111,32 @@ async function main(): Promise<void> {
maxRate: 1.5, maxRate: 1.5,
}; };
const noFallbackOracle = PublicKey.default;
// register token 0 // register token 0
console.log(`Registering USDC...`); console.log(`Registering USDC...`);
const usdcMint = new PublicKey(MAINNET_MINTS.get('USDC')!); const usdcMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
const usdcOracle = oracles.get('USDC'); const usdcOracle = oracles.get('USDC');
try { try {
await client.tokenRegister(group, usdcMint, usdcOracle, 0, 'USDC', { await client.tokenRegister(
...DefaultTokenRegisterParams, group,
loanOriginationFeeRate: 0, usdcMint,
loanFeeRate: 0.0001, usdcOracle,
initAssetWeight: 1, noFallbackOracle,
maintAssetWeight: 1, 0,
initLiabWeight: 1, 'USDC',
maintLiabWeight: 1, {
liquidationFee: 0, ...DefaultTokenRegisterParams,
netBorrowLimitPerWindowQuote: NET_BORROWS_LIMIT_NATIVE, loanOriginationFeeRate: 0,
}); loanFeeRate: 0.0001,
initAssetWeight: 1,
maintAssetWeight: 1,
initLiabWeight: 1,
maintLiabWeight: 1,
liquidationFee: 0,
netBorrowLimitPerWindowQuote: NET_BORROWS_LIMIT_NATIVE,
},
);
await group.reloadAll(client); await group.reloadAll(client);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@ -140,17 +147,25 @@ async function main(): Promise<void> {
const ethMint = new PublicKey(MAINNET_MINTS.get('ETH')!); const ethMint = new PublicKey(MAINNET_MINTS.get('ETH')!);
const ethOracle = oracles.get('ETH'); const ethOracle = oracles.get('ETH');
try { try {
await client.tokenRegister(group, ethMint, ethOracle, 1, 'ETH', { await client.tokenRegister(
...DefaultTokenRegisterParams, group,
loanOriginationFeeRate: 0, ethMint,
loanFeeRate: 0.0001, ethOracle,
maintAssetWeight: 0.9, noFallbackOracle,
initAssetWeight: 0.8, 1,
maintLiabWeight: 1.1, 'ETH',
initLiabWeight: 1.2, {
liquidationFee: 0.05, ...DefaultTokenRegisterParams,
netBorrowLimitPerWindowQuote: NET_BORROWS_LIMIT_NATIVE, 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); await group.reloadAll(client);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@ -165,6 +180,7 @@ async function main(): Promise<void> {
group, group,
solMint, solMint,
solOracle, solOracle,
noFallbackOracle,
2, // tokenIndex 2, // tokenIndex
'SOL', 'SOL',
{ {
@ -184,27 +200,72 @@ async function main(): Promise<void> {
console.log(error); 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 // log tokens/banks
for (const bank of await group.banksMapByMint.values()) { for (const bank of await group.banksMapByMint.values()) {
console.log(`${bank.toString()}`); console.log(`${bank.toString()}`);
} }
console.log('Registering SOL/USDC serum market...'); let nextSerumMarketIndex = 0;
try { for (const [name, mint] of MAINNET_MINTS) {
await client.serum3RegisterMarket( if (name == 'USDC') {
group, continue;
new PublicKey(MAINNET_SERUM3_MARKETS.get('SOL/USDC')!), }
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('SOL')!)),
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('USDC')!)), console.log(`Registering ${name}/USDC serum market...`);
1, try {
'SOL/USDC', await client.serum3RegisterMarket(
0, group,
); new PublicKey(SERUM_MARKETS[nextSerumMarketIndex]),
} catch (error) { group.getFirstBankByMint(new PublicKey(mint)),
console.log(error); group.getFirstBankByMint(usdcMint),
nextSerumMarketIndex,
`${name}/USDC`,
0,
);
nextSerumMarketIndex += 1;
} catch (error) {
console.log(error);
}
} }
console.log('Registering MNGO-PERP market...'); console.log('Registering MNGO-PERP market...');
if (!group.banksMapByMint.get(usdcMint.toString())) {
console.log('stopping, no USDC bank');
return;
}
const mngoOracle = oracles.get('MNGO'); const mngoOracle = oracles.get('MNGO');
try { try {
await client.perpCreateMarket( await client.perpCreateMarket(
@ -237,7 +298,7 @@ async function main(): Promise<void> {
-1.0, -1.0,
2 * 60 * 60, 2 * 60 * 60,
0.025, 0.025,
0, 0.0,
); );
} catch (error) { } catch (error) {
console.log(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 // Script which creates three mints and two serum3 markets relating them
// //
const MINT_COUNT = 5;
const SERUM_MARKET_COUNT = 4;
function getVaultOwnerAndNonce( function getVaultOwnerAndNonce(
market: PublicKey, market: PublicKey,
programId: PublicKey, programId: PublicKey,
@ -56,7 +59,7 @@ async function main(): Promise<void> {
// Make mints // Make mints
const mints = await Promise.all( const mints = await Promise.all(
Array(4) Array(MINT_COUNT)
.fill(null) .fill(null)
.map(() => .map(() =>
splToken.createMint(connection, admin, admin.publicKey, null, 6), splToken.createMint(connection, admin, admin.publicKey, null, 6),
@ -78,11 +81,11 @@ async function main(): Promise<void> {
// Make serum markets // Make serum markets
const serumMarkets: PublicKey[] = []; const serumMarkets: PublicKey[] = [];
const quoteMint = mints[0]; 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 feeRateBps = 0.25; // don't think this does anything
const quoteDustThreshold = 100; const quoteDustThreshold = 100;
const baseLotSize = 1000; const baseLotSize = 1000;
const quoteLotSize = 1000; const quoteLotSize = 1; // makes prices be in 1000ths
const openbookProgramId = OPENBOOK_PROGRAM_ID.devnet; const openbookProgramId = OPENBOOK_PROGRAM_ID.devnet;
const market = Keypair.generate(); const market = Keypair.generate();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -55,6 +55,7 @@ export class Group {
fastListingIntervalStart: BN; fastListingIntervalStart: BN;
fastListingsInInterval: number; fastListingsInInterval: number;
allowedFastListingsPerInterval: number; allowedFastListingsPerInterval: number;
collateralFeeInterval: BN;
}, },
): Group { ): Group {
return new Group( return new Group(
@ -79,6 +80,7 @@ export class Group {
obj.fastListingIntervalStart, obj.fastListingIntervalStart,
obj.fastListingsInInterval, obj.fastListingsInInterval,
obj.allowedFastListingsPerInterval, obj.allowedFastListingsPerInterval,
obj.collateralFeeInterval,
[], // addressLookupTablesList [], // addressLookupTablesList
new Map(), // banksMapByName new Map(), // banksMapByName
new Map(), // banksMapByMint new Map(), // banksMapByMint
@ -118,6 +120,7 @@ export class Group {
public fastListingIntervalStart: BN, public fastListingIntervalStart: BN,
public fastListingsInInterval: number, public fastListingsInInterval: number,
public allowedFastListingsPerInterval: number, public allowedFastListingsPerInterval: number,
public collateralFeeInterval: BN,
public addressLookupTablesList: AddressLookupTableAccount[], public addressLookupTablesList: AddressLookupTableAccount[],
public banksMapByName: Map<string, Bank[]>, public banksMapByName: Map<string, Bank[]>,
public banksMapByMint: 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 * 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). * by the time elapsed since it was last applied (capped to max. 1hr).
* *
* @param bids * @param bids

View File

@ -7,8 +7,10 @@ import {
} from '@coral-xyz/anchor'; } from '@coral-xyz/anchor';
import { OpenOrders, decodeEventQueue } from '@project-serum/serum'; import { OpenOrders, decodeEventQueue } from '@project-serum/serum';
import { import {
createAccount,
createCloseAccountInstruction, createCloseAccountInstruction,
createInitializeAccount3Instruction, createInitializeAccount3Instruction,
unpackAccount,
} from '@solana/spl-token'; } from '@solana/spl-token';
import { import {
AccountInfo, AccountInfo,
@ -24,6 +26,7 @@ import {
RecentPrioritizationFees, RecentPrioritizationFees,
SYSVAR_INSTRUCTIONS_PUBKEY, SYSVAR_INSTRUCTIONS_PUBKEY,
SYSVAR_RENT_PUBKEY, SYSVAR_RENT_PUBKEY,
Signer,
SystemProgram, SystemProgram,
TransactionInstruction, TransactionInstruction,
} from '@solana/web3.js'; } from '@solana/web3.js';
@ -322,6 +325,7 @@ export class MangoClient {
feesMngoTokenIndex?: TokenIndex, feesMngoTokenIndex?: TokenIndex,
feesExpiryInterval?: BN, feesExpiryInterval?: BN,
allowedFastListingsPerInterval?: number, allowedFastListingsPerInterval?: number,
collateralFeeInterval?: BN,
): Promise<MangoSignatureStatus> { ): Promise<MangoSignatureStatus> {
const ix = await this.program.methods const ix = await this.program.methods
.groupEdit( .groupEdit(
@ -337,6 +341,7 @@ export class MangoClient {
feesMngoTokenIndex ?? null, feesMngoTokenIndex ?? null,
feesExpiryInterval ?? null, feesExpiryInterval ?? null,
allowedFastListingsPerInterval ?? null, allowedFastListingsPerInterval ?? null,
collateralFeeInterval ?? null,
) )
.accounts({ .accounts({
group: group.publicKey, group: group.publicKey,
@ -443,6 +448,7 @@ export class MangoClient {
group: Group, group: Group,
mintPk: PublicKey, mintPk: PublicKey,
oraclePk: PublicKey, oraclePk: PublicKey,
fallbackOraclePk: PublicKey,
tokenIndex: number, tokenIndex: number,
name: string, name: string,
params: TokenRegisterParams, params: TokenRegisterParams,
@ -478,12 +484,15 @@ export class MangoClient {
params.depositLimit, params.depositLimit,
params.zeroUtilRate, params.zeroUtilRate,
params.platformLiquidationFee, params.platformLiquidationFee,
params.disableAssetLiquidation,
params.collateralFeePerDay,
) )
.accounts({ .accounts({
group: group.publicKey, group: group.publicKey,
admin: (this.program.provider as AnchorProvider).wallet.publicKey, admin: (this.program.provider as AnchorProvider).wallet.publicKey,
mint: mintPk, mint: mintPk,
oracle: oraclePk, oracle: oraclePk,
fallbackOracle: fallbackOraclePk,
payer: (this.program.provider as AnchorProvider).wallet.publicKey, payer: (this.program.provider as AnchorProvider).wallet.publicKey,
rent: SYSVAR_RENT_PUBKEY, rent: SYSVAR_RENT_PUBKEY,
}) })
@ -560,14 +569,18 @@ export class MangoClient {
params.maintWeightShiftAssetTarget, params.maintWeightShiftAssetTarget,
params.maintWeightShiftLiabTarget, params.maintWeightShiftLiabTarget,
params.maintWeightShiftAbort ?? false, params.maintWeightShiftAbort ?? false,
params.setFallbackOracle ?? false, params.fallbackOracle !== null, // setFallbackOracle
params.depositLimit, params.depositLimit,
params.zeroUtilRate, params.zeroUtilRate,
params.platformLiquidationFee, params.platformLiquidationFee,
params.disableAssetLiquidation,
params.collateralFeePerDay,
params.forceWithdraw,
) )
.accounts({ .accounts({
group: group.publicKey, group: group.publicKey,
oracle: params.oracle ?? bank.oracle, oracle: params.oracle ?? bank.oracle,
fallbackOracle: params.fallbackOracle ?? bank.fallbackOracle,
admin: (this.program.provider as AnchorProvider).wallet.publicKey, admin: (this.program.provider as AnchorProvider).wallet.publicKey,
mintInfo: mintInfo.publicKey, mintInfo: mintInfo.publicKey,
}) })
@ -629,6 +642,94 @@ export class MangoClient {
return await this.sendAndConfirmTransactionForGroup(group, [ix]); 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( public async tokenDeregister(
group: Group, group: Group,
mintPk: PublicKey, mintPk: PublicKey,
@ -737,16 +838,20 @@ export class MangoClient {
mintPk: PublicKey, mintPk: PublicKey,
price: number, price: number,
): Promise<MangoSignatureStatus> { ): Promise<MangoSignatureStatus> {
const stubOracle = Keypair.generate();
const ix = await this.program.methods const ix = await this.program.methods
.stubOracleCreate({ val: I80F48.fromNumber(price).getData() }) .stubOracleCreate({ val: I80F48.fromNumber(price).getData() })
.accounts({ .accounts({
group: group.publicKey, group: group.publicKey,
admin: (this.program.provider as AnchorProvider).wallet.publicKey, admin: (this.program.provider as AnchorProvider).wallet.publicKey,
oracle: stubOracle.publicKey,
mint: mintPk, mint: mintPk,
payer: (this.program.provider as AnchorProvider).wallet.publicKey, payer: (this.program.provider as AnchorProvider).wallet.publicKey,
}) })
.instruction(); .instruction();
return await this.sendAndConfirmTransactionForGroup(group, [ix]); return await this.sendAndConfirmTransactionForGroup(group, [ix], {
additionalSigners: [stubOracle],
});
} }
public async stubOracleClose( public async stubOracleClose(

View File

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

View File

@ -1,5 +1,5 @@
export type MangoV4 = { export type MangoV4 = {
"version": "0.22.0", "version": "0.23.0",
"name": "mango_v4", "name": "mango_v4",
"instructions": [ "instructions": [
{ {
@ -277,6 +277,12 @@ export type MangoV4 = {
"type": { "type": {
"option": "u16" "option": "u16"
} }
},
{
"name": "collateralFeeIntervalOpt",
"type": {
"option": "u64"
}
} }
] ]
}, },
@ -631,6 +637,14 @@ export type MangoV4 = {
{ {
"name": "platformLiquidationFee", "name": "platformLiquidationFee",
"type": "f32" "type": "f32"
},
{
"name": "disableAssetLiquidation",
"type": "bool"
},
{
"name": "collateralFeePerDay",
"type": "f32"
} }
] ]
}, },
@ -1041,6 +1055,24 @@ export type MangoV4 = {
"type": { "type": {
"option": "f32" "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", "name": "perpCreateMarket",
"docs": [ "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", "name": "altSet",
"accounts": [ "accounts": [
@ -7373,12 +7481,24 @@ export type MangoV4 = {
"name": "forceClose", "name": "forceClose",
"type": "u8" "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", "name": "padding",
"type": { "type": {
"array": [ "array": [
"u8", "u8",
6 4
] ]
} }
}, },
@ -7513,12 +7633,30 @@ export type MangoV4 = {
"defined": "I80F48" "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", "name": "reserved",
"type": { "type": {
"array": [ "array": [
"u8", "u8",
1920 1900
] ]
} }
} }
@ -7646,12 +7784,28 @@ export type MangoV4 = {
], ],
"type": "u16" "type": "u16"
}, },
{
"name": "padding2",
"type": {
"array": [
"u8",
4
]
}
},
{
"name": "collateralFeeInterval",
"docs": [
"Intervals in which collateral fee is applied"
],
"type": "u64"
},
{ {
"name": "reserved", "name": "reserved",
"type": { "type": {
"array": [ "array": [
"u8", "u8",
1812 1800
] ]
} }
} }
@ -7773,12 +7927,27 @@ export type MangoV4 = {
], ],
"type": "u64" "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", "name": "reserved",
"type": { "type": {
"array": [ "array": [
"u8", "u8",
200 152
] ]
} }
}, },
@ -9548,12 +9717,16 @@ export type MangoV4 = {
"name": "temporaryDelegateExpiry", "name": "temporaryDelegateExpiry",
"type": "u64" "type": "u64"
}, },
{
"name": "lastCollateralFeeCharge",
"type": "u64"
},
{ {
"name": "reserved", "name": "reserved",
"type": { "type": {
"array": [ "array": [
"u8", "u8",
160 152
] ]
} }
} }
@ -10474,6 +10647,9 @@ export type MangoV4 = {
}, },
{ {
"name": "Swap" "name": "Swap"
},
{
"name": "SwapWithoutFee"
} }
] ]
} }
@ -10829,6 +11005,9 @@ export type MangoV4 = {
}, },
{ {
"name": "Serum3PlaceOrderV2" "name": "Serum3PlaceOrderV2"
},
{
"name": "TokenForceWithdraw"
} }
] ]
} }
@ -13746,6 +13925,76 @@ export type MangoV4 = {
"index": false "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": [ "errors": [
@ -14093,12 +14342,17 @@ export type MangoV4 = {
"code": 6068, "code": 6068,
"name": "MissingFeedForCLMMOracle", "name": "MissingFeedForCLMMOracle",
"msg": "Pyth USDC/USD or SOL/USD feed not found (required by CLMM oracle)" "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 = { export const IDL: MangoV4 = {
"version": "0.22.0", "version": "0.23.0",
"name": "mango_v4", "name": "mango_v4",
"instructions": [ "instructions": [
{ {
@ -14376,6 +14630,12 @@ export const IDL: MangoV4 = {
"type": { "type": {
"option": "u16" "option": "u16"
} }
},
{
"name": "collateralFeeIntervalOpt",
"type": {
"option": "u64"
}
} }
] ]
}, },
@ -14730,6 +14990,14 @@ export const IDL: MangoV4 = {
{ {
"name": "platformLiquidationFee", "name": "platformLiquidationFee",
"type": "f32" "type": "f32"
},
{
"name": "disableAssetLiquidation",
"type": "bool"
},
{
"name": "collateralFeePerDay",
"type": "f32"
} }
] ]
}, },
@ -15140,6 +15408,24 @@ export const IDL: MangoV4 = {
"type": { "type": {
"option": "f32" "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", "name": "perpCreateMarket",
"docs": [ "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", "name": "altSet",
"accounts": [ "accounts": [
@ -21472,12 +21834,24 @@ export const IDL: MangoV4 = {
"name": "forceClose", "name": "forceClose",
"type": "u8" "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", "name": "padding",
"type": { "type": {
"array": [ "array": [
"u8", "u8",
6 4
] ]
} }
}, },
@ -21612,12 +21986,30 @@ export const IDL: MangoV4 = {
"defined": "I80F48" "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", "name": "reserved",
"type": { "type": {
"array": [ "array": [
"u8", "u8",
1920 1900
] ]
} }
} }
@ -21745,12 +22137,28 @@ export const IDL: MangoV4 = {
], ],
"type": "u16" "type": "u16"
}, },
{
"name": "padding2",
"type": {
"array": [
"u8",
4
]
}
},
{
"name": "collateralFeeInterval",
"docs": [
"Intervals in which collateral fee is applied"
],
"type": "u64"
},
{ {
"name": "reserved", "name": "reserved",
"type": { "type": {
"array": [ "array": [
"u8", "u8",
1812 1800
] ]
} }
} }
@ -21872,12 +22280,27 @@ export const IDL: MangoV4 = {
], ],
"type": "u64" "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", "name": "reserved",
"type": { "type": {
"array": [ "array": [
"u8", "u8",
200 152
] ]
} }
}, },
@ -23647,12 +24070,16 @@ export const IDL: MangoV4 = {
"name": "temporaryDelegateExpiry", "name": "temporaryDelegateExpiry",
"type": "u64" "type": "u64"
}, },
{
"name": "lastCollateralFeeCharge",
"type": "u64"
},
{ {
"name": "reserved", "name": "reserved",
"type": { "type": {
"array": [ "array": [
"u8", "u8",
160 152
] ]
} }
} }
@ -24573,6 +25000,9 @@ export const IDL: MangoV4 = {
}, },
{ {
"name": "Swap" "name": "Swap"
},
{
"name": "SwapWithoutFee"
} }
] ]
} }
@ -24928,6 +25358,9 @@ export const IDL: MangoV4 = {
}, },
{ {
"name": "Serum3PlaceOrderV2" "name": "Serum3PlaceOrderV2"
},
{
"name": "TokenForceWithdraw"
} }
] ]
} }
@ -27845,6 +28278,76 @@ export const IDL: MangoV4 = {
"index": false "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": [ "errors": [
@ -28192,6 +28695,11 @@ export const IDL: MangoV4 = {
"code": 6068, "code": 6068,
"name": "MissingFeedForCLMMOracle", "name": "MissingFeedForCLMMOracle",
"msg": "Pyth USDC/USD or SOL/USD feed not found (required by CLMM oracle)" "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 = export type FlashLoanType =
| { unknown: Record<string, never> } | { unknown: Record<string, never> }
| { swap: Record<string, never> }; | { swap: Record<string, never> }
| { swapWithoutFee: Record<string, never> };
// eslint-disable-next-line @typescript-eslint/no-namespace // eslint-disable-next-line @typescript-eslint/no-namespace
export namespace FlashLoanType { export namespace FlashLoanType {
export const unknown = { unknown: {} }; export const unknown = { unknown: {} };
export const swap = { swap: {} }; export const swap = { swap: {} };
export const swapWithoutFee = { swapWithoutFee: {} };
} }
export class InterestRateParams { export class InterestRateParams {